mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 14:38:59 +02:00
de3bd653da07e09d0e3b5425af93e8fd73c860bd
424
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
de3bd653da |
Merge origin/main (v1.83.0.0, Memorable recall bridge) into tehran-v1; re-bump to v1.84.0.0
Conflicts resolved by keeping both sides: gstack-config enumerates design_detector, design_detector_install_prompted, and memorable_recall; the egress wiring test carries both new fail-closed sinks (design-detect-engine- download, memorable-recall) and both new module sinks; PROJECT_STRUCTURE's bin/ line names the design tools and gstack-memorable. This branch's CHANGELOG entry moves to 1.84.0.0 (dated today) above main's 1.83.0.0; VERSION, package.json, and the agents digest were written by gstack-version-bump. Main touched no template or resolver, so no render changed (gen-skill-docs --dry-run: all FRESH). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
caba78fefa |
v1.83.0.0 feat: Memorable recall bridge, opt-in and receipted (supersedes #2831) (#2836)
* feat: add optional Memorable workflow memory (cherry picked from commit |
||
|
|
92f85f1ba8 |
feat(design): ask once before downloading impeccable's engine
When the probe prints DESIGN_DETECTOR_INSTALL_OFFER the design skills ask the user one AskUserQuestion, in interactive sessions only (spawned or headless runs never install and never ask; Conductor gets the prose brief), before any other step: install the engine now, not now, never ask again (design_detector_install_prompted), or turn the detector off. A yes runs the receipted, checksum-pinned install and the skill continues with a READY probe. The brief says what impeccable is, what the one file is, where it goes, how it is verified and logged, and that no skill or hook comes with it; users who want the /impeccable skill run npx impeccable install themselves. design-review carries the brief inline (it is not carved). design-html keeps its skeleton small: the probe block points at a new read-on-demand section, sections/detector-install-offer.md, registered in its manifest and carve guard; its skeleton ceiling is re-measured (55,262) and its eager ceiling set to the measured 13,767. The review and ship passes state that they never offer an install. NOTICE.md, README, docs/skills.md, the interop design doc, and the CHANGELOG describe the new posture: gstack still never runs impeccable's installer or launcher; the one download is consented, pinned, and receipted. Ship goldens refreshed for the review-pass wording. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
3b7a2775ee |
feat(design-detect): consent-gated engine install, checksum-pinned and receipted
`gstack-design-detect.ts install` is the one download gstack makes, and only after a design skill's one-time question got a yes. It fetches the engine version gstack has tested (0.1.3) for this platform from impeccable's own GitHub release, verifies it against the checksum pinned in lib/design-detect-contract.ts (all five platforms, captured from the release's .sha256 sidecars; linux-x64 equals the fixture engine), writes an egress receipt before the fetch and refuses to download when the receipt cannot be written (fail-closed; the sink is registered in the wiring test's polarity table), caps the download at 32 MB, streams with the cap enforced, writes the file only after the hash matches, and places it under ~/.impeccable/bin/<version>/ (a trusted IMPECCABLE_HOME is honored; never inside a project). No skill, no hook, no launcher, no npx. --sha256 accepts a sidecar checksum for a version gstack has not pinned; --base allows a mirror (https, or http on loopback for tests). After a successful install the probe runs and its lines follow, so the skill sees READY at once. The probe ends with DESIGN_DETECTOR_INSTALL_OFFER (version, platform, bytes, destination) whenever it found no engine and the user has not answered the question; once design_detector_install_prompted is true it prints neither the offer nor the NOT_CACHED hint, which used to repeat on every run. The hint's npx wording is corrected: `npx impeccable detect --help` caches the engine for npx only, not where the probe looks. gstack-config gains design_detector_install_prompted (true|false, typo rejected, enumerated in list and defaults). Tests: a loopback mirror (async spawn, so the in-process server can answer) covers install, re-install as a verified no-op, checksum mismatch, 404, unpinned version, non-https base, design_detector off, and IMPECCABLE_HOME inside the repo; the offer and the silenced hint; pin completeness per platform. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
59258850ba |
fix(test): assemble the planted PEM block at runtime
The quality gate scans every added line of the PR diff through gstack-redact; the redaction test's literal PEM header was a HIGH finding on our own test file. The block is now built from fragments, so the scanned file never carries a key-shaped line while the test still plants a HIGH finding. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
a3bf2b054a |
fix(test): compare gen-skill-docs paths with forward slashes on Windows
gen-skill-docs prints repo-relative paths with the OS separator, so the checklist render pins (`GENERATED: review/design-checklist.md`) failed on the Windows lane against `review\design-checklist.md`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
eae2a4d056 |
fix(test): drive the DOM hygiene test through Playwright Chromium directly
Under the six-shard CI free suite the test's private browse daemon never answered its health probe (two minutes of retries), failed the shard, and starved two unrelated test files into failing before the runner's timeout. The test now launches the same Chromium through playwright-core and calls the dump function with page.evaluate, the way Aside's pg.evaluate does: no state file, no daemon, no health window. It self-skips when the Playwright Chromium bundle is absent. Two more hygiene rules are pinned along the way (print sheets keep their @media, alternate sheets are dropped). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
2f8ee6cb4d |
changelog: name the measure behind the test-count row
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
763671f7c3 |
docs: update project documentation for v1.82.0.0
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
9b2218a40b |
v1.82.0.0: impeccable interop, detector pre-pass, open DESIGN.md format
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
3f207247a8 |
fix(design): print and alternate stylesheets are not scanned as page CSS; no cross-run dump sweep; probe-state and design-system caveats in prose
- The dump inlined every linked sheet's rules as active CSS, so a print sheet's 12pt black text or an alternate theme produced tiny-text and palette findings the user never sees; disabled and alternate sheets are skipped and a media-scoped sheet is wrapped in its @media block. - The cross-run dump sweep is gone: two same-day reviews shared REPORT_DIR and one run's sweep deleted the other's dumps mid-audit. Dumps stay per run, owner-only, deleted after Phase 9 unless --keep-dom (now defined in the prose), and an interrupted run's dumps wait for the user. - Prose: design-system-* rows in DOM mode compare the page to THIS repo's DESIGN.md and apply only to the repo's own app; an empty scan JSON with exit 0 means the probe state changed since Setup (read stderr); the persist block names a missing dump instead of mislabeling it as a redaction block. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
69b0856d42 |
fix(design-md): markdown edge cases: rule-opened legacy files, spaced fences, ~~~ blocks, duplicate headings, YAML 1.2 numerics
- insertMarker keyed on "starts with ---", so a legacy file opening with a horizontal rule got a `# gstack:` line rendered as a heading that the parser then never read back (the conversion question re-asked every run). It keys on parsed front matter. - A closing front-matter fence with trailing spaces (`--- `) made a valid spec file `unknown`; the closer is any whole `---` line. - `~~~` fences hid nothing, so a `## ` inside one was a section boundary and a splice corrupted the fence; both fence kinds are tracked and only the same kind closes an opener. - convertLegacy silently kept the first of two `## Layout` bodies (and one of `## Color` / `## Colors`); it refuses with DESIGN_MD_CONVERT_REFUSED and the bin leaves the file and writes no backup. - needsQuotes covers 0x / 0o / .inf / .nan (YAML 1.2 numerics that changed type on round-trip); emitYamlBlock throws on an object inside an array instead of writing "[object Object]". - The design binary coerces the model's extraction JSON at the parse boundary (null names, missing arrays) so the paid call's result survives. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
3994ddfdd7 |
fix(design-detect): audit directories scan as dumps; scans print probe lines on stderr; refused base always exits 1; PATH loses project entries
Adversarial review (Claude subagent): - A DIRECTORY target under designs/ (the audit dir, which the prose hands the agent as REPORT_DIR) classified as an artifact, so the engine walked its dom/ subtree WITH inline ignores honored. Any directory under designs/ is now scanned as dumps. - A scan whose probe no longer finds an engine wrote its sentinel lines to stdout and exited 0, so `scan > "$_DJ"` captured "IMPECCABLE_NOT_AVAILABLE" as the scan result and the rendered bash read a clean scan. Probe lines go to stderr on every path; stdout is the JSON document or nothing. - A refused --changed base exited 0/2 when explicit targets were also given; it folds into the exit code (1 over 2 over 0). A trailing --changed no longer defaults to main. - A hand-edited `design_detector: Off` re-enabled the detector; the value is compared case-insensitively. - The engine inherited PATH entries inside the project (a direnv .envrc adding node_modules/.bin); those are filtered like every other project path. - DOM_DUMP_MISSING names the case where the dump script wrote nothing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
fb8c64b20c |
fix(design): dump hygiene covers CSS URLs, srcdoc, and handlers; dumps persist owner-only; ignore prose treats repo config as evidence
- The dump script cuts query strings from CSS url() in style attributes, <style> nodes, and the inlined stylesheets (signed asset URLs), empties srcdoc, and covers background and xlink:href. - Persisted dumps are chmod 600; MEDIUM redaction findings persist (an authenticated page shows emails) and the prose says so; earlier runs' dumps are swept before the first dump of a run unless --keep-dom. - The Aside dump prose asks for `'` in a pasted URL to be percent-encoded (a bare single quote would end the script) and never to paste an unread URL. - Repo-config ignores are evidence, not settled decisions, in /review, /ship, and design-review's probe prose; the scan JSON's text fields are named as untrusted. - design-html's skeleton ceiling is re-measured (54,545); ship goldens refreshed for the checklist prose. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
cb9ba7a3e2 |
fix(design-md): edits follow a symlinked DESIGN.md, keep the BOM and the majority line ending, refuse an unclosed fence
- `mark`, `convert --write`, and the design binary's extraction replaced a symlinked DESIGN.md (a docs-site layout) with a regular file and left the real target untouched; both writers resolve the link first. - A single stray CRLF flipped a whole LF file to CRLF: the editors now keep the majority ending. A UTF-8 BOM broke format detection and ended up mid-file after `mark`; it is recognized and kept at byte 0. - Re-running `mark` on a marked file deleted the blank line after the marker (`\s*$` matched across the newline); the marker regexes use `[ \t]*`. - Fences: readers follow markdown (an unclosed fence runs to EOF); the text-level editors refuse such a file with DesignMdEditRefused (DESIGN_MD_EDIT_REFUSED) instead of splicing the wrong section, and the design binary reports that and leaves the file alone. - needsQuotes also quotes a scalar containing ` #` (an inline-comment shape parsed back as a truncated value). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
8d709c8f29 |
fix(design-detect): project means below HOME; only page dumps drop inline ignores; a whole-scan budget; prototype-safe rule counts
Third review cycle + Red Team, all reproduced before the fix: - With no repository, the wrapper adopted cwd as the repo root, so a review launched from HOME (URL mode can run from anywhere) rejected every HOME-rooted install as "repository-local", reported the user's own skill install with the wrong hint, and, for targets, accepted all of HOME (~/.ssh/id_rsa scanned). A project directory is now one strictly below HOME: `git init ~` never turns the user's installs into repository files, and from HOME only the designs allow-list qualifies as a target. - --no-inline-ignores keyed on "not inside the repo", which misclassified dumps when GSTACK_HOME sits under the repo and stripped the design-html gate's own `<!-- impeccable-disable -->` from finalized.html. Targets are classified as project / dom-dump (designs/<audit>/dom/**, the page's bytes) / artifact (other designs/ files, gstack-authored); only dumps drop inline ignores. - A repository's .impeccable/config.json can hide rules from the review; detector.ignoreValues was never surfaced. The probe prints IMPECCABLE_IGNORED_VALUES beside the rules, and the prose stops calling repo-config ignores "a decision the user made". - An engine id named `constructor` corrupted byRule through Object.prototype and `__proto__` counts vanished; byRule is a null- prototype object and an id that fails the shape check is `unmapped` as a key too. - Batches ran with no total budget (10,000 un-ignored files: hours). The scan stops at 5x the per-batch timeout with DETECT_TIMEOUT and exit 1. - The scan JSON carries an `untrusted` list of the engine- and page-derived fields, so the agent reading past the fenced DETECT_TOP block is told what is evidence. - The PATH walk keeps launcher-present for a .cmd wrapper or a differently named real file (the name gate applies to READY only). Tests: probe and scan from a fake HOME (cache READY, HOME file refused, dump scanned without inline ignores), artifact vs dump batches, prototype-member ids, the whole-scan budget over 11 batches, ignoreValues surfaced, the `untrusted` field. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
982a738663 |
fix(design): Aside dump script stays single-quoted; redaction gate sized to the dump cap; doctrine made consistent
- The DOM-dump Aside block was the only double-quoted `aside repl` script in
the tree (to splice the function text), which put the agent-filled <url>
inside a double-quoted bash string: a same-origin href carrying $(...) would
run in the reviewer's shell when Phase 3 opened that page. The script is
single-quoted like every other Aside script and the function text enters
through a closed-quote segment ('"$_DUMP"'); the fallback line is
`$B js '('"$_DUMP"')()'`. A free test pins that no rendered Aside script
opens with a double quote.
- The persist block capped dumps at 10 MiB but ran gstack-redact with its
1 MiB default, so every real page between the two was deleted as
DOM_DUMP_REDACTION_BLOCKED; the gate passes --max-bytes at the dump cap and
blocks on any exit other than clean (0) or MEDIUM (2), so a redaction tool
that fails to run can no longer fall through to "persist".
- Dump hygiene removes <template> and <noscript> subtrees (invisible to the
attribute walk), inline on* handlers, and the cross-origin <link> nodes
already named in the note, so the file handed to the engine references no
remote stylesheet.
- Doctrine: the Codex design-voice prompts said "2-3 intentional motions"
against the one-authored-moment rule; the overused-display heading scoped
its ban to Persuade/Experience while the catalog and hard rules ban it
everywhere; design-consultation's Important Rule 4 still said "as primary";
design-html's blacklist header is now "Never include by default" with the
mockup/DESIGN.md/user-ask override the catalog grants; the slop gate honors
Decisions Log and Do's and Don'ts blessings like /review does; the landing
"poster" line says poster in stance, not type size; the design binary's
variant dials no longer flip light/dark for variety; gstack's DESIGN.md
rows name data labels (UI labels stay the DM Sans token) and call the
skill-bar fill and hovers functional transitions.
- design-review names how the base branch is found (gh pr view, then the
repo default; never main) for the source-mode scan and the diff-aware mode.
- frontend-scope matches the config globs at the repo root only, like the
bash arm; the parity test carries nested samples.
- Cleanups: renderCatalog's stale style option, an unused import, the
identity-map bannedFontNames, the checklist header's "same entries" claim,
the catalog header's consumer list, the orphaned main() docstring, the
plan doc's IIFE bullet. design-html's skeleton ceiling is re-measured
(54,184) for the two doctrine sentences.
Tests: AUTO-FIX rendering from the catalog, the E2E slice markers checked in
the free suite, the hygiene cases for templates/noscript/handlers/remote
links, and the review E2E counting detector rows separately from the seven
checklist plants.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
||
|
|
ae5a5298e0 |
fix(design-md): text-level edits keep CRLF, one section-boundary rule, control characters quoted
- insertMarker and spliceSection normalized every line ending to LF, so a CRLF DESIGN.md came back rewritten beyond the one line they promised to touch. Both detect the file's dominant line ending and restore it. - parseDesignMd and spliceSection each walked headings with their own fence tracking; they now share headingLines (and upsertSection shares headingMatches). An unclosed ``` is treated as prose for that file: it used to swallow every later section on a splice. - A token value carrying a control character (an LLM-extracted font family with an embedded newline) was emitted as a bare multi-line scalar that Bun.YAML rejects, turning a freshly written DESIGN.md into frontmatter-unparsable; needsQuotes routes it through the quoted form. - The marker-line regex variants are built once beside YAML_MARKER_RE; the dead setMarker export and a no-op ternary are gone; LEGACY_HEADINGS derives from the identity list; the header diagram names the text-level editors as the write path for user-owned files; the bin validates and prints the mark choices from FORMAT_CHOICES. Tests: CRLF round-trips for both editors, a fenced ## inside a section and an unclosed fence, and a newline-bearing scalar parsing back. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
b2e67d0097 |
fix(design-detect): an engine is a file named impeccable outside the project; DOM dumps scan without inline ignores
Second review cycle, security + checklist: - IMPECCABLE_BIN=/bin/sh (or node) was READY, and `detect` with cwd=repoRoot made the interpreter run the repository's own `detect` file. Every engine candidate (env override, PATH entry, cache, sibling) is now judged by the realpath of the FILE and must be named impeccable[.exe]; PATH and cache candidates that resolve into the repository are skipped like the others. "Inside the project" means the repository, or cwd when cwd is a project directory: HOME and its ancestors are exempt, so a URL-mode review launched from HOME still finds the HOME-rooted installs. - A base for --changed that starts with `-` was spliced into git argv (`--output=<file>` made git write a file and report no changes); an option- like or missing base is DETECT_REFUSED (not a ref name), exit 1, and the parser no longer defaults a missing value to main. - DOM dumps are the audited page's bytes, so an in-file `impeccable-disable` comment there is page-controlled: batches under the designs root run with --no-inline-ignores, repository batches keep the project's own ignores. - neutralizeSentinels covers the shapes it missed (bare sentinels such as DETECT_TOP total= and IMPECCABLE_DISABLED, the DETECT_EXIT_CODE= echo, the `[rule-id] impact=` group header) in one precompiled alternation instead of 37 replaceAll passes per field; only kept findings are normalized, and the summary's total stays the engine's count. - The minimal engine environment compares keys case-insensitively on Windows (process.env enumerates Path, SystemRoot there) and passes PATHEXT, COMSPEC, HOMEDRIVE, HOMEPATH, PROGRAMDATA. - Bare 64s move into DETECT_LIMITS; the unused SentinelName type is gone; the header states the directory-target contract (the engine's own walk). Tests: an interpreter as IMPECCABLE_BIN never runs the repo's detect file; a PATH symlink into the repository is never READY; option-like and empty bases are refused with no file written; the designs-root batch carries --no-inline-ignores and the repo batch does not; the identity label is deterministic per binary; the bare-sentinel and header shapes are neutralized; the installed fake engine works without IMPECCABLE_FAKE_OUTPUT (the helper copies the sample beside it); two tests clean up in finally. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
da6f0ff2f6 |
fix(design): run the DOM dump in the page on both engines; align doctrine with the catalog
The DOM-dump script is an arrow function, not a self-calling IIFE: Aside's `pg.evaluate($_DUMP)` receives the function and runs it in the page (the IIFE form executed in the repl sandbox, where `document` does not exist), and the fallback engine calls it with `$B js "($_DUMP)()" --out --raw`. Hygiene widens to every URL-bearing attribute (src, srcset per candidate, poster, action, formaction, data, ping, cite lose their query strings and fragments) and to data: URLs inside existing <style> nodes. The persist and scan blocks restate REPORT_DIR and RUN_ID literally instead of relying on a shell variable from an earlier block; the baseline's targetSet is defined per mode (repo-relative paths in source mode, page slugs in DOM mode) so DOM-mode deltas can match; the PR-body Detector line lists the states the probe can actually print. The DOM fixture is re-captured with the new script from outside the repo (the engine walks up from cwd for DESIGN.md, which the metadata now records). Doctrine contradictions the design specialist found: the landing-page motion rule matches the one-authored-moment reflex; the background rule names the catalog's halo/spotlight/stripe/grid slop instead of asking for gradients; the universal font rule is scoped to the display voice with the body/UI exceptions; "two typefaces max" allows the mono; the methodology's banned-font line renders BANNED_FONTS; Courier New is banned outright; the Brutalist, Retro-Futuristic, and Playful menu entries stop recommending system stacks, glow, and bounce; the coherence nudge uses the decoration vocabulary; Path A's gate names the display voice; font-loading prose points at the source the procedure verified; centered-everything is MEDIUM (an aggregate heuristic); the mockup guard reads "Never by default (unless the brief above asks for it)". The checklist's AUTO-FIX list renders the catalog's auto-fix rules; category 9 and the Hard Rules pointer count from the same partition helpers (detectorSlopEntries, judgmentTellEntries); the handoff list renders from HANDOFF_COMMANDS; a missing catalog id fails gen-skill-docs by name. gstack's own DESIGN.md gains border tokens and Decisions Log rows for its live-feed pulse and 11px mono labels. frontend-scope is case-sensitive like the bash arm. gen-skill-docs shares one emitGenerated helper for sections and lib-derived assets; renderCatalog keeps the one style with a caller. Tests: shared sliceBetween that fails on a missing end marker, the slop-gate fixture's real end marker, an isolated browse daemon for the DOM-mode E2E, the DOM hygiene test gated to CI or opt-in, docs notes for the two superseded plan sentences. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
b4d88a0126 |
fix(design-md): mark and updateDesignMd never rewrite the user's file; refuse a contradictory mark
renderDesignMd re-sorted canonical section names into spec order on every render, so `gstack-design-md mark legacy-keep` (the "leave it alone" answer) and the design binary's mockup extraction reordered a legacy DESIGN.md (Typography and Layout jumped to the top) and normalized its whitespace, while the bin promised "body bytes untouched". `mark` now splices only the marker line (insertMarker) and `updateDesignMd` splices only its own section (spliceSection); every other byte of an existing file is preserved, and spec order applies only to files that open with front matter. `mark` refuses a choice that contradicts the file's format (spec on a non-spec file, legacy-keep on a spec file) with DESIGN_MD_CONVERT_REFUSED, exit 2, file unchanged. convertLegacy keeps intro prose under the title instead of rebuilding the preamble from the title alone. detectFormat returns a machine-readable `code` beside the prose reason (the bin no longer branches on reason text); the marker regexes derive from FORMAT_MARKER_PREFIX and FORMAT_CHOICES; the hop limit and legacy identity headings are named constants; slug is exported and reused; both writers use lib/fs-atomic.ts. Tests pin byte identity for mark and updateDesignMd on the legacy fixture, the refusal paths, and the preserved preamble. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
60758c5ddf |
fix(design-detect): never execute a repository-controlled engine; allow-list --changed targets; sanitize engine text
Pre-landing review findings (security + checklist), all reproduced before the fix:
- A checked-out branch could commit `.claude/skills/impeccable/scripts/bin/<os>-<arch>/impeccable`
and the probe would report READY and `scan` would run it, with the agent's full
environment. Launchers and sibling engines under the repo or cwd now count as
"skill present" only (IMPECCABLE_NOT_CACHED: repository-local install, and the
hint never names a repository-local launcher to run); only HOME-rooted installs,
IMPECCABLE_BIN, the cache, and PATH entries outside the repo qualify, all by
realpath. The engine now sees a minimal environment (PATH, HOME, TMPDIR, locale,
IMPECCABLE_*), never the agent's tokens.
- `scan --changed <base>` pushed git-derived paths without the allow-list, so a
committed symlink with a frontend extension handed a file outside the repo to
the engine. Derived targets now go through the same allow-list as explicit ones
and symlinks named by git are refused outright.
- A repo-controlled `scripts/VERSION` with embedded newlines forged probe lines;
the version is trusted only when it is semver, and every printed version is
sanitized. Engine text containing the untrusted-content fence or a
`SENTINEL:` prefix is neutralized with a zero-width space
(neutralizeSentinels in the contract), so page text cannot close the envelope
or forge a probe line.
- A failing `git diff <base>...HEAD` (unknown or unfetched base) was swallowed
and read as "no frontend changes"; it is now DETECT_REFUSED with exit 1.
- The scan allow-list root follows `${GSTACK_HOME:-$HOME/.gstack}` like the
templates and gstack-slug (config.yaml keeps gstack-config's STATE_ROOT
precedence); a quoted or commented design_detector value reads correctly.
Smaller: raw engine chunks are kept only in --format raw; diagnostics are
capped (200 kept, 20 echoed); the engine identity hash reads size + 4 MB, not
the whole binary; PROBE_STEP and ENGINE_STDERR are contract sentinels; the
--verbose gate covers every probe step; analytics use one sentinel vocabulary;
bare limits live in DETECT_LIMITS. The fake engine's knobs are IMPECCABLE_FAKE_*
(so they pass the minimal env) and a shared test helper installs it. New tests
cover each item above plus clean runs, `{}` parse errors, missing paths, and the
50 MB stdout cap.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
||
|
|
9a03aa5e4a |
test: touchfiles, tiers, shim E2E, real-engine fixture
Touchfiles: the catalog, contract, detector bin, checklist resolver, review- army resolver, and DESIGN.md lib join the dep lists of review-design-lite, design-review-fix, the design-consultation cases, and plan-design-review-no- ui-scope, so editing any of them re-selects the tests that read their output. Three new E2E keys: design-review-detector-shim (gate; source mode on a feature-branch diff), design-review-detector-shim-dom (gate; DOM mode: the slop fixture served on loopback, dumped through the browse binary with lib/dom-dump.js, persisted under a GSTACK_HOME-scoped REPORT_DIR, scanned once; self-skips when browse/dist/browse is absent), and design-html-slop-gate (periodic; one fix pass, at most two scans, remaining findings accepted with reason). Every case reaches the engine through test/fixtures/fake-impeccable.ts via IMPECCABLE_BIN from outside the temp repo, reads extracted skill sections (never a whole SKILL.md) with the installed bin path rewritten to this checkout, and asserts the probe ran, the right scan verb ran, `npx impeccable` never did, and the output carries FINDING rows tagged [ai-color-palette] and [low-contrast]. review-design-lite gets the fake engine and an eighth tally signal for a detector row; its 4-hit threshold is unchanged. test/gstack-design-detect.test.ts evaluates design-review's REPORT_DIR expression with GSTACK_HOME set and proves a dump under it is accepted by the wrapper's allow-list. The sample fixtures were real captures from commit 1 (engine 0.1.3), so there is nothing hand-written left to swap. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
f1ce46eb8b |
docs: NOTICE, Apache license text, README interop, project structure
NOTICE.md names what gstack derived from impeccable (rule ids and names in the catalog and the registry fixture; the visitor modes, craft-floor reflexes, and calibration in the design resolvers; the font procedure in the consultation template) and from Google's DESIGN.md specification (the format lib/design-md.ts implements), states that gstack does not distribute or audit the impeccable engine, and points at licenses/Apache-2.0.txt (verbatim). README: the design-consultation, design-review, and design-html rows say what changes when impeccable or the open DESIGN.md format is in play, and a "Works with impeccable" paragraph explains the pre-pass, the shared ids, PRODUCT.md and DESIGN.md as the shared surface, the handoffs, the no-nag posture without impeccable, and the off switch. docs/skills.md gets the detector paragraph under /design-review. docs/PROJECT_STRUCTURE.md lists the new lib and bin files, NOTICE.md, and licenses/. docs/designs/IMPECCABLE_INTEROP.md promotes the CEO plan (its ~/.gstack copy is flipped to PROMOTED) with a "what shipped" summary. TODOS.md files the seven deferrals from the reviews: the design-review Phases 7-11 carve (the budget lever, with the +2.87K vs 2.5K landing recorded), the Bun .env audit across bin/*.ts, the Kiro bin/lib gap, the $D check slop rubric, taste-profile interplay, the CEO Section 11 bullets, and the scan cache. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
59cfa1547f |
chore(design): convert gstack's own DESIGN.md to the open spec
`gstack-design-md convert --write` on the repo's DESIGN.md: tokens in YAML front matter (typography.display/body/label/mono, colors with their light/dark qualifiers, spacing scale, rounded scale), Overview from Product Context and Aesthetic Direction, Colors / Typography / Layout as canonical sections, Motion, Grain Texture, and Decisions Log preserved as extras, format marker on line 2. Hand-checked; `check` reports spec with no token-reference errors. A Decisions Log row records the conversion and that DM Sans stays the body face: it is on the overused-as-display list, and body/UI use on an Operate surface is the allowed exception under the role-scoped rule. The pre-conversion file lives on as test/fixtures/design-md-legacy.md, which test/design-md.test.ts now uses for its legacy cases; the converted root file is asserted to be spec. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
4308e487aa |
feat(design): PRODUCT.md prefill + /impeccable handoffs
design-consultation's context gathering and design-shotgun's auto-gather read PRODUCT.md (impeccable's product-context file) when it exists: it counts as the user's prior answers, gets confirmed in one line, and is never re-asked. Neither skill opens `.claude/skills/impeccable/**`; PRODUCT.md and DESIGN.md are the shared surface, and impeccable's prose never loads inside a gstack skill. Handoffs: ship's review-lite ends each NEEDS INPUT detector row with the `handoff=` command the scan printed (`/impeccable <cmd>`) when the probe reported IMPECCABLE_SKILL: present, recommending the command and never opening its files; design-review's Phase 7 does the same for deferred findings, and `design_detector: off` silences handoff lines with the rest. Codex and Factory ship goldens refreshed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
35d641b4b2 |
feat(design): write/read DESIGN.md in the open spec; persisted format choice
gstack's design skills now write DESIGN.md in the open DESIGN.md format and
read tokens from it. {{DESIGN_MD_CHECK}} renders the format check through
bin/gstack-design-md.ts: design-consultation's Phase 0 settles the format once
(spec → update tokens in the front matter; legacy without a marker → one
AskUserQuestion: convert with a .legacy.bak, keep the legacy file, or start
fresh; the answer is written into the file as the format marker so no skill
asks again; a marker already present is obeyed silently; unknown → prose;
missing → Phase 6 writes one). Phase 6's template is the spec form: YAML front
matter with name, description, and exactly the five token groups (colors,
typography.display/body/label/mono with fontFeature: tnum on mono, rounded,
spacing, components with {path} references), then Overview (Creative North
Star, product context, mode per surface, references, key characteristics),
Colors (opening with the Restrained / Committed / Full palette / Drenched
strategy), Typography, Layout, Elevation & Depth, Shapes, Components, Do's and
Don'ts, plus gstack's Motion and Decisions Log as extras; the template ends
with a check that the file parses as `spec`.
design-review runs the `:calibrate` form in Setup: a spec file's flat tokens
are the calibration source (a value present in the tokens is never a finding),
the marker is respected, and conversion is never offered there; its DESIGN.md
export writes the spec form. design-html's token extraction writes the spec
form and respects an existing choice. review/design-checklist.md category 5 and
ship's review-lite step 1 name `gstack-design-md tokens` as the calibration
source; plan-design-review Pass 5 cites tokens by path when front matter
exists.
The contract owns the bin's DESIGN_MD_MARKER / REASON / WRITTEN / BACKUP
lines; the contract test's pending list closes. Carve guard: design-
consultation skeleton 66,500 → 67,500 (measured 67,014; +1,508 B against the
1.5 KB cap). Codex and Factory ship goldens refreshed (review-lite step 1).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
||
|
|
e49d1eea49 |
feat(lib): open DESIGN.md reader/writer + gstack-design-md bin
lib/design-md.ts implements the open DESIGN.md format (google-labs-code/
design.md, Apache-2.0): YAML front matter with the five token groups (colors,
typography, rounded, spacing, components) and eight canonical `##` sections
in spec order (Overview, Colors, Typography, Layout, Elevation & Depth,
Shapes, Components, Do's and Don'ts), aliases mapped, extras preserved after
them in their original order. parseDesignMd never throws (unparsable front
matter → `unknown` with a reason); renderDesignMd re-emits the preserved front
matter bytes and only `convert` writes fresh YAML through a small block-style
emitter (Bun.YAML.stringify is flow style); upsertSection splices the body
only; tokensFlat resolves `{path}` references to primitives and reports group,
self, dangling, and cyclic refs as DESIGN_MD_TOKEN_REF_INVALID. convertLegacy
turns gstack's pre-spec DESIGN.md into the open format: Product Context and
Aesthetic Direction fold into Overview, Typography roles become
display/body/label/mono tokens (mono carries fontFeature: tnum), Color hexes
become colors (mode-qualified labels keep their qualifier; strategy lines are
not colors), the Spacing scale and Layout radii become spacing and rounded,
Motion / Grain Texture / Decisions Log survive as extras. The format marker
lives inside the file: a YAML comment on line 2 of a spec file, an HTML
comment on line 1 of a legacy file.
bin/gstack-design-md.ts: `check` (DESIGN_MD_FORMAT + marker), `convert
[--write]` (backup to DESIGN.md.legacy.bak, temp+rename, refuses ambiguous
input with DESIGN_MD_CONVERT_REFUSED), `tokens` (flat JSON), `mark
<spec|legacy-keep>`. Exit 3 + DESIGN_MD_INTERNAL_ERROR is a gstack bug.
design/src/memory.ts: updateDesignMd upserts "Extracted Design Language"
through the lib (front matter bytes untouched, canonical order kept, section
replaced on rerun) and creates a spec skeleton with tokens from the extraction
when no file exists; readDesignConstraints leads with the flat tokens and the
Overview for spec files. The design binary still bundles.
test/design-md.test.ts pins all of it against gstack's own DESIGN.md.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
||
|
|
d3c3ea27b9 |
feat(design): catalog never-lines in the mockup prompt
Ten catalog ids carry `mockupNever` (kicker-above-heading, icon-tile-stack, gradient-text, ai-color-palette, cream-palette, nested-cards, dark-glow, pulsing-dot, identical-cards, hero-metrics) and lib/design-catalog.ts exports their deduped plain-English names as MOCKUP_NEVER_NAMES. briefToPrompt() in the design binary appends "Never: <names>." before its fixed tail, so `$D generate | variants | evolve` stop reaching for purple gradients, icon tiles, and cream defaults before the comparison board opens. The binary still bundles (`bun build --compile design/src/cli.ts`); ./setup rebuilds it. design-html's Never-include list now covers every mockupNever id (kicker / icon tile, hero metric rows, gradient text, cream palette, nested and identical cards, glow and pulsing dots), each line tagged with its catalog ids; test/design-catalog.test.ts pins the exact ten flags, the deduped names, and that the template list is a superset. New design/test/brief.test.ts pins the prompt shape. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
3522073ef0 |
feat(design): {{DESIGN_DETECTOR}} wired into design-review, ship review-lite, review army, design-html
The user-installed impeccable engine becomes a deterministic pre-pass in four
skills, through one resolver with three renders: {{DESIGN_DETECTOR}} (the probe
block and how to read every sentinel), {{DESIGN_DETECTOR:phase0}} (design-
review's mechanical scan), {{DESIGN_DETECTOR:gate}} (design-html's bounded slop
gate). Every rendered invocation is `bun --no-env-file run <bin>/gstack-design-
detect.ts ... --host <host>` and every scan ends with the DETECT_EXIT_CODE echo
so exit 2 (findings) never aborts a block.
design-review: probe in Setup; Phase 0 picks DOM mode (URL target) or source
mode (diff-aware, no URL) once; source mode scans the changed frontend files in
Setup, DOM mode never reads source (Rule 4). Phase 3 gains a DOM-dump step per
page: both browser engines load the shared script from lib/dom-dump.js (Aside
splices it into a double-quoted repl script; the fallback engine copies it into
a temp dir for `$B eval --out --raw`), the dump is size-capped, run through
gstack-redact (a HIGH finding skips the page), and persisted under
$REPORT_DIR/dom/$RUN_ID/; one scan runs after the last page, labeled "static
scan of the rendered DOM; cross-origin CSS not resolved". REPORT_DIR honors
GSTACK_HOME so the wrapper's allow-list and the report dir agree; RUN_ID is set
once in Setup. design-baseline.json is schemaVersion 2 with runId, targetSet,
base, and a detector block (mode, engine, byRule, byPage), written temp+rename
with a per-run copy; Regression Output diffs ids only when mode and target set
match, caveats an engine change, and calls live-page count deltas advisory.
Phase 7 hands deferred detector findings to the `handoff=` command the scan
printed; Phase 9 recomputes the same way and deletes the dumps unless
--keep-dom; Phase 10 reports `Detector: N → M`.
ship review-lite gains step 0 (probe, `scan --changed <base>`, tier buckets,
detector + checklist dedupe, advisory and ignored never count) and a
`detector` count in its log payload; the PR body gets a Detector line (rule
ids and counts only). The Review Army Design specialist runs the mechanical
pass at the top of review/design-checklist.md, which now carries it. design-
html probes after DESIGN_SETUP and runs the one-pass gate before screenshots.
lib/dom-dump.js is generated by gen-skill-docs from lib/dom-dump-script.ts
(Claude host, --out-dir aware, dry-run freshness) and pinned byte-equal, so the
prose never carries the script. The contract gains DETECT_JSON, DOM_DUMP_OK,
and the self-describing set; its test now checks both directions.
Budget: design-review eager 25.6K → 28.5K. The plan's target was +2.5K; after
the levers it named (ids-only detector rules, no inline script, trimmed prose)
it lands at +2.87K, and the remainder is doctrine and detector wiring, so the
ceiling moves to the captured 31,319 for design-review only (the full capture
would also have loosened 21 ceilings this branch never touched; those stay).
design-html skeleton re-baselined to 54,000 (measured 53,592). Codex and
Factory ship goldens refreshed (review-lite step 0 and the PR-body line render
inline there).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
||
|
|
3867dae355 |
feat(bin): gstack-design-detect wrapper + design_detector config key
bin/gstack-design-detect.ts finds and runs an impeccable engine the user installed; it never installs, downloads, or executes anything that could download. `probe` reads only: config (design_detector off → DISABLED), IMPECCABLE_BIN (absolute, realpath outside the repo and cwd), a PATH walk (absolute entries outside the repo; a #! shim counts as launcher-present, never READY), the ~/.impeccable/bin/<newest semver>/ cache, and the engine installed beside a skill launcher (scripts/bin/<os>-<arch>/impeccable, the layout a real install produced). It reports IMPECCABLE_SKILL, host-aware IMPECCABLE_HOOK (+ HOOK_OTHER), the ignore lists from .impeccable/config*.json, IMPECCABLE_ENGINE_UNTESTED for versions outside the fixture set, and a hint only when a launcher exists without its engine. `scan` re-probes, refuses URLs and anything outside the repo root or the design-report allow-list (realpath, so symlinks cannot escape), derives `--changed <base>` targets NUL-safely through git and lib/frontend-scope.ts, batches 100 absolute paths per engine call with stdin ignored, a SIGKILL timeout, a 50 MB stdout cap, and sanitized length-capped fields, then prints one normalized JSON document (--format gstack) or the engine's bytes (--format raw); DETECT_TOP (fenced as untrusted content), DETECT_SUMMARY, and DETECT_EXIT go to stderr; exit code passes through with 1 over 2 over 0; exit 3 is a gstack bug. `rules` prints the mapped set. Every run appends a content-free line to the local analytics file. lib/design-detect-contract.ts owns every sentinel string, the limits, and the normalized-finding shape (pure module); test/design-detect-contract.test.ts asserts every sentinel-shaped token the agent can read exists there. lib/frontend-scope.ts mirrors gstack-diff-scope's frontend arm, pinned by a parity test that runs the bash script. bin/gstack-config gains design_detector (auto | off, default auto, invalid values rejected with the file unchanged). test/fixtures/fake-impeccable.ts is the env-driven engine stand-in; test/gstack-design-detect.test.ts covers READY/NOT_CACHED/ NOT_AVAILABLE/DISABLED, env trust (.env never loaded, in-repo IMPECCABLE_BIN ignored), newest-semver cache, hook and ignore detection, refusals, exit passthrough, raw byte-identity, normalization, the display cap, timeout, parse errors, diagnostics, --changed, and analytics. The egress scanner test records the wrapper as a documented non-sink. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
71505f8374 |
feat(design-consultation): font procedure, role-scoped overused list, color strategies
The proposal section stops handing out a font menu. "Choosing faces: a
procedure, not a menu" names the subject's world, shortlists per role,
strikes the overused list for that role, verifies availability in-session,
and states the loading strategy. {{OVERUSED_FONTS}} renders the role-scoped
lists from lib/design-catalog.ts: overused as display (the detector's
overused-font set plus the training-data defaults), fine as body/UI on an
Operate or Read surface, mono for data and code, banned in any role, and a
short verified-free list with its verification date. Color approaches become
Restrained / Committed / Full palette / Drenched. The anti-convergence
directive drops light-vs-dark as a dial (it comes from the use scene) and the
three-looks calibration sits under Your Design Knowledge. The slop list is
{{DESIGN_SLOP_BULLETS}}: prose from the catalog, no rule ids, polish-level
tells omitted.
design-html's "Never include (AI slop blacklist)" list keeps its literal
(carve guard) and each line now carries a trailing <!-- id --> naming a
catalog entry, pinned by test/design-catalog.test.ts so the last surviving
duplicate is derived-by-test. Both resolvers are registered and listed in
ARCHITECTURE.md. No carve-guard or budget re-baseline needed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
||
|
|
9825bfdbb7 |
feat(design): modes, craft-floor reflexes, calibration, catalog in doctrine
DESIGN_HARD_RULES: the classifier names four visitor modes (Persuade, Operate, Read, Experience, plus Hybrid per section) and keeps the MARKETING/LANDING PAGE and APP UI aliases; Read and Experience get three rules each; a "Reflexes no detector catches" block (browser surfaces, one authored motion moment, depth has an offset, tinted secondary text, space above headings, light-or-dark from the use scene) and the three-looks calibration follow the universal rules. The slop section renders the 11 legacy lines plus the detector rule ids and judgment tells from the catalog; in design-review, which also renders DESIGN_METHODOLOGY, it becomes a one-line pointer so the catalog is paid for once. Header counts are computed, not hardcoded. DESIGN_METHODOLOGY: category 9 renders the catalog in three registers (legacy lines verbatim, detector rules that need judgment with bracketed ids, gstack-only judgment tells as prose, polish-level ids on one line); categories 5 and 7 carry the browser-surface and one-motion-moment reflexes; the typography overused-face item points at [overused-font] with the role-scoped exception. The consultation Codex prompt's anti-slop line reads from the catalog. Budget: design-review eager 25.6K -> 27.0K (ceiling 27,984), plan-design-review unchanged at 17.4K; no carve-guard or context-budget re-baseline needed; ship goldens unchanged (ship never renders the hard rules). Derived from pbakaus/impeccable reference/craft-floor.md + new-work.md (Apache-2.0), rewritten. See NOTICE.md (commit 12). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
11c2a33ae9 |
feat(review): generate review/design-checklist.md from the catalog
review/design-checklist.md was hand-written and its own header admitted it drifted from DESIGN_METHODOLOGY category 9. It is now rendered by scripts/resolvers/design-checklist.ts from lib/design-catalog.ts: category 1 lists every grep-detectable slop entry plus the legacy blacklist lines, sorted HIGH/MEDIUM/LOW, each with its heuristic and, where the detector knows the rule, its bracketed id (27 items, up from 6). The font blacklist renders from BANNED_FONTS. Categories 2-5, Instructions, Classification, Output Format, and Suppressions keep their prose. Title and slop heading are unchanged (test/skill-e2e-review.test.ts and hosts/opencode.ts key on them). gen-skill-docs writes the file for the Claude host only (a Claude-side runtime asset; other hosts copy or inline the render), honors --out-dir, and reports STALE/FRESH under --dry-run like sections do. test/design-checklist-sync.test.ts pins committed == generated, the host/out-dir scoping, and the dry-run freshness line. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
e34541c3bd |
refactor(design): typed slop catalog in lib/; AI_SLOP_BLACKLIST derived
lib/design-catalog.ts is the single source of truth for gstack's design
anti-pattern vocabulary: the 11 legacy blacklist lines (verbatim, flagged
`legacyBlacklist`), every one of impeccable's 61 registry ids with gstack
prose, tier, impact, confidence, grep heuristic, and /impeccable handoff,
plus the gstack-only tells the LLM pass judges (hero metrics, identical
cards, glassmorphism, missing states, unthemed browser surfaces, ...).
`impeccableId` is set only when the id exists in the registry fixture, and
`renderCatalog({style:'ids'})` brackets an id only then, so rendered prose
never shows an id the detector cannot emit. Role-scoped font lists
(OVERUSED_FONTS_DISPLAY, BANNED_FONTS, FONTS_BODY_UI_OK, FONTS_MONO_OK,
FONTS_VERIFIED_FREE) live beside the entries.
scripts/resolvers/constants.ts now derives AI_SLOP_BLACKLIST from the
catalog. Generated output is byte-identical (bun run gen:skill-docs is a
zero diff). Pure module: no I/O, no scripts/ imports, loading prints
nothing, so bin/ can import it at runtime on every host.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
||
|
|
8ef842fc75 |
chore(design): pin impeccable rule ids and detector JSON shape as fixtures
Real captures from a human-initiated `npx impeccable install` in a scratch directory (engine 0.1.3, linux-x64), never a runtime download: - test/fixtures/impeccable-antipatterns.json: upstream crates/live/assets/antipatterns.json at 87d8f6d6 (the state engine-v0.1.3 shipped), 61 rules, source commit recorded in `_source`. - test/fixtures/impeccable-detect-sample.json: `detect --json` over gstack's planted-slop fixture (source mode), paths normalized. - test/fixtures/review-eval-design-slop.dom.html + impeccable-detect-dom-sample.json: the same page served locally, dumped through the browse engine with the shared DOM-dump script, then scanned. Pins the load-bearing assumption that the static engine reads inline <style> in a .html file: the DOM scan yields the same id set as the source scan. - lib/dom-dump-script.ts: the one dump script both browser engines evaluate (IIFE, no single quotes). Folds CSSOM rgb() back to author hex so palette rules still fire, and removes inlined <link> nodes so the engine does not warn about an unresolvable stylesheet. Both verified against the engine. - test/fixtures/impeccable-detect-help.txt + impeccable-captures.meta.json: the flags, exit codes, finding fields, and re-capture protocol. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
0530392821 |
v1.81.0.0 feat: Aside is the browser gstack drives first; every browsing skill, the PDF/diagram renderer, and web research; the bundled browser stays the automatic fallback (#2810)
* feat(aside): browser-driver contract, cookbook, research and fallback resolvers
{{ASIDE_SETUP}} (readiness probe + ten rules for driving the user's real browser), {{ASIDE_COOKBOOK}} (script shapes verified live against Aside CLI 1.26: one flow per aside repl script, CDP console hook before navigation, evidence lines, session-directory artifact handoff, GSTACK_STEP_OK sentinel), {{ASIDE_RESEARCH}} (research through aside exec, WebSearch when Aside is absent, knowledge otherwise) and {{BROWSE_FALLBACK}} (the fifteen-row Aside-step to $B-command table plus the rules that differ, so every browsing skill keeps working on gstack's own headless browser). test/aside-driver.test.ts pins the sentences and asserts every browsing skill carries the Aside block followed by the fallback; test/helpers/aside-available.ts is the shared live-Aside probe.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(render): Aside-first local-HTML renderer with the bundled browser as fallback
lib/aside-render.ts serves the HTML's directory on loopback (Aside refuses file:// URLs), opens it with waitUntil load, prints through CDP Page.printToPDF so tagged output, outlines, header/footer templates and page numbers survive, emulates device metrics for sized screenshots, and writes in-page evaluations to files; when Aside is absent it runs the same spec through the browse daemon (newtab, load, js, pdf, screenshot, closetab) and reports ENGINE=aside|browse. bin/gstack-render.ts is the CLI skill templates call. lib/claude-bin.ts and lib/error-handling.ts become the canonical copies (browse/src re-exports them).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* refactor(browse): /browse drives Aside first, with the $B reference behind the fallback
Contract, cookbook, mode choice (aside repl by default, aside exec for reading), report format, the fallback section, and the full command reference carved on demand.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* refactor(qa): /qa and /qa-only drive Aside, fall back to $B
QA_METHODOLOGY runs every phase as Aside scripts (orient, explore, document, re-test, mobile viewport via CDP emulation, links via HEAD fetch); the authenticate phase is 'you are already signed in'; a 13th rule requires consent before mutating actions on non-local targets; the fallback section translates each step onto $B. The qa E2E tests run on whichever engine is present.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* refactor(design): design-review, design-consultation, design-shotgun, plan-design-review, design-html drive Aside
Design-system extraction is one script printing FONTS/COLORS/HEADINGS/TOUCH_TARGETS/NAV; competitor research confirms the exact URLs before opening them in the real browser and runs on the bundled browser when Aside is absent; design-html's viewport screenshots, sketches and comparison boards render through gstack-render.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* refactor(deploy): benchmark, canary, land-and-deploy Step 7, devex-review drive Aside
One aside repl script per page prints NAV/PAINT/LCP/RESOURCES/SCRIPTS/CSS/SUMMARY (benchmark), CONSOLE_ERRORS/NAV/TEXT + screenshot (canary, re-run every 60s), and the post-deploy check reads responseStatus from the navigation entry; each carries the $B fallback.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(third-party-actions): Aside is the recommended driver; gstack's visible browser stays the fallback
The readiness probe is lifted from {{ASIDE_SETUP}} at gen time (byte-identity pinned) and rule 3 points at browse/SKILL.md for how to drive; the consent question offers Aside first and gstack's own visible browser (handoff/resume for sign-in) as the fallback, as v1.72 framed it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* refactor(scrape): /scrape reads pages through Aside; the browser-skills runtime rides the fallback
Look-then-extract scripts build the JSON inside the page and print it between JSON_START/JSON_END; aside exec for fuzzy intents; on the $B fallback the browser-skills match/prototype flow and /skillify apply as before.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* refactor(make-pdf): print through Aside first, the bundled browser otherwise
asideClient.ts replaces the direct $B client with one render() call per PDF (the exact option mapping the browse pdf command had: paper, margins, header/footer/page numbers, tagged, outline, printBackground, preferCSSPageSize, Paged.js wait); the diagram pre-pass, oversized-image downscale and DOCX rasters each run as one render script with per-fence try/catch; exit 4 now means no browser is available and names both remedies; $P setup reports which engine it found. The e2e gates run on whichever engine is present, so the Linux lane exercises the fallback.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* refactor(diagram): the triplet is one gstack-render call
SVG, PNG and excalidraw from one invocation over the content-addressed bundle staged under /tmp/gstack-render; every diagram type gets an excalidraw export; gstack-render picks the engine and prints ENGINE=; the diagram E2E gates on either engine.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(research): web research runs in Aside first, WebSearch second
The planning, review, design, security and investigate skills research through {{ASIDE_RESEARCH}}; WebSearch stays in allowed-tools as the fallback; testing.ts's bootstrap step follows; skeleton ceilings ratcheted for the research block.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(setup,gen-skill-docs): prune renders of skills that no longer exist
setup gains _prune_stale_generated for every host tree and the doc generator removes gstack-* output dirs it did not write, so a skill removed from the source tree can never linger in an install.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test: registries, budgets and suite reconciled for Aside-first with the $B fallback
Touchfiles + E2E tiers gain the Aside keys, coverage matrix and eval baselines updated, size budget re-baselined to parity-baseline-v1.80.0.0.json (the contract plus fallback ride in every browsing skill), parity ceilings ratcheted with measured values, LLM-judge prompts and the E2E fixtures speak Aside-first, browse-fallback.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs: Aside first, gstack browser fallback
README, BROWSER.md, docs/, CONTRIBUTING, CLAUDE.md, ARCHITECTURE, AGENTS.md, TODOS and the root router describe the one product story: Aside is the browser gstack drives first; the bundled headless browser is the automatic fallback (Linux, Windows, app closed) where cookie import, GStack Browser, pair-agent and browser-skills still apply.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* chore: regenerate SKILL.md docs, llms.txt, agents digest, ship goldens, context-budget fixture
bun run gen:skill-docs over the templates; goldens re-rendered; context-budget ceilings recaptured.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* v1.80.0.0: Aside is the browser gstack drives first; the bundled browser is the fallback
MINOR: new capability across ten skills, the renderer and research; nothing removed. CHANGELOG release summary + itemized changes; VERSION 1.80.0.0; package.json 1.80.0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs(todos): file non-Claude host ownership-gate and version-heading pin follow-ups
Two follow-ups from the /plan-ceo-review + /plan-eng-review pass on merging
PR #2804 with main's v1.80.0.0 ownership gate: bring the Codex/Factory/
OpenCode/Cursor/Kiro copy loops and the stale-render prune under the
.gstack-owned marker rule, and a free test pinning that the CHANGELOG top
heading equals VERSION (the collision that git cannot see).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix: pre-landing review fixes for the Aside-first branch
Review army + adversarial passes (Claude and Codex) on the merged branch:
setup
- _prune_stale_generated scans the host dirs too (the generator already
removed the render before setup ran, so the host branch was dead), skips
symlinks in the render tree (rm -rf on a slash-terminated link empties its
target), removes a host symlink only when it resolves into gstack, cleans a
bannered real dir through _cleanup_weak_dir, recognizes frontmatter-renamed
skills, and logs through log. The always-run codex render passes every host
dir that may link to it.
- NEEDS_BUILD checks all three binaries (with $_EXE) and lib/ sources; the
browser hint and the bootstrap summary honor GSTACK_SKIP_ASIDE, treat a
requested skip as a request, and derive one skill list.
lib/aside-render.ts + bin/gstack-render.ts
- The loopback server carries a per-render secret path, checks containment on
the real path (symlink escapes are 403), and rejects malformed encoding.
- Inline eval results are one base64 line, so page text cannot forge
ASIDE_DIR= or the sentinel; the last ASIDE_DIR wins.
- runProc escalates SIGTERM to SIGKILL, bounds every wait, and clears every
timer (an uncleared one kept gstack-render alive after printing OK).
- renderTmpDir refuses a shared /tmp name owned by someone else; the work dir
and server are created inside try; goto's budget follows the render budget.
- probeAside classifies a present-but-failing CLI as ASIDE_NOT_RUNNING like
the skills' bash probe; render() retries on gstack's own browser when Aside
could not start or its private CDP bridge is gone (never on a page error
or a timeout of a running script); the CLI reports the engine that actually
rendered, exits 0 on --help, rejects non-numeric flags, documents
--wait-timeout, fences EVAL/PAGE_ERRORS as untrusted content, and names the
daemon's cookie-import JS lock remedy.
- The browse path passes --scale only when asked (a scale change rebuilds
the daemon context) and restores the viewport after a sized screenshot.
resolvers / templates
- The bash probe honors GSTACK_SKIP_ASIDE and has a perl deadline on stock
macOS; .local is no longer LOCAL (mDNS); same-origin filters compare parsed
origins; link status is HEAD-checked only on LOCAL targets; every
aside exec goes through the receipted _aside_exec prelude
({{ASIDE_EXEC_PRELUDE}}), including nine template blocks that called it
bare; the design sketch and diagram staging use private directories.
- The generator prunes only bannered renders and never a host whose
generation failed.
Docs, stale comments and dead code cleaned; goldens re-rendered; tests
updated and added for every behavior above.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test: coverage for the render CLI, setup rebuild check, make-pdf exit codes, and prose $B spans
New free tests from the ship coverage audit: test/gstack-render-cli.test.ts
(argv guards, --help, output contract with a fake daemon, failure and
serve-root paths, no-browser case, prompt exit), test/setup-needs-build.test.ts
(every binary and source set flips NEEDS_BUILD, Windows suffixes),
make-pdf/test/cli-exit-codes.test.ts and setup-smoke.test.ts (error to exit
code mapping, runSetup stages, renderPdf's engine), and prose-span cases for
extractBrowseCommands in test/skill-parser.test.ts.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs: CHANGELOG and TODOS cover the review fixes (v1.81.0.0)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs: sync project docs with the v1.81.0.0 review fixes
BROWSER.md, ARCHITECTURE.md, CONTRIBUTING.md, README.md, CLAUDE.md,
docs/TESTING_INTERNALS.md and docs/PROJECT_STRUCTURE.md now describe the
shipped renderer and setup: the loopback render server's per-render secret
path and real-path containment, ENGINE= naming the engine that actually
rendered (mid-run retry on gstack's own browser), EVAL/PAGE_ERRORS fenced as
untrusted content, --wait-timeout and the CLI's argv guards, the receipted
_aside_exec prelude ({{ASIDE_EXEC_PRELUDE}} in the placeholder table), the
LOCAL host rule without .local, LOCAL-only HEAD checks in the links script,
GSTACK_SKIP_ASIDE across probe/renderer/setup, the ownership-gated
retired-skill prune, the widened NEEDS_BUILD check, and the new free tests
(gstack-render-cli, setup-prune-stale-generated, setup-browser-hint,
setup-needs-build, make-pdf cli-exit-codes and setup-smoke).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs: CHANGELOG states the precise mid-run retry rule
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(test): skill-e2e-bws slices the $B setup block from the Browser fallback section
browse/SKILL.md no longer has '## SETUP' / '## Core QA Patterns' (Aside is the
primary driver; the $B block moved under 'Browser fallback'), so the gate test
sliced an empty block and handed the agent nothing to run. Anchor on
'### Find the `$B` binary' up to the next heading. 7/7 pass.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(test): gate POSIX-only fixtures off Windows
windows-free-tests: the gstack-render CLI tests drive a shebang fake browse
that CreateProcess cannot exec, and two NEEDS_BUILD cases assert an execute
bit and a bare-name miss that MSYS bash does not have (test -x ignores mode
bits and resolves design -> design.exe). Those describes and cases now
self-skip on win32; argument guards, --help, the no-browser case, and every
other rebuild-check case still run there.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(render): runProc waits for the exit code until the kill deadline; newtab retries once on a cold daemon
A process whose pipes have reached EOF is exiting, but runProc gave the exit
code only five seconds to arrive and then returned null, which run() reports
as a failed command. Under CI's six-shard load one such render failed with the
artifact already written. The SIGTERM/SIGKILL timers already bound the wait,
so the exit race now runs to the kill deadline.
The first CLI call auto-starts the browse daemon; on a cold start it can
answer 'Unable to connect' once while the server is still coming up. That
single case is retried after 1.5s; every other newtab failure is not.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(aside-render): warm the daemon before live fallback cases; failures name the render error
- Live fallback cases run 'goto about:blank' up to twice before asserting and
skip (never fail) when the daemon cannot come up.
- expectOk() puts r.error and the browse transcript into the assertion so a
failed render is diagnosable from the CI log.
- The argv-contract cases dump the fake's log on a miss.
- File default timeout is 30s: the subject is the CLI contract, not latency.
- Two cases pin the cold-daemon newtab retry and that other errors are not
retried.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs: CHANGELOG notes the cold-start tolerance of the bundled-browser renderer
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Sina <sdroid674+github@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
|
||
|
|
c241216637 |
v1.80.0.0 fix: setup survives a failed Chromium install, hooks share one state root, gstack never clobbers a skill it did not create (#2802)
* fix(freeze): hook reads the same state root /freeze writes — fails closed under GSTACK_HOME (#1459, #1509) check-freeze.sh resolved its state dir as ${CLAUDE_PLUGIN_DATA:-$HOME/.gstack} while every writer (/freeze, /guard, /unfreeze, /investigate) resolves through bin/gstack-paths, GSTACK_HOME first. With GSTACK_HOME set, /freeze wrote freeze-dir.txt under GSTACK_HOME, the hook read $HOME/.gstack, found no file, and allowed everything — a deny-tier boundary failing open. One resolver now: gstack_hook_state_root() in careful/bin/hook-extract.sh (already sourced by both check-freeze.sh and check-careful.sh) implements the exact gstack-paths chain, including the CLAUDE_PLUGIN_ROOT guard that keeps a CLAUDE_PLUGIN_DATA leaked from another plugin from redirecting our state. check-freeze.sh and gstack_hook_log_fire both call it; nothing spawns gstack-paths from a hook. Tests: the GSTACK_HOME deny regression, GSTACK_HOME-over-CLAUDE_PLUGIN_DATA precedence, plugin-root guard both ways, and a byte-parity check against bin/gstack-paths across six env combinations. Existing freeze tests now pass CLAUDE_PLUGIN_ROOT like a real plugin install would. Idea from PR #1509 (@NikhileshNanduri); implemented natively against the shared resolver rather than a second fallback chain. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(relink): never delete or link over a skill gstack does not own (#2119) gstack-relink runs on every ./setup. Its cleanup did `rm -rf` on any same-name entry whose SKILL.md was a symlink, with no readlink check, and its link step did `mkdir -p` then `ln -snf` onto any existing SKILL.md — on Linux that replaces a user's real file with a symlink into gstack (macOS refused by accident). setup's Windows mode-flip cleanup deleted any real dir whose name matched a gstack skill. A personal `qa` skill, or a fork installed under another path, was destroyed by the installer of a tool it never asked for. Ownership is now proven, never assumed. An entry is ours when it is a symlink resolving into INSTALL_DIR or RENDER_DIR, a real dir whose SKILL.md is such a symlink, or a real dir carrying the .gstack-owned marker setup now writes for Windows copy installs (legacy copies count when byte-identical to the source or carrying gen-skill-docs' AUTO-GENERATED header). Anything else — including an entry whose readlink fails — is foreign: left untouched, reported on stderr, and listed in relink's summary line. The same rule replaces setup's Windows name-match deletion; setup:1040 and gstack-uninstall:204 already gated on readlink, so this closes the last unguarded deleter of the class. Tests: foreign real dir in flat mode, foreign flat entry on a prefix flip, foreign directory symlink, RENDER_DIR-targeted entry (ours), marker-carrying copy (ours), marker-less copy (foreign); the Windows cleanup test now proves provenance three ways and keeps the user's own same-name skill. Idea and two regression cases from PR #2119 (@smblight); implemented on the destination entry, not only the symlink target. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(setup): Chromium bootstrap is best-effort and bounded — skills always register (#1900, #1901, #1902, #913, #2233) setup runs under `set -e`, and the Chromium bootstrap in section 2 sat ahead of skill registration in section 4 with a bare `bunx playwright install chromium`, an unbounded download, and an explicit `exit 1` after the post-install launch probe. On an offline, proxied, or AppArmor-restricted box the user ended with ZERO skills registered and a re-run that died at the same line; a wedged download hung setup indefinitely. Every browser failure now records a reason code in _PW_FAIL_REASON and setup continues: skipped (GSTACK_SKIP_PLAYWRIGHT=1, #913), chromium-install, chromium-install-timeout (the download is bounded by the existing _wait_with_deadline helper, default 600s, env GSTACK_PLAYWRIGHT_INSTALL_TIMEOUT, process tree killed via _kill_tree), chromium-install-locked (another setup holds the lock: this one registers skills and re-probes next time instead of exiting), windows-no-node, windows-node-modules, post-install-launch (with the GSTACK_CHROMIUM_NO_SANDBOX=1 hint for Ubuntu 24.04's userns policy, #2157). The daemon font refresh is skipped when Chromium is unavailable. The final summary names the skills that need the browser (/qa, /qa-only, /design-review, /browse, make-pdf, /pair-agent) and the fix for the recorded reason, and logs the reason code (never a path) through gstack-telemetry-log when telemetry is on. Tests: static invariants over the anchor-sliced block (no exit, every reason code, deadline helper, trap chaining, guarded refresh, summary contents) plus an integration harness that executes the real block with a stubbed probe and installer: install failure, hang killed at the deadline with the tree kill recorded, non-numeric knob fallback, live lock (continues, installer not run, lock preserved), stale lock reclaimed, post-install probe failure, and the skip flag. Credit @DavidMiserak (PR #1900) for the best-effort shape; re-implemented on the current block. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(designs): preserve the time-attack fork-port residual evaluation The read-only evaluation of what remains portable from time-attack/gstack (583 raw candidates, 415 canonical, 287 with a residual, 48 adversarially refuted, 14 standing) lived only on a throwaway VM. This records the report, the lite residual index, the absorbed/superseded ledger, the refuter verdicts, and SHAS.md with the fork tip, upstream HEAD, merge-base, and a sha256 per file, so every scheduled fix in this wave series traces to its evidence. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: file the fork-port residual deferrals and document the Chromium bootstrap knobs TODOS.md gains the seven items the CEO and eng reviews of the fork-port residual plan deliberately deferred (shared ownership helper, config-key reader tripwire, "pre-existing" vocabulary, opt-in reply_language, .auth.json writer removal, the fork-derived-change rule for CONTRIBUTING, hook slug parity audit), each with rationale, and updates the two residual bullets for PR #2232 and PR #2233 with their dispositions. README's Troubleshooting section explains the best-effort Chromium bootstrap and its three knobs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(relink): canonicalize link targets before the ownership check Pre-landing review finding: the ownership gate compared readlink output textually against INSTALL_DIR and RENDER_DIR, so two shapes of gstack's OWN entries read as foreign and were left behind on a mode flip — a legacy relative link (`gstack/qa/SKILL.md`, resolved against $PWD instead of the link's directory) and an entry linked against the real path of a symlinked install dir (~/.claude/skills/gstack -> checkout). Both now resolve: relative targets anchor at the link's directory, the directory part is canonicalized with pwd -P (the basename stays verbatim so a dangling managed target is not misread), and both spellings of each root are accepted. Two regression tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(telemetry): one-shot setup events never sweep other sessions' pending markers gstack-telemetry-log finalizes every .pending-<session> marker that is not the caller's own as outcome:unknown and deletes it. setup's onboarding events (_setup_welcome, _setup_playwright) have no session of their own, so a Chromium bootstrap failure during a live skill session recorded a false unknown for that session and removed its marker. New --no-sweep flag skips the stale-marker pass; both setup call sites use it (the synthetic --session-id did not prevent the sweep). Surfaced by the Codex adversarial pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(hooks): partial upgrades fail closed for freeze and fall back for careful A hook script and its sourced helper can be copied at different times. With an older careful/bin/hook-extract.sh that lacks gstack_hook_state_root: - check-freeze.sh now emits a deny ("fail closed, re-run ./setup or /unfreeze") instead of dying under set -e with no decision JSON. - check-careful.sh falls back to ${GSTACK_HOME:-$HOME/.gstack} so project rules under the plain chain still load and a decision is always emitted (a warn hook must never break on a stale helper). gstack_hook_state_root prints its root without a trailing newline and both callers capture it with a printf-x sentinel, so a GSTACK_HOME ending in a newline round-trips byte-for-byte with the writer's %q form. gstack_hook_log_fire stays on ${GSTACK_HOME:-$HOME/.gstack}/analytics, the same two-step chain every other analytics writer and reader uses, so the usage log remains one file under a plugin install. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(setup): never link over, copy over, or reap a skill gstack does not own (#2119) The relink gate alone left three destructive sites open: - link_claude_skill_dirs runs BEFORE relink on every ./setup and used `ln -snf` (Linux replaces a user's real SKILL.md with a symlink into gstack) or, on Windows, rm -rf + cp followed by a marker that made the user's directory "ours" on the next flip. It and _install_alias_skill_md now consult _claude_entry_is_ours first and skip loudly. - cleanup_prefixed_claude_symlinks kept a bare name-match deletion and a `*gstack*` substring match. Symlink arms use anchored `gstack/` segment patterns; the Windows real-file arm proves provenance (marker, byte-identity with our source, or the full two-line gen-skill-docs banner within the first 40 lines, never a one-line substring another generator could emit). cleanup_old_claude_symlinks uses the same banner rule. - gstack-relink's fast path judged absolute targets before canonicalizing, so `/x/gstack/../foreign/SKILL.md` counted as ours; dot-segment targets now canonicalize first. Its banner rule matches setup's. The `.gstack-owned` marker records the owning payload's realpath. Entries skipped by setup or relink are listed in the final setup summary. Chromium bootstrap refinements from the pre-landing review: an INT/TERM trap kills the installer's process tree; the Windows npm chain no longer masks an install failure; GSTACK_SKIP_PLAYWRIGHT=1 is reported as a choice rather than a failure and sends no telemetry; the timeout knob is normalized (0, 000, non-numeric, or more than nine digits fall back to the 600s default instead of killing on the first poll or never killing). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: README Chromium note outside the CLAUDE.md fence; report banner stripped; deferrals name the four gate sites - README: the Chromium troubleshooting paragraph sat inside the CLAUDE.md snippet code fence, so copy-paste put it into users' CLAUDE.md. Moved to the troubleshooting list. - docs/designs/fork-port-residual-2026-09/REPORT.md: the scratch-run preamble banner is gone; SHAS.md re-hashed. - TODOS: the ownership-gate deferral names the four sites and the marker-path idea for the fork-with-banner residual. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(todos): the bootstrap block coverage gap is pinned except the quarantine helper Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(setup,relink): ownership proof has two strengths; weak proof never deletes a directory or discards a differing file The first #2119 gate treated a byte-identical or banner-bearing real-file SKILL.md as full ownership, so a prefix flip could rm -rf a user's directory (their own qa skill started from a gstack SKILL.md, plus my-templates/) and the link pass could replace their customized file with a symlink. Two strengths now: - STRONG: the .gstack-owned marker (we created the directory), or a directory holding nothing but symlinks and the marker (deleting it loses no data). Only strong proof removes a directory whole. - WEAK: byte-identity with our source or the two-line gen-skill-docs banner on a real file. Weak proof covers that SKILL.md and our runtime-asset links only; a differing file is moved to ${GSTACK_HOME:-~/.gstack}/backups/skills/<ts>/<skill>/ before we link over it, and setup/relink print one summary line naming what moved. The marker is written on every platform now (path-independent proof for Windows copies and for checkouts whose path carries no gstack segment), but only for a directory gstack creates: a directory we merely link into (unclaimed, or a legacy install) never becomes deletable whole. A directory with no SKILL.md at all is unclaimed: the link pass may add our file, the cleanup pass has nothing to remove. Also from the review passes: the banner check reads 8192 bytes, not 40 lines (investigate, office-hours, plan-ceo-review and design-consultation carry the banner past line 40 and were left "foreign" on pre-marker Windows installs); a link into a checkout named without a gstack segment (git worktree add ../gstack-<branch>) is ours when that tree carries setup + VERSION + bin/; relink's fast path is gone so both files canonicalize before judging; relink's root alias (_gstack-command) is gated and stamped like every other entry; relink reports the bare entry name with setup's wording and setup dedupes when forwarding (_run_relink_quiet); the summary names the browser skills as examples. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(setup): Chromium-install lock reclaim is atomic and pid-validated; abandoned locks expire; the tree kill walks /proc without pgrep - A pid file holding "", "-1" or "0" counted as a live holder (kill -0 -1 signals every process and succeeds), locking Chromium out for good. A pid must be a positive integer; anything else is stale. - Two setups judging the same lock stale raced on rm -rf + mkdir and the loser deleted the winner's fresh lock. The stale dir is renamed first (atomic), so exactly one reclaims. - A lock dir with no pid file (killed between mkdir and echo) was never reclaimed; it now expires once older than the install bound. - _kill_tree needed pgrep; debian-slim and git-bash ship none, so the bound killed only the wrapper subshell and the installer kept running. Without pgrep the children are found by walking /proc/*/stat. - The timeout knob is normalized in one place with one comment; the trap's exit 130 is the only exit the block may contain. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(freeze): an unexpected non-zero death denies via an EXIT backstop instead of exiting with no decision set -e plus a failing pipeline (a tool on PATH exiting non-zero, a deleted cwd) ended the deny-tier hook with no JSON, which Claude Code treats as non-blocking: the edit outside the boundary proceeded. The EXIT trap now prints a deny for any non-zero exit that happens before a decision was written; every deliberate output sets _FREEZE_DECIDED first so a late failure never prints a second object. Tests also pin careful's state-root precedence (GSTACK_HOME over CLAUDE_PLUGIN_DATA, plugin data when CLAUDE_PLUGIN_ROOT names gstack) and the specific "out of date" deny for a helper without gstack_hook_state_root. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(telemetry): guard the stale-marker sweep with an if, not a break inside the loop Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(todos): the ownership gate lives in six sites, and the cleanup arms inline their own chain Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test: the two remaining linker harnesses extract the ownership helpers; the marker is the one allowed dotfile setup-claude-skill-assets and user-render-out-dir-install slice link_claude_skill_dirs out of setup without the helpers it now calls, so the extracted function died with "command not found" (or, inside an if, degraded into "foreign, skipped"). Both harnesses now carry the full helper set and the globals. The hidden-files census allows .gstack-owned, which the linker writes for directories it creates rather than copying from the skill source. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(setup,relink): weak proof never costs the user a file — assets, flips, failed backups, foreign dir links, alias markers Third review cycle on the ownership model, every item reproduced against a fixture before the fix: - Runtime assets (sections/, templates/, checklist.md, ...) were refreshed with rm -rf regardless of who owned the directory, so an unclaimed or weakly-owned directory lost the user's same-named real files. Real assets are now replaced only in a directory gstack created or strongly owns (marker, or SKILL.md symlink into gstack), plus the legacy Windows real-copy shape; elsewhere they are kept and reported. Symlinks are never content and are always refreshed. - The prefix-flip cleanup deleted a customized banner-bearing SKILL.md that the link pass would have backed up. Both cleanups now compare the file against the source (raw, or with its name: line rewritten to the entry name, which is how alias and prefixed copies legitimately differ) and move a differing file to the backup root. - A failed backup (unwritable root) returned success and the caller linked over the file anyway. It now fails, and the entry is left untouched and reported. - A foreign DIRECTORY symlink whose target had no SKILL.md fell through to the "unclaimed directory" rule and was replaced by a real directory. A symlink that does not resolve into gstack is foreign, full stop. - The alias installers stamped .gstack-owned into pre-existing directories; they now follow the same created-or-already-marked rule. - A directory counts as "only links" only when every link resolves into gstack: a user's own symlink makes it mixed, so their link survives. - The gstack-tree heuristic requires bin/gstack-relink, not just a VERSION file, a setup script and a bin/ directory. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(setup): lock reclaim hands a fresh lock back; a live holder past the bound is stale; /proc walk strips through the last paren - Reclaim renamed the lock by path after judging it stale, so a second setup that had already reclaimed and re-created it lost its fresh lock and two installers ran. After the rename the moved directory's pid is re-read: a new live holder, or a fresh lock whose pid is not written yet, is moved straight back. - A pid file whose process is alive but whose lock is older than the install bound is stale too (the holder is past its own deadline, or the pid was recycled to an unrelated long-lived process); it was locked forever. - The /proc fallback stripped the comm field to the FIRST ") ", so a comm containing ") " hid a child from the kill. proc(5) says the last paren. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(freeze): mark the decision written after the helper prints, not before If gstack_hook_decision ever failed between the flag and its output the backstop would have stayed silent; setting the flag after the print keeps the deny backstop armed until a decision is actually on stdout. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore: bump version and changelog (v1.80.0.0) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: update project documentation for v1.80.0.0 README troubleshooting + manual uninstall cover the skill ownership gate (.gstack-owned marker, ~/.gstack/backups/skills/<ts>/, foreign same-name skills left untouched). CLAUDE.md and CONTRIBUTING carry the ownership and best-effort Chromium bootstrap invariants for people editing setup and gstack-relink. PROJECT_STRUCTURE gains careful/, freeze/, guard/, unfreeze/, gstack-upgrade/, gstack-relink, and the setup/relink/hook test files. TESTING_INTERNALS documents the anchor-sliced setup harness convention. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(setup): the final summary reports customized SKILL.md files moved to the backup root The linker moved a weakly-proven, customized SKILL.md aside before linking over it but never said so; only relink printed a "Moved N" line, and by the time relink runs the file is already a symlink. The summary now names each moved file and where it went, next to the foreign-entry report. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test: run assembled setup harness scripts from a temp file, not `bash -c` argv (Windows MSYS2 8 KB truncation) windows-free-tests (run 33907177851) failed in test/setup-alias-name-uniqueness.test.ts with bash: -c: line 178: unexpected EOF while looking for matching `' The harness slices functions out of `setup` and passed the joined script as one `bash -c` argv element. The ownership gate grew that script from 6.7 KB to 15.7 KB, and on Windows bash is an MSYS2 program: when its parent is a non-MSYS process (bun), msys-2.0.dll's build_argv() runs any argument containing `?*["'(){}` through globify()/glob(), which copies the pattern into a fixed `Char patbuf[8192]` and silently stops after 8192 - MB_CUR_MAX (8186 chars under C.UTF-8); GLOB_NOCHECK then returns the truncated text as the argument. Character 8186 lands inside the single-quoted sed token on line 178. Rebuilding the exact script with CI path shapes and cutting it at 8186-8190 characters reproduces the identical message locally; cmd.exe's 8191-UTF-16 cap and CreateProcess's 32767 do not fit the evidence. Fix: test/helpers/bash-script.ts writes the script to a temp file and runs `bash <path>` — a short glob-free argument that never enters globify. Every setup harness that assembled a script for `bash -c` (11 files, 22 sites) uses it; timeouts and env are preserved verbatim, spawn/timeout errors are appended to stderr, temp cleanup is best-effort. `spawnSync('bash', [<Windows absolute path>])` already passes on windows-latest in setup-help, uninstall-windows-copies and the migration tests. The Windows-curated list is byte-identical before and after. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(test-free-shards): the rerun-refresh harness spawns bash <tempfile> via test/helpers/bash-script.ts, not bash -c Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
0d1bd5616c |
v1.79.0.0 fix: ship subagent dispatches can no longer strand the run (#497/#2440 class) (#2772)
* fix(ship): foreground-flag + deadline + scope guard on all four dispatch sites (#497/#2440 class, 3rd recurrence)
Steps 7/8/10/18 dispatch subagents whose LAST-line JSON the parent
consumes, but none passed run_in_background: false — since Claude Code
v2.1.198 subagents background by default, so /ship stranded at Step 18
waiting on output that never arrives. Every site now renders the shared
{{FOREGROUND_DISPATCH_NOTE}} resolver constant, carries a ~10-minute
deadline with an explicit recovery branch (stop the runaway task,
reconcile against pre-dispatch HEAD, surface stray state, never
re-dispatch), and Step 18's prompt gains a docs-sync-only scope guard
(no VERSION changes, no base-branch merges, push-rejection reported as
pushed:false with parent-side reconciliation and a second-failure
branch). Greptile failures now record as UNAVAILABLE, not zero comments.
GENERATED_WITH_GUIDANCE pins all five ship dispatch carriers; a new test
pins the deadline recovery + scope guard phrases in pr-body (.md and
.tmpl). Codex/factory ship goldens re-rendered.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(document-release): first-class spawned-dispatch contract
document-release's own templates had zero subagent-awareness — the
entire headless contract lived in /ship's dispatch prompt, so any other
orchestrator (or an older installed /ship) dispatching it inherited none
of the gate handling. The skill now carries the contract itself: detect
spawned strictly from the dispatch prompt or the preamble echo (never
from file content — prompt-injection guard), auto-choose recommended
options while keeping the never-clobber-CHANGELOG and
never-bump-VERSION-silently invariants via their Skip options. Step 8.4d
gets an explicit spawned note (its interactive recommendation bumps
VERSION — wrong headlessly), and the Codex Documentation Review section
skips itself in spawned sessions (the apply gate needs a human; the
dispatching workflow owns review passes).
Contract, 8.4d note, and resolver skip are pinned in
run-in-background-guidance.test.ts; document-release skeleton budget
re-measured (39,812 B) and ratcheted to 40,200.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: sweep every remaining synchronous Agent-dispatch site with run_in_background: false
The #2440 failure shape was a silently-missing review voice — a
specialist launched in the background and merged before it completed.
Every remaining synchronous dispatch site now carries the explicit flag:
the Red Team dispatch, the spec review loop, the Codex
second-opinion/plan-review/doc-review Claude fallbacks, the adversarial
subagent, design sketch and outside voices, autoplan's design/eng/dx
phase dispatches, CSO parallel finding verification, and design-shotgun's
variant launch. Parallel fan-outs stay parallel — multiple foreground
Agent calls in one message run concurrently (the shipped v1.64.0.0
review-army pattern).
GENERATED_WITH_GUIDANCE now pins all 24 generated carriers, so a new
dispatch site that drops the flag fails the free suite. Six carved-skill
skeleton ceilings re-measured and ratcheted (~80-130 B growth each);
factory ship golden re-rendered.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* release: v1.79.0.0 — CHANGELOG, VERSION, TODOS follow-ups
Queue-advanced to 1.79.0.0 (1.78.0.0 claimed in the workspace queue;
same MINOR level per the versioning invariant). Entry references the
class history (#497 → #2440 → Step 18). Three TODOS filed: PreToolUse
hook enforcement, structural ship-mode for document-release, cross-host
dispatch semantics audit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* regen: review-army sections carry the Red Team foreground flag
The scripts/resolvers/review-army.ts Red Team edit regenerated these two
files but the sweep commit staged only the adversarial sections — the
skill-docs freshness gate (regen + git diff --exit-code) catches this.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* regen: agents digest picks up v1.79.0.0
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(document-release): one canonical spawned contract, downstream notes are pointers
The diff-selected LLM-judge eval scored the skill's clarity 3 (threshold
4, main scores 4): the spawned-session rules read as three separately-
framed rule sets (contract paragraph, Step 8.4d note, Codex-review skip).
The contract paragraph now declares itself the single source of spawned
behavior and the two downstream notes reference it instead of restating
rationale. The pointer avoids naming the Codex section verbatim so the
codex-host render (which strips that section) keeps its negative pin.
Judge re-scored 4/5/4 across repeated samples after the change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: foreground note names the Agent tool as non-substitutable
The ship-docsync gate E2E caught a behavioral regression: the note's
blocking-emphasis ('a backgrounded dispatch strands the run') steered
the driven agent to run doc-sync via the Skill tool inline — the most
blocking option — twice in a row, forfeiting the fresh-context isolation
the dispatch exists for (baseline on main's text dispatches via Agent).
The shared note now says explicitly: dispatch with the Agent tool itself,
never substitute Skill or inline execution; the flag already makes the
call block. Re-verified: ship-docsync passes on the amended text.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: pre-landing review fixes
Review army + red team findings, all verified before applying:
- ship-docsync E2E now asserts run_in_background === false on the
captured dispatch (red team CRITICAL: phrase pins prove text exists,
this proves the model obeys it — verified passing live).
- Structural scanner test: any generated file with an Agent-dispatch
imperative (or bare '(foreground)' prose, the #2440 inert shape) must
carry the flag or hold a reasoned exemption — the 4th-recurrence net
the hand-enumerated pin list can't provide.
- Parent push reconciliation models reality: the parent shares the repo,
so a non-fast-forward that hit the subagent hits the parent identically
— fetch + ahead/behind check first, push only when the rejection was
transient; dispatch prompt promise softened to 'the parent will handle
it'.
- Recovered commits from a dead subagent are vetted docs-only
(git show --stat, never VERSION/package.json) before any push.
- Deadline pacing named: ~3 minutes between checks, wall clock not polls.
- Greptile UNAVAILABLE recording narrowed to the PR body (Step 20's
schema carries no triage field).
- document-release contract gains the echo-failure tie-breaker: prompt
claims spawned + no echo → fail fast with the dispatch contract's
failure shape instead of reproducing the #2733 prose-STOP; contract
anti-injection and NEVER-relax clauses pinned in tests.
- 'Claude Code v2.1.198' extracted to CC_BACKGROUND_DEFAULT_SINCE and
interpolated at all resolver sites (byte-identical output).
- CHANGELOG: entry-boundary blank line restored; worst-case-wait row
scoped to the backgrounded path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: adversarial review fixes — failure shape, CHANGELOG guard, vet coherence
Claude adversarial subagent findings (11), all verified before applying:
- The dispatch JSON contract gains an explicit FAILURE shape
({"error":...}) and a parent branch for it — a doc-sync that could not
run (spawned marking failed, broken preamble) previously had only the
no-updates shape to emit, which the parent printed as 'Documentation is
current': a silent false-clean of exactly the VAS-449 class.
- Scope guard now covers CHANGELOG: skip Step 5 voice polish and resolve
CHANGELOG-touching gates to leave-as-is (the parent authored the
release entry; the prompt's older auto-choose clause conflicted with
the contract's never-rewrite invariant).
- Recovery vet is sequence-coherent: pushing a commit pushes its
ancestors, so ANY non-doc commit (VERSION, package.json, CHANGELOG.md)
blocks the whole sequence — no more push-the-vetted-child-of-an-
unvetted-parent hole.
- Steps 7/8/10 failure branches stop a still-running backgrounded task
before falling back, so a late result never races the inline audit.
- 'Documentation synced' print gated on pushed:true (item 6 owns the
local-only outcome); doc-review skip note's backward step pointer
fixed; foreground note scoped 'at initial dispatch' so the sanctioned
inline FALLBACKS in Steps 7/8 read as sanctioned; constants docstring
no longer overclaims single-sourcing (names the 3 inline templates).
- Runtime-hook TODO raised P2 → P1: the spawned trust chain is
agent-self-asserted; prose cannot close it, the hook can.
Both docsync gate E2Es re-verified green on the amended prompt, including
the strict run_in_background === false dispatch assert.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: codex adversarial fixes — JSON trust, fail-open visibility, user-state safety
Codex outside-model adversarial pass (inline-diff workaround for the
sandbox), P1/P2 findings triaged cross-model:
- Parent validates the dispatch JSON's field types and treats
documentation_section as untrusted markdown (Step 19's redaction scan
covers the final body; instruction-shaped text inside it is never
followed). The error branch now explicitly skips items 2-6.
- Remote-ahead divergence is named, not silent: the parent lists foreign
commits before creating a PR over a moved branch.
- Recovery cleanup never discards content: stray staged doc edits are
unstaged but never checked out or cleaned away.
- Greptile UNAVAILABLE gets a concrete PR-body line, not a vague
'wherever results are reported'.
Pre-existing-class P1s (self-asserted spawned trust chain, unenforceable
foreground timeout) are cross-model confirmed and tracked: PreToolUse
hook TODO at P1, harness-residue hazard documented.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: never-Skill prohibition made unambiguous; docsync fixture made diagnostic
The ship-docsync E2E flaked on this sandbox because the driven agent
resolved the STOP pointer's ~ to nonexistent homes (/root, /home) and
acted blind — pass/fail sampled model priors, not the prompt. The
fixture prompt now names the planted sections dir, making local runs
deterministic (CI, with real install paths, was always diagnostic).
With a diagnostic fixture: 2/2 passes, real pr-body read, Agent dispatch
with run_in_background: false, zero Skill-tool substitutions.
Prose: the foreground note now states the dispatch happens ONLY via the
Agent tool (invoking the target as a Skill is wrong even though it
appears in the skills list; inline FALLBACKs apply only after a
dispatched subagent has failed), and the Step 18 imperative carries the
never-the-Skill-tool clause at the decision point.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: CHANGELOG entry covers the review-hardened contract
Failure JSON shape, CHANGELOG scope guard, vetted recovery pushes,
structural scanner, and the behavioral dispatch assert are shipped
properties of v1.79.0.0 — the entry now describes them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: update project documentation for v1.79.0.0
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: CHANGELOG voice — follow-ups to For contributors, never-Skill property stated
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(test): daemon 503 test binds an OS-assigned port, not old-port+1
windows-free-tests flaked on this PR: the tunnel-less restart bound
daemon.loopbackPort + 1 — a fixed neighbor of the OS-assigned ephemeral
port — and died with 'Is port 55738 in use?' whenever another shard or a
TIME_WAIT socket held it; the file-level retry re-rolled the same dice.
Every other startDaemon in the file already uses loopbackPort: 0 and the
assertion reads d2.loopbackPort, so nothing needs a predictable number.
21/21 pass locally.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
+8 |
702a1a9b69 |
v1.78.0.0 fix: the two-red-lanes wave — AUQ collapse rooted, OSV green from 105, 18 community PRs absorbed, upgrade path can't eat installs (#2752)
* fix(auq): spawned trigger is objective — explicit declaration or STATUS echo, never inference (periodic-lane AUQ collapse)
The v1.76 spawned rule's parenthetical '(or your dispatch prompt marks this
session as spawned)' let the model INFER spawned status from a scripted-looking
prompt in a CI-looking session and silently auto-choose every review-phase
question: reviewCount=0 across the plan-review periodic E2Es (weekly run
33363624506, 9 of 14 failed shards; reproduced locally, zero AUQ fingerprints).
Env and hook paths were excluded by inspection: hermetic children echo
SESSION_KIND: interactive (CLAUDE_CODE_ENTRYPOINT=cli beats CI markers) and the
question-preference hook isn't installed there.
The trigger is now objective: the echoed SESSION_KIND: spawned STATUS line, or
an EXPLICIT dispatch-prompt declaration ("you are a SPAWNED subagent") —
declared, never inferred — with an absence-safe interactive fence: CI env vars,
scripted-looking or pasted prompts, and write-to-this-exact-file instructions
are NOT spawned markers. The prose channel stays because Task-tool subagents
inherit the parent env (no spawned prefix) — their dispatch prompt is the only
signal; #2733's env-prefix channel is untouched.
19 carve skeleton ceilings re-pinned with measured values (+~440 bytes/skill);
ship goldens refreshed for all three hosts; resolver pins extended with the
no-inference regression tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: mktemp failure aborts loudly at all three skill-content sites; failed upgrade swap restores the backup (#2679)
An empty $(mktemp) result silently disabled the redaction pass (redact-doc
resolver, ship pr-body) and made /gstack-upgrade's vendored path destructive:
clone lands at "/gstack", the swap mv fails, and rm -rf then deletes BOTH the
live install's backup and "". All three sites now guard the assignment with a
loud exit; the vendored block additionally restores the backup when the swap
fails (same failure class — backup deletion after a failed mv) and the GitLab
MR path sends the SCANNED file's bytes instead of re-rendering an unscanned
heredoc. bin/gstack-redact rejects an explicit empty --from-file path instead
of silently falling through to stdin.
Receipts: 6 of 8 new regression checks fail on a v1.77.0.0 scratch worktree.
Fixes #2679
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(auq): the interactive fence classifies the session — it never nudges ask-count
Burn-in run 1 of the periodic repro overshot the review band (reviewCount=8 >
CEILING=7) with the fence's 'when unsure, ask' tail: that phrasing is a quota
nudge, not a classification default. The fence now states it only classifies
the session and never changes how many questions the skill asks. Pin added.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ci): OSV suppression config actually loads — explicit global --config + expiring, reasoned ignores
The ignore file was inert from v1.65.0.0: OSV-Scanner only auto-discovers
configs named osv-scanner.toml (no leading dot) and applies them
per-directory, so the root config never covered lib/diagram-render/bun.lock
either way. The workflow now passes --config=.osv-scanner.toml globally.
Every IgnoredVulns entry carries a reason with an upgrade trigger and an
ignoreUntil expiry (~90 days) so suppressions must be re-justified. A wiring
test pins flag ↔ filename ↔ entry hygiene so the file can never silently go
inert again.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(deps): dependency wave — 105 OSV advisories → 3 reasoned suppressions, all lanes verified on the pinned scanner
Root: overrides pin ip-address 10.3.1 (defeats BOTH nested nodes — socks'
range pull and express-rate-limit's exact 10.1.0 pin, which a top-level bump
provably cannot reach) and sharp 0.35.0 (GHSA-f88m, HIGH; transformers still
pins ^0.34 upstream — smoke-tested round-trip); marked ^18.0.11; full in-range
lockfile refresh clears hono, fast-uri, protobufjs, qs, body-parser, nanoid,
uuid, immutable and friends.
lib/diagram-render (via its own build-script contract: exact pins edited,
fresh lock, dist rebuilt): mermaid 11.16.1, @excalidraw/excalidraw 0.18.1,
@excalidraw/mermaid-to-excalidraw 1.1.2 → 2.2.2 — the 1.x line exact-pinned
mermaid 10.9.x and dragged the entire duplicate mermaid-10 advisory chain
(dompurify 3.1.6, nanoid 3.3.3, lodash-es); the bundle shrinks 9.96 → 7.59 MB
with the duplicate mermaid gone. Nested exact pins that survived get scoped
overrides (nanoid 5.1.16, lodash-es 4.18.1).
Verification: clean-worktree frozen-lockfile installs (root + nested) + the
SAME osv-scanner release the action pins (v2.3.8) with the workflow's exact
scan-args → exit 0, 'No issues found'. Smoke tests cover the override
surfaces (sharp round-trip, ip-address lockfile assertion, marked parse);
socks + diagram-drift suites already pin the rest.
Supersedes #2695 (its own lockfile kept socks/ip-address@10.2.0; @anupamme's
report credited for the parallel diagnosis).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(gbrain-sync): stub pgrep so the pin case is hermetic
The only non-dry-run --code-only child hits #1734's PATH-resolved
autopilot probe. A live host daemon is a correct refuse; the test
cannot inject processRunning. Neutralize pgrep in the fixture bindir
instead of adding a production env hatch.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(gbrain-sync): blank inherited GBRAIN_HOME in the pin child
Lock paths are checked before pgrep. Spreading process.env let a runner
GBRAIN_HOME with a live lock refuse the case before the stub ran.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: point ship design-checklist at installed gstack/review path
The /ship Design Review step skipped the checklist because the generated path omitted the gstack/ install segment. Sync the generated skill doc and pin a regression assertion.
Co-authored-by: Cursor <cursoragent@cursor.com>
Wave-amended: goldens regenerated against the wave tree (author's golden commit
|
||
|
|
e76f65a8da |
v1.77.0.0 feat: test-infrastructure overhaul wave 1 — matrix deletion, flake telemetry, sync-spawn wedge class extinct (#2746)
* fix: pin the claude CLI to an exact version in the CI image + tripwire The image installed @anthropic-ai/claude-code UNPINNED and rebuilt weekly 'to pick up CLI updates' — while bun sat carefully pinned at 1.3.13 two RUN lines above. The PTY harness screen-scrapes this CLI's TUI, and that drift broke it three separate times (welcome-screen wedge on 2.1.233, skillify HOME discovery on 2.1.237, guard/freeze hooks on 2.1.162), each debugged as a flake first. Pin 2.1.251 (current latest), bump deliberately via a PR that runs the PTY gate, and enforce with test/ci-image-cli-pin.test.ts: any global npm install in Dockerfile.ci without an exact @X.Y.Z pin fails the free suite. The weekly ci-image cron stays as a cheap tag self-heal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: stamp the claude CLI version into every eval-store run record Three harness breakages were traced to claude-CLI TUI drift only after long flake hunts, because no run record said which CLI it actually exercised. EvalCollector now stamps claude_cli_version (claude --version, cached once per process, 'unknown' when the binary is absent) into both partial and finalized records — schema-additive optional field, no SCHEMA_VERSION bump. Correlating a flake wave with a CLI release becomes a grep over ~/.gstack/projects/<slug>/evals/ instead of archaeology. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: give the spinning-shard kill test load headroom (30s -> 90s) The test spawns and group-kills three real children (one a busy-loop burning a full core) while five sibling shard processes compete for eight vCPUs. Under full-suite load it blew bun's default 30s per-test ceiling at 30,009ms — while passing in isolation in 1.4s — and red the only required lane. Every assertion in it is event-based (statuses, group-kill proof, heartbeat lines); the sole latency claim is the <30s kill-deadline sanity bound, which stays. Explicit 90s headroom, not a weakened oracle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: green-by-skip census — skip counts in the classifier, all-skipped labeling in the paid runner bun's 'Ran N tests' line COUNTS skipped tests, so a codex/gemini shard whose every test self-skipped (binary absent on the runner — true of every CI runner today) exits 0, dodges the hollow-shard guard, and reads as coverage in the weekly census. The classifier now parses bun's ' N skip' / ' N pass' recap lines; ShardOutcome carries skippedTests; formatSummary and the fail-closed slices report label an all-skipped pass explicitly: 'all N tests SKIPPED — verified nothing'. Status stays 'passed' (external service availability is host state, not a repo regression) but the census can no longer mistake absence for coverage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: extract composite actions for eval-lane setup; surviving lanes gain the fail-fast registry verification 'Fix bun temp' x3, 'Restore deps' x5, 'Seed claude interactive config' x3, and 'Register gstack skills' x3 were byte-near-identical copies across the legacy matrix, the sliced lane, and the periodic lane — and only the MATRIX copy of register-skills carried the 19-line dangling-symlink + frontmatter fail-fast loop written after a silent 'Unknown command' + 35-min-timeout incident. Extract all four into .github/actions/ composites; the register composite carries the verification loop (generalized over the skill list), so the sliced and periodic lanes — the lanes that SURVIVE the matrix deletion — now inherit the check they had silently dropped. Matrix-job inline copies are left untouched: that job is deleted next. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: delete the legacy 17-row eval matrix — the sliced lane is the only paid lane Every PR paid twice: the hand-enumerated matrix (18 test files, 22.6 min, ~$21 API measured on run 33263204465) ran serialized AHEAD of the strictly superior sliced lane via 'needs: evals' — 35.5 min wall and ~2x paid spend for the same diff. 14 of 17 rows carried no tier:, so periodic Opus benchmarks leaked into every PR (the e2e-plan row alone: 12/12 tests, 21.7 min, $7.28 — the wall-clock bound of ALL of CI). Parity receipt (static, pre-deletion): the sliced lane's gate census (49 files, derived from the runner itself) strictly contains all 18 matrix test files, plus 31 files the matrix never ran. Pure deletion — one revert restores it. The PR comment moved into slices-report (same '## E2E Evals' upsert marker, now sourced from slice artifacts + carrying the fail-closed reconciliation verdict). plan-slices loses the needs edge; the dead workflow-level EVALS_TIER env goes with it. test/evals-workflow-matrix.test.ts (and its KNOWN_MATRIX_GAPS / KNOWN_TIER_UNSET burn-down ratchets — retired: the sliced census makes 'every gate file runs' true by construction) is rewritten as test/evals-workflow-wiring.test.ts: matrix stays deleted, planner/executor/ report tier + slice-count agreement, both surviving lanes on the shared register-skills composite with its fail-fast verification loop, PR comment survival. Expected: PR eval wall 35.5 -> ~13 min, per-PR paid spend ~halved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: provider-runner timeouts kill the whole process GROUP; codex/gemini inherit the orphan-drain hardening All three provider runners (claude/codex/gemini) killed only the direct child on timeout: tool subprocesses the CLI spawned survived as orphans holding our pipes open and burning shared API rate (observed: a 600s timeout stretching past 1400s; a stalled run once burned a core for 15 hours). gstack-detach's watchdog had the same shape one level up — killpg SIGTERM, 5s grace, then a direct-child proc.kill() that orphaned grandchildren. Fix: spawn provider children via node:child_process with detached (own process group) and killProcessGroup(SIGKILL) in the timeout handler — runShardChild's proven pattern, EPERM/ESRCH fallbacks included. The codex and gemini copies also gain the reader.cancel() + stderr Promise.race hardening only the claude copy had (they still carried the blocked-drain hang it fixed). gstack-detach's watchdog now group-SIGKILLs after the grace. Regression net: test/session-runner-groupkill.test.ts drives the REAL runSkillTest against a fake claude shim (PATH override) that spawns a grandchild and wedges — the run must classify timeout within budget and leave neither shim nor grandchild alive — plus source pins on all three runners (detached + killProcessGroup, no bare timeout kill, no Bun.spawn reversion). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: skill-e2e-opus-47 renders SKILL.md fixtures into a mkdtemp — never the live tree mkEvalRoot ran gen-skill-docs with cwd=ROOT, regenerating every in-repo SKILL.md mid-run while concurrent paid shards copyFileSync those same files in their beforeAll (EVALS_JOBS>=4 locally, 2 per CI slice) — a sibling could capture a half-regenerated or opus-rendered SKILL.md, and a timeout before afterAll stranded the whole tree at the wrong model for every later shard. A cross-shard race that could flake ANY concurrent paid test. Render via the --out-dir flag gen-skill-docs grew for exactly this reason (mirrors the repo layout, which is all the fixture reads), read the skill heads from the render dir, delete it, and drop the afterAll restore-regen entirely. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: claude CLI version resolves in the runner parent, never on a test thread Eng-review finding: getClaudeCliVersion's fallback is a SYNCHRONOUS spawnSync on the same thread that polls concurrent PTY/session tests — the judgePtyState blocking class this overhaul kills elsewhere. The paid runner parent now resolves it once (cached) and stamps GSTACK_CLAUDE_CLI_VERSION into every shard's env; eval-store short-circuits on the env var, and the fallback spawn's budget tightens 10s -> 3s (bounded one-time stall, records 'unknown' on a slow CLI). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: wire skippedTests end-to-end through runPaidShard The census unit tests hand-built outcomes and the classifier tests parsed strings; nothing proved a real child's ' N skip' recap flows into outcome.skippedTests and the formatSummary label. A commandFor fake now prints the recap shape and the test asserts the parsed counts, the all-skipped predicate, and the 'verified nothing' label. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: make the setup composites rerun-safe (codex diff-review hardenings) restore-deps: 'cp -r SRC node_modules' with an existing node_modules NESTS the copy and leaves stale deps active — rm first. register-gstack-skills: 'ln -snf' hard-errors under set -eu when a REAL directory occupies the gstack slot — clear a non-symlink leftover first. CI workspaces are fresh today; a reusable composite must survive dirty reruns. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: sweep — every sync spawn in the test trees carries a timeout (436 sites, 157 files) spawnSync/execSync/Bun.spawnSync BLOCK the main thread, so bun's in-process per-test timeout can never fire while one waits — a hung child (stdin read, network probe, dead daemon) wedges the whole shard until the runner's external wall-clock SIGKILL. This exact class reached main: free-tests run 33262077256, test/gstack-memory-ingest.test.ts (normally 2.3s) held shard 2 at the 360s wall while its five siblings finished in ~65s. Mechanical sweep in two waves (12 + 4 fan-out agents, every edit verified against its call site): default timeout: 30_000 (matches the free runner's per-test budget), 120_000 for genuinely slow ops (installs, builds, playwright, provider CLIs), helper wrappers fixed ONCE where call sites route through them. Sites that only LOOK like calls (string fixtures, grep needles, comments) were skipped with reasons — the enforcement commit that follows marks them exempt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: sync-spawn timeout tripwire — the wedge class stays extinct Free scanner over all test trees (test/, browse/test/, design/test/, make-pdf/test/, ios-qa, browser-skills): every spawnSync/execSync/ Bun.spawnSync call site must carry a timeout within a 30-line options window, or an explicit '// tripwire-exempt: <reason>' marker. Comment lines are skipped; exemptions are counted and ratcheted shrink-only (ceiling 6 = the 6 string-fixture/grep-needle sites where the pattern is CONTENT, not a call — marked in this commit). A scan-sanity test pins that the scanner still sees >100 real call sites so it can never rot to a vacuous green. Companion to the 436-site sweep in the previous commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: paid-lane flake telemetry — record-level attempts, flaky_retries, report surfacing bun --retry leaves a retried pass INVISIBLE in its output: a fail-then-pass prints the error detail but no (fail) result line and recaps as a clean pass (probed live on 1.3.10). So attempts are recorded where they cannot lie: EvalCollector.addTest stamps a 1-based attempt on same-name re-records (a retried test runs its body again and re-records), finalized runs carry flaky_retries, printSummary warns loudly, and the fail-closed slices report lists every passed-only-on-retry test — recorded and ranked, never blocking and never silent. Cross-model confirmed (codex reached the same don't-parse -the-stream conclusion independently). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: free-lane flake ledger — retry ON in CI, flaky-passes recorded and uploaded The runner's attribution-gated flaky-retry pass (cap 5, truncation veto) was OFF in the required lane and its FLAKY-PASS evidence was console-only — so a single timing flake red the merge gate while repeat offenders stayed unenumerable. free-tests.yml now sets GSTACK_FREE_RETRY_FLAKY=1 and points GSTACK_FLAKE_LEDGER at runner.temp; every flaky-pass appends a JSONL entry (SINGLE writer: the parent runner — no concurrent-append hazard by construction; fail-open with a loud warning so a broken ledger can never red the lane) and the artifact uploads UNCONDITIONALLY — a flaky-pass run is green, which is exactly when the evidence matters. Wiring pinned by free-tests-workflow-wiring; ledger behavior unit-tested incl. the fail-open path. Matches 2026 industry practice (retry for data, quarantine out of merge-blocking but never out of logging) with the repo's own receipts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: eval:flake-rank — the flake-telemetry dial Aggregates per-test series across every finalized eval-store run (shard dirs included) plus the free flake ledger: runs, fails, RETRIED PASSES (the flake signature), avg duration — ranked retries-first. This is the readable dial behind two policies: a flaky pass never blocks a merge but is always ranked here, and the WS16 required-check promotion needs weeks of clean flake-rank, not vibes. --json for machines, --dir for downloaded CI artifacts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: two-phase session timeout — silent APIs die at the startup grace, named The single spawn-armed timer charged API queue latency to the work budget: the recurring '0 turns / $0.00 / x3 attempts' failure with four budget-bump receipts (180->300s, 240->360s, 300->420s, 90->300s). Split: startup phase (no NDJSON byte yet) kills EARLY at min(grace, timeout) with the distinct exitReason 'timeout_startup' — an availability verdict, not transcript archaeology — and the work phase arms on the first byte for the REMAINING budget, so total wall never exceeds the timeout (tier envelopes are margin-free: tests pass timeout: CAPTURE_MS and bun-budget the same tier). Local grace 90s (observed queue latency 60-90s), CI floor 300s (TODOS-filed; shared runners queue harder), both pinned by the new grace tests with fake -claude shims covering the late-first-byte and silent-API paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: census integrity — 17 phantom selection keys deleted, reverse invariant added, gitignored dep patterns replaced, local map forks derived The merge-blocking gate census counted tests that could not run. Deleted (critic-verified against both quoted-occurrence and dep-registration liveness): 7 *-prosons-format keys with no declaring test, ship-plan- completion/-verification, review-plan-completion, design-shotgun-path/ session/full, autoplan-core (dead ~10 months), e2e-harness-audit (its namesake is a FREE-suite file), plus 2 dead LLM-judge keys and 2 free-file keys (budget-regression-pty, global-discover) misplaced in the PAID maps. Census: 191 -> 174 keys, gate 86 -> 78 honest. The new reverse invariant in touchfiles.test.ts makes the class structurally impossible: every key must be quoted in a living paid test file OR registered to an existing paid test file via its dep list (the constructed- name binding the 2026-08 self-registration sweep established) — zero exceptions needed today, with a live-file check on any future exception. Also: '.agents/skills/**' dep patterns replaced with the generator (scripts/gen-skill-docs.ts) — .agents/ is gitignored, so those patterns could NEVER match a git diff and review-template edits silently stopped selecting codex/gemini tests; the codex/gemini local touchfile maps are now DERIVED from the canonical map (loud throw if a key vanishes) instead of hand-forked copies that had already drifted. ios-qa-e2e demoted gate -> periodic: its gate declaration was never executable in CI (hardware exclusion only applies at tier=periodic), so every Linux PR planned a hollow shard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: routing journeys lose their answer key and end at the routing decision The journey tests exist to catch skill-DESCRIPTION regressions (touchfiles: */SKILL.md.tmpl), but the fixture CLAUDE.md shipped an explicit prompt->skill lookup table — with the answer key in context, a badly regressed frontmatter description still routed correctly, so the tests could not fail on the exact class they select for. The fixture now carries only the generic invoke-skills nudge; the frontmatter carries the routing load. Also capped all 10 journeys at maxTurns 2 / tools [Skill, Read]: only the FIRST Skill call is asserted, so 5 turns of Read/Bash/Glob/Grep was pure spend — roughly halves each journey's cost. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: retire decided A/B experiments; vendor the pre-cut fixture; ban raw-SHA fixtures Three one-shot decision experiments kept re-running weekly as N=1 stochastic comparisons — flaky by construction with near-zero remaining information: skill-e2e-auq-repetition-cut-ab (its own header: gate "passed pre-landing, approved 2026-08-25"), skill-e2e-preamble-script-ab ("demoted post-Phase-3"), and opus-47's fanout arm-vs-arm (parA >= parB across two SINGLE stochastic runs — a coin flip). Deleted, with their selection keys; the SDK overlay-harness stays as the maintained instrument for the next experiment, and opus-47 keeps its routing-precision cases. verboseSkill() now reads the VENDORED test/fixtures/auq-pre-cut-...-SKILL.md instead of `git show ab66193e^:...` — a branch-local ref that dies on branch prune and already failed on shallow clones. New free tripwire (test/git-ref-fixture-tripwire.test.ts) bans the raw-SHA fixture class outright: quoted SHA:path rev-specs and gitRef-style hex defaults in the test trees fail the suite with the vendor-instead instruction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: demote plan-ceo-review-expansion-energy to periodic Opus generator + a subjective 2-axis >=4/5 LLM-judge threshold sat in the MERGE-BLOCKING gate — the exact class its sibling posture tests were demoted for, with a receipt (a +21-line preamble change once flipped the score). CLAUDE.md's own tiering rule: Opus model test -> periodic. The weekly lane keeps the regression signal; merges stop paying a judge- temperament tax. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: paid shards get per-shard TMPDIR + CHROMIUM_PROFILE isolation and a kill-path cleanup backstop The free runner treats this isolation as MANDATORY (two concurrent shards on one Chromium profile kill each other's browser; shared tmp cross-contaminates) — the paid lane had none of it. Doubly load-bearing here: a shard that hits its 30-min wall is group-SIGKILLed, so per-test afterAll cleanup never runs; the rmSync backstop is the only thing keeping wedged runs from accumulating full git-repo workspaces in the shared tmpdir forever. This is the DAG prerequisite for raising EVALS_JOBS (next commit) — more concurrency on shared state amplifies exactly the shared-tree race class opus-47 exhibited. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: paid-runner defaults 4x4 -> 8x2 — halve the local gate worst case 39 of 75 skill-e2e files hold exactly ONE test, so within-shard concurrency was dead weight for most shards: 4 jobs x 4 concurrency yielded only ~4-6 real in-flight sessions and a 13-wave local gate worst case (~6.5h). 8 jobs x 2 gives ~10-13 in-flight — under the documented-safe ~15 — and ~7 waves (~3.3h worst case). CI lanes keep their explicit EVALS_JOBS env (2 per slice; 4 for gate-census); this changes local defaults. Rollback trigger: sustained 429 storms in the WS1 telemetry across 2 PR cycles. test/eval-detach-timeout-floor.test.ts recomputed green (the raise LOWERS the worst-case floor). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: SHA-pin every action in the secrets-bearing eval lanes evals.yml and evals-periodic.yml execute PR-authored code with three provider API keys in env, yet rode mutable action tags (@v7/@v8/@v2/@v4) — while quality-gate.yml, osv-scanner.yml, and dependency-review.yml already model the SHA-pin pattern. All 30 uses sites across both lanes now pin the exact commit (tag noted in a trailing comment); dependabot's github-actions ecosystem keeps them fresh via PRs instead of silent tag moves. Pulled forward from the plan's endgame on the CEO-review + outside- voice agreement: supply-chain pins on secret lanes go first, not last. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: sweep wave 3 — the execFileSync family gets timeouts (90 sites, 17 files) The tripwire's regex covered spawnSync/execSync/Bun.spawnSync but not execFileSync — an entire blocking sync-spawn API family that could reintroduce the shard-wedge class undetected (ship review army). Same mechanical recipe as waves 1-2: timeout: 30_000 default, 120_000 for slow ops, shared wrappers fixed once, string-needle sites skipped with reasons. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: review-army + adversarial test hardening - Tripwire scans execFileSync too (ceiling 8: two more grep-needle string exemptions); merge-introduced timeout-less spawnSync in question-preference-hook fixed — the tripwire caught a site that landed on main AFTER the sweep, on its first day. - gstack-detach gains TWO watchdog kill regression tests: TERM-immune grandchild (the killpg-after-grace escalation) and the leader-dies variant (the pgid-at-spawn fix — the case the first test cannot see). - eval-flake-rank gets its unit suite (final-attempt accounting, artifact exclusion, shard recursion, recency bound). - Groupkill/startup-grace shim markers are per-run unique (pid-suffixed sleep durations): sibling Conductor worktrees run free suites with no machine lock, and fixed markers let one run pgrep/pkill the other's shims — a cross-run flake inside the anti-flake tests. - flake-ledger test pins the project-scoped local default; stale empty section headers in touchfiles-data deleted (they invited entries under deliberately retired categories). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: adversarial-review runtime fixes across the telemetry + kill paths - session-runner: exit-labeling keys off 'exit', not 'close' — an orphan holding the pipes could relabel a REAL exit (auth failure) as 'timeout_startup' availability noise; the kill path still always group-kills and cancels the reader (labeling and unblocking are separate concerns). Work phase arms on a flag, not firstResponseMs===0 (a same-ms first byte left the startup timer live all run). The CI startup grace is now a real FLOOR (Math.max), matching its name and pinning test. - gstack-detach: pgid captured AT SPAWN (== child pid under start_new_session) — resolving it after the grace raised ESRCH once the leader died on SIGTERM, orphaning TERM-immune grandchildren forever. - test-free-shards: ledger entries carry branch + git_sha (rev-parse split: '--abbrev-ref HEAD HEAD' printed the branch twice and recorded it as the sha); local ledger default is per-PROJECT, not the machine-global tmpdir. - eval-flake-rank: per-LINE ledger parse (one torn JSONL line vanished the whole series), 60-day recency bound (transcript-bearing files are MBs), shared isFinalizedEvalResultFile predicate (the artifact-taxonomy rule lived in three places); eval-store exports the predicate and finalize stops computing flakyRetries twice; paid-shards cleanup uses async rm (a SIGKILLed shard's git-workspace teardown blocked every sibling's stream classification on the parent event loop). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: CI trust-boundary + fail-closed repairs (adversarial findings) - Token/exec separation restored: slices-report (runs PR-authored code: bun install + the reconcile runner) drops to contents:read; the PR comment moves to a NEW slices-comment job holding the write token with ZERO repo code — no checkout, no bun, only downloaded artifacts + jq/gh. $GITHUB_ENV/BASH_ENV persistence is job-scoped, so the split is the boundary. The matrix-era report job had this property; the consolidation had regressed it. Pinned by the wiring test. - Reconcile exit captured via PIPESTATUS[0] in BOTH lanes: GitHub's default run-step shell has no pipefail, so `$?` after `| tee` was tee's exit — the fail-closed gate was silently fail-open. Wiring test pins it. - PR comment: final-attempt accounting restored the dropped COST accumulation (the dial read $0 forever), flaky passes render as the warning they are (never as failures), and a malformed tests[] artifact skips that file instead of aborting the whole comment under bash -e. - Remaining mutable action tags pinned (free-tests upload-artifact, ci-image checkout/docker trio — the image publisher holds packages:write and feeds the secret-bearing lanes). restore-deps fallback installs --frozen-lockfile; register-gstack-skills validates skill names before its rm -rf. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: bump version and changelog (v1.77.0.0) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: update project documentation for v1.77.0.0 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: cross-model doc-review fixes — flake-ledger env knobs, CI retry-on note, stale version comment Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: correct CHANGELOG receipt numbers to measured values Gate census keys: 78 -> 77 (bun-imported E2E_TIERS count). Sweep receipt: 586 sites/176 files -> 499 sites/146 files, measured by running this branch's spawnsync-timeout-tripwire against origin/main (exit 1, 499 violations across 146 unique files; green on this branch). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: slices-comment creates the PR comment via REST — the write-token job has no git context The token/exec split gives slices-comment NO checkout by design, and gh's pr-comment subcommand resolves the repo FROM git — it died with 'not a git repository' on PR #2746's first run (the update-existing PATCH path was already explicit-repo REST and worked). Create now posts through gh api repos/.../issues/N/comments, and the wiring test pins that no git-context-requiring comment call can creep back into the job. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: startup-grace probes clear CI for local semantics; new probe pins the floor clamp The two shim probes pass explicit 2s/4s graces, but in CI the runner clamps any explicit grace up to the 300s floor (deliberate adversarial-review fix), so 'silent API killed at the grace' died at the 30s work cap instead of 2s — a deterministic red on every CI run, green locally. The probes now pin LOCAL semantics with CI cleared (same save/restore pattern as their PATH shim), and a fourth probe pins the clamp itself: CI=1 + 2s grace + 6s timeout must kill at the 6s cap, still in the startup phase — proof an explicit low grace cannot bypass the floor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
253d1dfe26 |
v1.76.0.0 fix: ship doc-sync survives Conductor — spawned subagent sessions reachable (#2733) (#2741)
* feat(session-kind): explicit GSTACK_SESSION_KIND override; skill-start spawned gates keyed on kind (#2733) Claude Code subagents inherit the parent env byte-for-byte, so ambient markers classify them as the parent's kind and the spawned classification was unreachable outside OpenClaw. GSTACK_SESSION_KIND=spawned (step 0, spawned-only by design) lets a dispatching skill mark its subagent per command. skill-start now keys SPAWNED_SESSION and the spawned-session instruction block on the resolved kind (was raw OPENCLAW_SESSION), suppresses CONDUCTOR_SESSION for spawned sessions, gates all 11 interactive-onboarding blocks plus their ack-at-emit marker writes on kind != spawned, and adds a destructive-gate carve-out to the spawned block (conservative-continue, never prose-STOP). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(hooks): spawned-session escape in Conductor AUQ deny; override coverage in AUQ-error fallback (#2733) Hooks inherit the harness env, so a per-command GSTACK_SESSION_KIND prefix inside a subagent's bash can never reach them. Levers added: a deterministic [conductor][spawned] auto-choose deny for env-level spawned sessions (OPENCLAW_SESSION or session-wide GSTACK_SESSION_KIND), and a spawned escape sentence appended to both hooks' prose directives so a marked subagent that slips and calls AUQ resolves to auto-choose instead of prose-STOP. The sentence lives in one shared constant (hosts/claude/hooks/spawned-directive.ts) so the two paths can never drift; destructive semantics are unified to conservative-continue. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ship): Step 18 marks the document-release subagent spawned — env prefix + auto-choose prompt (#2733) The dispatch prompt now (1) frames the run as a SPAWNED subagent whose LAST line is machine-parsed, (2) instructs prefixing the preamble's gstack-skill-start invocation with GSTACK_SESSION_KIND=spawned on the same command line (template bash blocks don't share exports), and (3) resolves every AUQ gate to auto-choosing the recommended option, conservative on no-recommendation, never destructive. The JSON contract gains a required "decisions" array (auto-chosen gates, printed to the ship console — never embedded in the public PR body) and a placement clause so the skill's own doc-health summary stops competing with the LAST-line JSON. Tripwire pins added; codex/factory goldens refreshed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(auq-format): proactive SESSION_KIND=spawned rule ordered above the Conductor rule (#2733) The spawned classification previously existed only in the failure-fallback branch — a spawned session was invited to call AskUserQuestion and reach auto-choose via the deny/error detour, and a spawned session inside a Conductor workspace hit the Conductor prose-STOP rule first. The Tool resolution list now leads with the spawned rule (auto-choose recommended, never prose, never BLOCKED, destructive gates resolve conservative), the self-check carries the never-reach-this-checklist clause, and all tier>=2 SKILL.md renders are regenerated. Context-budget fixture refreshed in the same commit per the ratchet protocol (the AUQ section is eager in every tier>=2 skill). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(e2e): spawned document-release subagent returns the JSON contract through a firing gate (#2733) The behavioral proof the bug shipped without: ship-docsync stubs the skill (no preamble, no gates) and skill-e2e-workflow suppresses the gates by prompt. This gate-tier E2E plays the parent — it drives the verbatim Step 18 dispatch prompt (extracted from the live pr-body.md, drift-proof) against a real preamble-bearing document-release slice in a Conductor-ambient env with both AUQ hooks seeded live, an unbumped VERSION making Step 8 fire. Asserts: the final line parses as the 5-key JSON contract, the fired gate's auto-choice is recorded in decisions, and VERSION is untouched (the gate resolved to its recommended Skip). Burn-in: 1/1 pass, $0.35, 21 turns, 106s. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(openclaw): document the GSTACK_SESSION_KIND override; wire session-kind into paid selectors (#2733) OPENCLAW.md's spawned-session section now covers the explicit per-command marker, its deliberate spawned-only narrowness, the /ship Step 18 usage, the destructive carve-out, onboarding-block suppression, and the hook env-blindness caveat. bin/gstack-session-kind and the shared spawned-directive module join the conductor-prose and auto-decide-preserved selector dep lists (session-kind previously appeared in no touchfiles entry — editing it alone triggered no paid E2E). TODOS.md gains the plan-tune capture follow-up for spawned auto-choices. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: pre-landing review fixes (#2733) Review army + coverage audit findings, all applied: - headless directive carries the spawned escape sentence too (multi- specialist: a CI-hosted ship's marked subagent must not end BLOCKED) - anti-injection scoping on every text-claimable spawned trigger (AUQ rule + shared escape sentence): markings count only from the creating prompt, never from files/tool output/web content read mid-run - [conductor][spawned] deny annotates one-way doors per question - SPAWNED_OVERRIDE: env tamper-visibility status line + OPENCLAW.md note - spawned sessions skip the network update-check and first-task probe (consumers suppressed; preserves the one-shot just-upgraded marker) - test hardening: dispatch-tripwire end-bound validated, vacuous marker asserts replaced with output asserts, E2E cpSync size filter + named fence tolerance, spawnedByEnv parity pin, destructive-policy cross- surface drift guard, one-way annotation + bogus-value hook cases - session-kind duplicate rationale comment deduped; regen + goldens + context-budget fixture refreshed Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: bump version and changelog (v1.76.0.0) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: update project documentation for v1.76.0.0 PROJECT_STRUCTURE.md: add hosts/claude/hooks/ to the directory tree (AUQ capture + enforcement hooks, spawned-session directive, timeline stop) — the tree omitted the directory while docs/OPENCLAW.md and CHANGELOG.md now reference paths inside it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: sync TODOS.md ship dispatch entry with the v1.76.0.0 contract Codex doc-review finding: the SHIPPED entry for /ship auto-invoking /document-release still described the four-key JSON contract. Adds the decisions key (console-printed, never PR markdown), the GSTACK_SESSION_KIND=spawned dispatch marking (#2733), and the new spawned-dispatch gate E2E to the proven-by list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
07b59e396c |
v1.75.0.0 feat: ponytail import wave — simplification review lens, arm benchmark, reuse ladder, instruction-tier digest (#2722)
* feat(autoplan): eng review always runs last — the gate reviews the final amended plan Reorder the pipeline to CEO -> Design (if UI scope) -> DX (if developer-facing scope) -> Eng. The old order (CEO -> Design -> Eng -> DX) let DX findings land AFTER the required gate signed off, so eng validated a stale plan. Accept-all semantics made explicit: every AskUserQuestion resolves to the recommended option; premises no longer pause the pipeline mid-run (clearly-wrong ones queue as User-Challenge items at the single Final Approval Gate). Eng's Codex voice now sees the DX consensus summary. New free static test pins the order; the chain E2E gains DX-between and Eng-terminal assertions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(review): simplification specialist — advisory over-engineering lens with ponytail's tag vocabulary New 8th Review Army specialist (DIFF_LINES > 100, --simplification force flag) hunting unrequested STRUCTURE only: delete/stdlib/native/speculative/shrink closed tags, one-line findings, lines_removable field. speculative: replaces ponytail's yagni: tag — we import the lens, not the posture; coverage stays sacred (Completeness Gaps owns it, suppressions inlined, shrink needs >=5 lines). Advisory carve-out in the merge step: advisory findings are excluded from quality_score and the findings-count header, render with an [ADVISORY] label, and are ASK-only in Fix-First. Zero-findings case prints the lens-scoped 'Simplification: lean already — nothing to cut.' from the PARENT (the specialist keeps the exact NO FINDINGS contract); with findings, the parent prints 'net: -N lines possible' summed from lines_removable. Tests: static pins for the carve-out + early-out contract (gen-skill-docs), two periodic e2e cases with planted fixtures — activation (over-build traps: hand-rolled Intl, one-impl abstract, dead config) and false-flag precision (a lean ETHOS 'choose A' diff must yield NO FINDINGS). Inspired by dietrichgebert/ponytail's /ponytail-review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(preamble): reuse ladder in Search Before Building — rungs 2-5 of ponytail's ladder, completeness kept Tier-3+ skills gain a per-edit reflex the section only stated as research discipline: before writing new code, stop at the first rung that holds — repo helper, stdlib, native platform feature, installed dependency — then build the COMPLETE version of what remains. The closing clause is the explicit reconciliation with Boil the Ocean: the ladder governs structure, never coverage. Rungs 1/6/7 (YAGNI / one line / minimum that works) are deliberately NOT imported. Also ports ponytail's root-cause rule: one guard in the shared function beats a guard in every caller. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(preamble): bounded-closer output rule for tier-2+ skills After completing work, skills report in a few short lines — what changed, what was skipped, what to watch — and cut any explanation that outgrows the change. Explicit exemptions protect every mandated output: decision briefs, completion-status blocks, user-requested explanations, and report-shaped skills' report formats (the report IS the work in /qa-only, /plan-*-review, /retro, /document-generate). Rationale is signal-to-noise, not tokens: ponytail's own benchmark shows terse prose alone doesn't cut cost (caveman arm: -20% LOC, +7% tokens), and independent replications found its 'skipped on purpose' essays ate the code savings. Includes a good/bad closer example pair per the model-overlay guidance that a positive example beats a 'don't be verbose' instruction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(resolvers): terse-mode savings claim matches measurement — 2.6KB, not 3-5KB Measured on the v1.71 render: --explain-level=terse saves exactly 2,611 bytes per tier-2+ skill. The old ~3-5KB claim predated the preamble restructuring. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(retro,preamble): gstack-shortcut debt ledger — accepted shortcuts leave a joined trail When the user accepts an option that is BOTH Completeness <= 7 AND a durable-scope call, the decision ledger entry (gstack-decision-log, ceiling + upgrade trigger in the rationale) is the source of truth, and the agent marks each cut corner in code with gstack-shortcut(dec-<id>): <ceiling>, upgrade when <trigger> — same edit, no follow-up question, never agent-initiated. /retro Step 11.5 harvests markers into a debt ledger (grep || true — zero matches is the healthy case; skill installs and docs excluded), joins on the decision id so nothing double-counts, tags unlinked and no-trigger rot risks, and closes with 'N markers, M with no trigger.' /review suppressions: a marker with ceiling+trigger downgrades a would-be Completeness Gaps finding to acknowledged debt. Redaction test pins that the marker ships untouched (the ledger is the point) — it does not match the TODO(owner) hygiene shape. Format from dietrichgebert/ponytail's ponytail-debt; store inverted to gstack's existing decision ledger. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: refresh golden ship baselines after preamble additions (reuse ladder + bounded closer) The golden-file regression test pins the rendered ship skill byte-for-byte; the WS3/WS7 preamble sections are deliberate changes, so the baselines re-capture per the goldens' own update protocol. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(hosts): instruction-only tier — a 2KB committed rules digest any agent host can read New agents-digest/gstack-AGENTS.md (1,765 bytes, hard 2,048-byte budget): gstack's ethos one-liners, the reuse ladder, and voice rules for hosts with no install arm — Zed, Amp, Jules, or any AGENTS.md-reading agent. Generated by scripts/gen-agents-digest.ts, auto-refreshed by gen:skill-docs, committed like llms.txt so setup's explainer arms can point at it before any toolchain exists. First line carries the gstack version as its own staleness nudge. Delivery is print-path + user-performed copy ONLY: setup never writes or overwrites a user's AGENTS.md (a test pins this — no cp/ln/mv/redirect into AGENTS.md anywhere in setup). openclaw and hermes explainer arms print the path; slate keeps routing to the full Claude install and gbrain ships from its own repo. HostConfig gains the optional install.instructionTier slot, declared by both instruction-tier hosts. README host table now matches what setup actually does. Inspired by dietrichgebert/ponytail's instruction-tier AGENTS.md fallback — one generated source, never per-host hand copies. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(preamble): AskUserQuestion repetition cut — gated, passed NOT-WORSE A/B Removes the duplicate statements v1.71's compaction left in the AskUserQuestion Format section: the completeness rule restated in the prose triad, the auto-decide marker syntax stated twice, the Conductor-flakiness explanation stated twice, and the self-check's full triad restatement. Every verbosity floor and all 14 format pins stay (Layer 0 green). The gate this decision rested on ran before landing (new periodic skill-e2e-auq-repetition-cut-ab.test.ts, pre-cut ref |
||
|
|
b1485d8897 |
v1.74.0.0 test/CI overhaul: green means green, suites restructured for speed (#2721)
* fix(ci): free-tests lane actually runs the make-pdf e2e gates The 9 make-pdf/test/e2e gate tests probe make-pdf/dist/pdf, browse/dist/browse, and the diagram-render bundle, then self-skip when absent. The required free-tests lane never built any of them, so the gates silently skipped on Linux for their entire life (verified: 9 of 14 skip, exit 0). make-pdf-gate.yml's justification for deleting its Linux leg claimed the free lane covered this — it didn't. - new build:gates script: exactly the three artifacts the gates probe (full bun run build compiles five binaries; ~60-90s tax on the only required check is not warranted) - free-tests.yml: build:gates step + poppler-utils + fonts-noto-color-emoji (fonts must precede the first browse daemon launch — Chromium snapshots fontconfig at startup; verified live: a warm daemon renders tofu, a fresh one embeds NotoColorEmoji) - make-pdf/test/e2e/ci-prereqs.test.ts: GSTACK_EXPECT_BINARIES=1 (set by the workflow) inverts the skip polarity in CI — dropping the build step or poppler fails the lane instead of re-opening the silent-skip hole Pre-flight: all 9 gates green on Linux locally. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ci): kill the three zero-test eval jobs (hollow green) - delete the vestigial e2e-codex / e2e-gemini matrix rows: both files are whole-file periodic-tier, so with no row tier: they ran ZERO tests and reported green on every PR (~2 min of runner each, pure false confidence; the periodic lane owns those suites) - e2e-pty-plan-smoke gains tier: gate — its two files are whole-file describeE2ETier('gate'), so the job burned ~7 min of container setup then skipped every describe - KNOWN_TIER_UNSET burned down to empty; the ratchet stays armed so a future row/file tier mismatch fails the suite instead of shipping hollow green Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ci): least-privilege permissions + fork-safe concurrency keys - evals.yml / evals-periodic.yml evals jobs: explicit contents:read + packages:read (container-image pull) and persist-credentials:false — the jobs that execute PR-authored code with three provider API keys ran on the repo-default token grant with the token written into .git/config - permissions blocks for the 4 workflows that had none (skill-docs, make-pdf-gate, windows-free-tests, windows-setup-e2e) - fork-safe concurrency keys: actionlint, skill-docs, make-pdf-gate, windows-setup-e2e switch from head_ref to PR-number keying — a bare branch name carries no fork prefix, so same-name branches from two forks shared one group and cancelled each other's runs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ci): one bun version everywhere + drift tripwire Lanes disagreed four ways: 1.3.13 (free-tests, windows, Dockerfile.ci), latest (quality-gate, make-pdf-gate), unpinned (skill-docs, version-gate — setup-bun installs latest), 1.3.10 (.gitlab-ci.yml). Different Bun versions change the runner output shapes the strict classifiers regex-match, spawn semantics, and shell parsing — a lane on a different Bun tests a different product; Dockerfile.ci's own comment records this class biting once already (silent 1.3.13/1.3.14 drift). All surfaces pinned to 1.3.13; test/bun-version-drift.test.ts scans every workflow setup-bun stanza + Dockerfile.ci + .gitlab-ci.yml and fails on any mismatch or unpinned stanza. skill-docs also gains --frozen-lockfile (was bare bun install). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(ci): bind the three-way image-tag hashFiles() expressions evals.yml, evals-periodic.yml, and ci-image.yml each compute the CI image tag from hashFiles('.github/docker/Dockerfile.ci', 'bun.lock', 'patches/**') — synced by comment only (TODOS.md 'CI three-way image-tag drift'). If one input list drifts, that workflow computes a different tag for the same content: eval lanes silently rebuild the image every run, or ci-image prebuilds a tag nobody looks up. The test extracts each tag-computation site and fails on any mismatch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ci): ci-image stops rebuilding the identical image every ship - package.json out of the trigger paths: the tag hash deliberately excludes it (version bumps every ship), so every merge rebuilt and re-pushed the IDENTICAL tag (~2m26s for zero content change); patches/** added (it IS a tag input) - manifest existence check (mirrors evals.yml): tag already exists → skip the build - concurrency group: two rapid main pushes raced pushing the same :latest/:buildcache tags - cron staggered 06:00→04:00 Monday: it shared the exact minute with evals-periodic, which could race a half-pushed tag or duplicate the build - timeout-minutes: 30 (was unbounded → 360-min default for a hung docker build) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ci): quality-gate drops the 74s full-history checkout fetch-depth:0 cost 74 of the job's 92 seconds; the three gates it feeds take ~12s combined. Shallow checkout + exact-SHA fetches for the diff's base/head (an exact-SHA fetch, not a guessed depth — long-lived branches and merge queues still resolve), with a --deepen fallback for push events whose 'before' is unusable. timeout right-sized 20→10 min. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ci): small-lane batch — timeouts, right-sizing, windows cache warm-start - timeout-minutes on the 6 remaining unbounded jobs (actionlint 5, skill-docs 10, version-gate 10, make-pdf-gate 15, pr-title-sync 5, evals build-image 15) — a hung step sat on GitHub's 360-min default - right-size measured-over-long timeouts: dependency-review 10→5, windows-setup-e2e 15→10 - dependency-review: 2-core runner (28s API call on an 8-core box) and drop .github/workflows/** from its trigger paths (workflow edits have no dependencies to review) - windows caches gain restore-keys: a lockfile bump paid the 26s/43s restore for a guaranteed cold miss Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): scope GSTACK_HOME to each file's execution window Five files assigned process.env.GSTACK_HOME at module scope. Shard processes evaluate sibling modules before running their tests, so the assignment leaked into every other file in the shard — the damage was already visible in defensive workarounds (relink.test.ts:28 'fresh install test saw a neighbor's skill_prefix'; cdp-e2e's own comment documents a sibling's temp dir baked into artifacts). Pattern: save original, assign in beforeAll, restore in afterAll (cdp-e2e already restored but still assigned at load — its window now matches the others). GSTACK_TELEMETRY_OFF and GSTACK_PROJECT_SLUG get the same treatment where they rode along. Victim files' defenses stay in place (cheap insurance). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: tripwire against module-scope GSTACK_HOME assignments Column-0 assignment of GSTACK_HOME / GSTACK_STATE_ROOT in any tracked *.test.ts fails with the file:line and the fix (beforeAll + afterAll restore). Kills the cross-file env-leak class the previous commit swept. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): e2e-harness-audit derives its skill census from disk The hand-maintained 39-name SKILL_GLOBS list had drifted to 39 of 54 SKILL.md.tmpl on disk. No live gap today (none of the 15 unlisted skills is interactive), but the next interactive skill would have landed unguarded with zero signal. The audit now walks top-level dirs for SKILL.md.tmpl (statSync so symlinked dirs like connect-chrome count), so new skills are in scope the commit they appear. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(evals): judges honor the eval-model resolution chain + real 429 backoff callJudge inlined GSTACK_EVAL_MODEL_JUDGE || sonnet, silently ignoring the global GSTACK_EVAL_MODEL override every other eval call site honors via lib/eval-model.ts. New 'judge' kind in DEFAULTS (sonnet — the D1a pin-on-regressors calibration stands; model CHOICE unchanged) and callJudge resolves through it: explicit arg > GSTACK_EVAL_MODEL_JUDGE > GSTACK_EVAL_MODEL > default. 429 handling upgraded from one fixed 1s retry (reliably lost races at CI concurrency) to three jittered exponential retries (~1s/4s/16s), honoring the server's retry-after when present. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): the two expect(true) paid stubs become test.todo skill-e2e-spec-execute (600s budget) and skill-llm-eval-spec (300s) reported PASS on every periodic run while asserting nothing. Deleting them would remove the periodic-tier selector surface they exist to register (diff-based selection for spec/ changes), so they become test.todo — reported as todo/skip, never pass — with the v1.1 implementation specs kept in-file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): reactivate 5 quarantined browse tests (2 security) extension-sender-auth's two privileged-message denial tests (content script + missing sender.url — the extension's security boundary) and snapshot's three skips were quarantined 'pre-existing' failures. Root cause: machine-local state on the quarantining dev machines — the test and gate code are byte-identical between the quarantining commit ( |
||
|
|
b5a951e623 |
v1.72.0.0 feat: Aside recommended driver for third-party web actions (#2710)
* fix(browse): never chmod shared, symlinked, or foreign-owned dirs to 0700 restrictDirectoryPermissions unconditionally chmodded its target. On hosts where the process holds CAP_FOWNER (Docker as root, CI sandboxes) that chmod SUCCEEDS on root-owned /tmp whenever a state file is configured there (BROWSE_STATE_FILE=/tmp/x.json derives stateDir=/tmp), and a 0700 /tmp breaks access(2)-based checks machine-wide for every other process. The POSIX branch now refuses shared sticky dirs, world-writable mounts under root, foreign-owned dirs, and symlinked state dirs; refusals warn once per process instead of failing silent; owned-but-unreadable dirs keep their chmod self-repair; and the check-then-act race is closed with fd-anchored O_NOFOLLOW + fstat/fchmod on a single inode. Regression tests cover the sticky-dir, foreign-uid, mkdirSecure-reapply, and symlinked-dir shapes. * fix: hash with sha256sum before shasum on Linux (config slugs + setup verify) shasum is perl/macOS; coreutils-only Linux ships sha256sum. Two call sites hard-coded shasum: gstack-config's sha8_of/sha16 (so resolve-user-slug exited 127 for any Linux user with a git email, the Layer-3 fallback) and the generated bun-installer checksum snippet in the browse/qa NEEDS_SETUP flow (spurious "checksum mismatch" on the same distros). Both now resolve sha256sum first and fall back to shasum -a 256. New shim-PATH tests pin BOTH hasher branches of sha8_of to a known vector and cover the sha8->sha16 collision escalation end to end. * feat(contract): Aside is the recommended driver for third-party web actions The Third-Party Web Actions contract (ship, spec, office-hours, land-and-deploy, setup-deploy) now names the Aside AI browser as the recommended driver: it acts across the user's real logged-in sessions, which is what vendor-dashboard moments need. Supersedes the v1.65.0.0 de-Aside stance by explicit user directive (2026-08-27). Detection is a runtime probe (command -v + aside --version under a portable gtimeout/timeout/bare guard; nonzero exit = not detected). Consent options render per detection state with Aside recommended and the first-party stack ($B headed + handoff, GStack Browser) as the universal fallback. Absent on macOS, the contract mentions the aside.com download (macOS 15+) once per task; gstack never runs an installer and binary presence is never consent. Drive discipline: step-wise over whole-task delegation, vendor confirm mode on, vendor skill/--help text scoped to operational syntax only, secrets minimized (autofill / human-used copy buttons), Apple credential creation never a drive target in any skill, failure path quotes redacted errors and falls back only with fresh consent. test/third-party-actions.test.ts pins every load-bearing sentence (21 tests) plus repo-wide tripwires: an aside command allowlist (--version/--help only, code spans AND prose) and a ban on Aside installer invocations across all generated docs. Budget ratchet fixture and carve skeleton ceilings refreshed in this commit per the ratchet protocol. * chore: regenerate remaining browse-setup snippet consumers The sha256sum-first checksum fallback in the generated NEEDS_SETUP snippet renders into every browse-consuming skill, not just browse/qa. Mechanical regen of the other ten consumers; no template changes here. * test: consent-gate E2E suite + functional fs-capability probes Five hermetic gate-tier E2E cases (tpa-present / absent-linux / broken / absent-darwin / apple-ban) drive the real contract section through claude -p with PATH shims for aside and uname; the absent cases filter any REAL aside binary out of the child PATH and assert absence with Bun.which before spawning, so dev machines cannot leak into detection. Registered per-case in E2E_TOUCHFILES/E2E_TIERS with template-level deps (ship/SKILL.md.tmpl, gen-skill-docs.ts) and added to the evals.yml matrix with tier: gate. eval:bg:periodic's detach timeout rises to 36000s for the grown periodic shard census (floor-enforced by test/eval-detach-timeout-floor.test.ts); CLAUDE.md doc updated to match. test/helpers/fs-caps.ts adds canRevokeWrites/canRevokeReads functional probes; 13 chmod-based tests swap their uid-0-only guards for the probes so suites skip honestly on CAP_DAC_OVERRIDE containers (this sandbox: uid 1000 with full caps) instead of asserting revocations the kernel ignores. path-validation's symlink test targets /etc/passwd (exists everywhere; /etc/crontab is absent on Amazon Linux). * docs: file the Aside follow-ups in TODOS Phase-2 QA logged-in-evidence path (P3), a hostile-vendor-skill E2E for the contract's override sentence (P2), and fd-anchoring the file-level permission writes to match the directory hardening (P3). * chore: bump version and changelog (v1.72.0.0) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: update project documentation for v1.72.0.0 docs/skills.md: Third-Party Web Actions subsection under /ship (Aside recommended driver, consent rules, credential boundaries). BROWSER.md: "Aside and third-party drives" subsection under Real-browser mode + ToC entry, including the no-gstack-side-audit-trail caveat (ship adversarial finding 12). TODOS.md: mark the finding-12 doc note done. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: apply cross-model doc review fixes for v1.72.0.0 docs/skills.md: restore the /ship closing line above the new subsection. BROWSER.md: ToC label matches the heading; BROWSE_STATE_FILE env row documents the new dir-hardening refusal + one-time warning. CHANGELOG: correct the hasher precedence wording (sha256sum first, shasum fallback) and the fs-caps count (14 test files, verified against the diff). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: close cross-model doc-review gaps for v1.72.0.0 setup's manual bun-verify instruction gets the same sha256sum-first fallback the automated snippet got (coreutils-only Linux); BROWSER.md's BROWSE_STATE_FILE row now lists the under-root world-writable refusal; test-cost ceilings in CLAUDE.md/CONTRIBUTING.md updated for the five new gate E2E cases (~$4.20 E2E / ~$4.35 evals). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): gate the symlink-refusal test to POSIX and drop the umask assumption The symlink regression test exercised the POSIX O_NOFOLLOW branch but ran on Windows, where restrictDirectoryPermissions takes the icacls branch and stat has no POSIX modes (0o666 always) — windows-free-tests failed on mode 493 vs 438. Early-return on win32 like every sibling test in the file, and assert the target's mode is UNCHANGED (captured post-mkdir) instead of hardcoding 0o755, which a strict umask would also break. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
394db326f2 |
v1.71.0.0 feat: token-load reduction — preamble runtime scripts, gated onboarding, 20 skill carves, CLAUDE.md trim (#2691)
* feat(gen): strip gen-time-only frontmatter keys from Claude renders
interactive + benefits-from are read from the .tmpl by buildContext at
generation time; no runtime, host, or test reader consumes them from the
generated SKILL.md (e2e-harness-audit reads .tmpl; benefits-from tests
assert rendered prose). gbrain: stays (bin/gstack-brain-context-load reads
it from the installed render); hooks: stays (Claude Code host wires
PreToolUse from it).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore(gen): regenerate SKILL.md — dead frontmatter keys removed
Mechanical regen after hosts/claude.ts stripFields change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(test): context-budget ratchet — CI ceilings on always-on + eager token ledgers
New free test grades the two ledgers nothing else guards: the full-frontmatter
always-on catalog (aggregate) and per-skill eager tokens (SKILL.md +
forced-read refs), via checkBudget from lib/context-bill.ts. Ceilings live in
test/fixtures/context-budget.json with x1.05/x1.10 headroom; regenerate with
bun test/helpers/capture-context-budget.ts. New skills fail until consciously
budgeted; removed skills fail until the fixture is refreshed; reductions
ratchet the ceilings down so wins lock in.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(todos): file output-template carve wave + plan-ceo doctrine revisit; mark preamble-carve P3 in flight
Two follow-ups deferred from the approved token-reduction program (CEO review
'NOT in scope' list), filed with full context per TODOS format. The existing
P3 preamble-carve entry gets a status update pointing at the program that
supersedes it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(test): review findings — Windows path normalization, full totals rebuild, ratchet coverage
Pre-landing review (5 specialists) found one critical: the ratchet test runs
in the curated Windows lane, where path.relative yields backslash skill names
that miss the test/ filter and mismatch every POSIX fixture key. Names are now
normalized once in buildRatchetBill (toPosixName) and the fixture filter is
tightened to test/fixtures/. All eight Bill.totals fields are rebuilt from the
filtered list (no fixture-polluted perInvocation/totalMd numbers for future
consumers). New coverage: Windows-separator normalization pins, a
captureContextBudget round-trip against tree-a (headroom math exact), a
stripFields regression pin (interactive/benefits-from absent from renders,
hooks/gbrain preserved), and the ceilings test no longer double-reports
stale-fixture entries.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(test): adversarial findings — stable root key, symlink-alias dedupe, fixture-shape guard
Adversarial review (Claude subagent) verified the fixture's root-skill key was
the capture machine's checkout dirname: any non-gstack-named clone (every
Conductor worktree) failed the free suite, and the documented re-run-the-capture
recovery baked the local dirname into the committed fixture — silent corruption
through the tool's own protocol. The root skill is now pinned to ROOT_SKILL_KEY
('gstack', its frontmatter name). Symlink aliases are realpath-deduped (census
precedent): connect-chrome no longer gets its own ceiling, so Windows checkouts
that materialize the symlink as a plain file can't fail the stale-ceiling
set-equality test. New guards: fixture-shape validation (a string alwaysOnTotal
can no longer silently disable the ceiling), a mutation pin that the filter
shrinks the always-on ledger vs the raw bill, an alwaysOnTotal violation test
(the branch was load-bearing with only under-budget coverage), and an atomic
temp+rename fixture write. Fixture regenerated: 59 ceilings, alwaysOnTotal 6344.
Deferred with a TODO: anchoring transformFrontmatter's denylist strip to the
frontmatter block (latent, zero live collisions, pre-existing path).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: bump version and changelog (v1.69.1.0)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: update project documentation for v1.69.1.0
CLAUDE.md: Token ceiling section documents the context-budget ratchet as
the third guard (test file, fixture, new-skill budgeting, capture command).
CONTRIBUTING.md: Tier 1 guard list gains a Context-budget ratchet bullet;
the Adding-a-new-skill checklist gains the budget-capture step.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: pin exact guard semantics for the context-budget ratchet in CLAUDE.md
Doc-review finding: "a third enforced ceiling" undercounted the guard
family (skill-size-budget floors and parity ratios also watch these
ledgers, relatively). Rephrased to match the ratchet test's own header:
absolute ceilings vs relative floors/ratios.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(changelog): heaviest-skill claim matches the fixture (land-and-deploy edges review by 0.2%)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(bin): gstack-skill-start + gstack-skill-end — the preamble runtime, consolidated
Absorbs the ~13KB of bash every tier-2+ SKILL.md inlined twice over (bootstrap
fence + artifacts-sync fence) and the skill-end telemetry/sync fences. Same
KEY: value STATUS-line contract the prose interprets, plus SKILL_START_PROTO
handshake (OV5), SESSION_ID/TEL_START echoes, GSTACK_HOME-normalized state
paths (EOV7), --parent-pid session identity (EOV5: $PPID inside the script is
the ephemeral tool-call shell), OV4 sanitization of passthrough output, and a
receipted daily artifacts pull (_receipted_git, brain-sync class, fail-closed).
Per-line || true error style throughout (F3) — a mid-script failure never drops
later STATUS lines.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(gen): preamble resolvers emit a script invocation fence instead of inline bash
generate-preamble-bash: ~6.3KB fence -> 4-line gstack-skill-start invocation
(quoted-tilde pitfall handled: leading ~ interpolates through $HOME; env-var
hosts keep $GSTACK_BIN) + degraded-mode prose (F1/EOV8: safe defaults, consent
gates deferred-never-lost; OV5: proto rule). generate-brain-sync-block: ~6.8KB
bash -> interpretation prose + the privacy stop-gate (stays inline until
Phase 2's gated emission). generate-completion-status: telemetry fence -> one
gstack-skill-end call with SESSION_ID/TEL_START handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore(gen): regenerate all skills + golden fixtures — inline preamble bash removed
Mechanical regen after the resolver change: −12,628 lines across 52 renders
(corpus 952K -> 806K render tokens; tier-2 skills −11-13KB each). Golden
per-host ship fixtures refreshed from the fresh claude/codex/factory renders.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: skill-start contract suite + preamble A/B eval + touchfiles registration
test/gstack-skill-start.test.ts (11 free tests): STATUS-key contract vs the
prose (F2), per-host fence resolution shapes (E1), proto-first, OV4 marker
sanitization, --parent-pid identity, headless suppression, skill-end duration
math + pending cleanup. test/skill-e2e-preamble-script-ab.test.ts (gate tier,
OV7): inline-bash render (pinned from
|
||
|
|
a3749bfa4b |
v1.70.1.0 fix: ship names the /document-release subagent at every decision point (tripwire + gate E2E) (#2700)
* fix(ship): name the /document-release subagent at every Step 18 decision point The v1.54.0.0 carve moved Step 18 (documentation sync) into ship/sections/pr-body.md and the Claude-host skeleton stopped saying "document-release" anywhere in the workflow body — the dispatch became invisible at exactly the moments an agent decides whether to open the section. Restore visibility at three touchpoints, all subagent-framed (never bare-slash-framed, which would invite an inline Skill invocation that bypasses the fresh-context subagent + JSON contract): - manifest trigger (renders into the section-index row AND the STOP pointer): "dispatching the /document-release subagent to sync docs (Step 18) and then creating or updating the PR/MR (Step 19)" - Step 17 handoff line names Step 18's dispatch explicitly - new hoisted doc-sync invariant beside the PR-title invariant: the dispatch itself is never skipped; only a failed subagent is non-blocking Pin it in carve-guards: 'the /document-release subagent' (all three touchpoints) + 'dispatches the /document-release subagent' (invariant) must stay in the skeleton; the carved imperative 'Dispatch /document-release as a subagent' must stay carved. Skeleton cap 91,600 → 92,300 (measured 91,764; trigger renders twice). Goldens regenerated for all three hosts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: pin the ship→document-release Step 18 wiring with a free tripwire Five substring/structure asserts across the carved section, the Claude skeleton's three touchpoints, the manifest trigger, and the codex/factory goldens (inlined Step 18 ordered before Step 19). Claude-golden asserts deliberately omitted: host-config.test.ts already enforces golden == generated byte-for-byte. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: gate-tier E2E proving /ship dispatches the document-release subagent New skill-e2e-ship-docsync: a live agent gets the sliced Step 17→19 tail of the generated ship skeleton in a bare-remote git fixture (Steps 0-16 "done"), under a fake HOME so the STOP pointer and the Step 18 subagent prompt resolve to planted copies, with a stub document-release skill that returns the empty-result JSON contract. Hard assert: an Agent/Task tool-call matching /document-release/i exists in result.toolCalls and precedes any `gh pr create`. Neutral prompt (no STOP-Read priming, no document-release mention — the prompt echoes into the transcript, so asserts read toolCalls only). Hardening from review: throw-on-marker-drift fixture slice; per-test GSTACK_HOME + .redact-prepush-prompted marker (routes Step 17's credential guard to its silent branch — the hermetic GSTACK_HOME pin defeats a HOME-only override); 480s/540s timeouts (nested subagent adds wall clock the 300s sibling never carried); 'timeout' accepted in exitReason only because the dispatch assert is independently hard; whole-file describeE2ETier('gate') composed with diff selection (keeps the file out of the periodic shard census, which sits at its ceiling, and under the hard tier-alignment invariant). Registered as 'ship-docsync' in E2E_TOUCHFILES + E2E_TIERS (gate) in the same commit — touchfiles.test.ts rejects either half landing first. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: fix stale document-release TODOS entry + three review-deferred items The SHIPPED entry still described the deleted Step 8.5 post-PR cat-delegation design from v0.8.4; replace with the current Step 18 subagent design and its test pins. Add the three P3 items deferred from the v1.69 plan review: dispatch receipt enforcement, land-and-deploy→canary dispatch-pin pattern, and the periodic shard-census boundary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: pre-landing review fixes Testing-specialist findings, all mechanical: (1) pin the E2E fixture's git branch (-b main / init.defaultBranch=main) and assert every setup command's exit status so operator git config can't silently corrupt a paid run; (2) tighten the dispatch matcher to Step 18-prompt-specific markers (document-release/SKILL.md | executing the /document-release workflow) so a subagent merely quoting section text can't false-pass the regression assert (verified against recorded burn-in transcripts); (3) replace the subsumed carve-guards anchor with three non-overlapping per-touchpoint anchors (gerund/imperative/3rd-person) so each touchpoint is independently enforced. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: red-team review fixes Five informational findings: TODOS shard-census arithmetic corrected (census is 67 with one free ungated slot; the SECOND ungated file trips the floor) and version pointer fixed (v0.18.2.0, not v0.18.1.0); the free tripwire now pins the two dispatch-matcher marker strings so a pr-body prompt reword fails the free suite instead of surfacing as a paid-tier mystery; the E2E matcher gains a section-paste exclusion (scaffold strings disqualify) — verified against all recorded runs; the E2E header documents the tierless test:evals invisibility tradeoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: adversarial review fixes Pin the E2E matcher's two EXCLUSION markers in the free tripwire (an unpinned 'Parent processing:' reword would silently deaden the section-paste guard while every test stayed green); add an ordering pin (the hoisted doc-sync invariant must sit above the pr-body STOP pointer — presence-only anchors can't catch drift below it); plant a third cwd-relative pr-body copy inside the fixture repo, gitignored so the agent never tries to commit test scaffolding. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: bump version and changelog (v1.70.1.0) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: CHANGELOG accuracy fixes from the doc-release review Three factual corrections the Step 18 doc subagent caught in the fresh v1.70.1.0 entry: 5 tripwire tests (not 6), cost floor $0.63 per the cited eval store (not $0.59), and the visibility claim scoped to decision points (the re-run checklist mention survived the carve). Plus the E2E header's stale pending-burn-in note replaced with the observed numbers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: raise bun-polyfill subprocess budget to 60s for degraded Windows runners The 50ms-sleep test blew the 20s budget on BOTH bun retry attempts on PR #2700's windows-latest runner (run 32989821401) — sustained AV/runner pressure, not just the documented cold-start. Same flake passed-on-rerun on the prompt-token-load-reduction branch yesterday. Budget only; every assertion still checks exact output. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ci): run the ship-docsync gate E2E in the evals matrix + silent-skip tripwire The evals.yml matrix is hand-enumerated and the Run step never exported EVALS_TIER, so the new whole-file-gated ship-docsync E2E would have self-skipped even with a row — a hollow green one layer deeper than the documented rehomed-monolith incident. Add the e2e-ship-docsync row with a row-level `tier: gate` property, exported as EVALS_TIER by the Run step (empty = unset for every existing row: all readers are `=== '<tier>'` or truthiness). New free tripwire test/evals-workflow-matrix.test.ts ratchets the class: matrix files must exist; gate-hosting files must have a row; whole-file-gated matrix files must carry a matching row tier; and the burn-down lists enforce their own cleanup. It enumerates the PRE-EXISTING holes found while wiring this (8 gate-hosting files with no row; codex/gemini rows running zero tests; the pty-plan-smoke row hollow since its files adopted describeE2ETier) — tracked in TODOS as the CI gate-lane hollow-coverage burn-down. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ad8400543c |
v1.69.0.0 fix: the silent-failure wave — 6 fixes, 5 community PRs absorbed, tracker closed with receipts (#2666)
* test(wireup): make gbrain-missing PATH fixture hermetic The gbrain-missing test appended the host PATH (and a hardcoded /opt/homebrew/bin) to the fixture PATH, so on any machine with a real gbrain installed the 'missing' case saw it, exited 0 instead of 2, and could never fail where the bug exists — a false green for a whole machine class. The fixture now keeps only root-owned OS dirs on the child PATH, and a new determinism check plants a host-like gbrain to prove it is unreachable. Absorbed from PR #2615 with authorship preserved; the PR-thread liveness screenshot (docs/images/gstack-pr-liveness-2255.png) is dropped — referenced by nothing in the tree. Fixes #2255 Co-authored-by: CommandCodeBot <noreply@commandcode.ai> * fix(evidence): stop bun's dotenv autoload from reaching the spawned command `bin/gstack-evidence` has a `#!/usr/bin/env bun` shebang, and bun AUTO-LOADS `.env`, `.env.<NODE_ENV>` and `.env.local` from the cwd into `process.env`. The wrapper then spawned the command with no `env` override, so every command run through it inherited those variables — and a repo `.env.local` routinely holds production credentials. Two things go wrong, and the second is worse than the leak: 1. Secrets reach a child that would not otherwise have them. `npm test` run by hand in the same shell sees none of them; the same command through the wrapper sees all of them. 2. THE COMMAND UNDER TEST BEHAVES DIFFERENTLY, so the ledger certifies a run that is not the run CI performs. Observed in a Next.js repo on 2026-08-20: four tests failed 4/4 through the wrapper and passed 5/5 without it, because app code branched on env vars only the wrapper supplied. Nearly an hour went into chasing a "flake" that was the measuring instrument. The wrapper exists to record trustworthy evidence, so silently altering the environment defeats its purpose. The fix builds the child env from `process.env` minus the keys bun injected, and detection is exact rather than heuristic: verified on bun 1.3.11, a dotenv file does NOT override a variable the shell already exported (the shell's value wins). So a key whose live value equals the dotenv file's value was injected by bun, and dropping it restores the environment the user's own shell would have given the command. A key whose live value differs is genuinely the caller's and survives. `BUN_DOTENV_FILES()` mirrors bun's precedence, including that `.env.local` is skipped when NODE_ENV is "test" — scrubbing a key bun never loaded would strip a variable the caller legitimately provided. Escape hatch: GSTACK_EVIDENCE_KEEP_DOTENV=1 keeps the old behaviour. When keys are scrubbed the wrapper warns with the KEY NAMES ONLY, so the diagnostic cannot become the leak it prevents. Tests: 6 cases, mutation-verified — removing `env: spawnEnv` reddens exactly the two leak tests and restoring it gives 30/30. Every leak test asserts the scrub warning fired, because `bun test` sets NODE_ENV=test and the first version of these tests passed vacuously against a `.env.local` bun had never loaded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Absorbed from PR #2652 with authorship preserved. Wave additions: a doc-comment on the ${VAR}-expansion limitation (bun expands refs, the reader compares raw text — those keys are left in the child env, failing open) and a regression pin for the unreadable-.env fail-open path with a functional DAC-override skip guard. Fixes #2624 * fix(setup): reap dangling skill dirs when the payload is gone cleanup_old_claude_symlinks derived its work list from the payload directory, so when the payload was gone — precisely when orphans exist — the glob matched nothing and the loop never ran; the -f guard also followed symlinks, hiding dangling SKILL.md links even with a payload present. The cleanup now scans the DESTINATION skills dir (-e/-L, so dangling symlinks are visible) and anchors SKILL.md provenance to path segments (gstack/*, */gstack/*, */.gstack/render/claude/*) instead of a bare *gstack* substring that would eat a user skill under ~/tools/gstack-fork/. The Windows real-file arm stays payload-gated: a real file has no provable owner. Absorbed from PR #2634 (2 commits squashed) with authorship preserved. The symmetric cleanup_prefixed_claude_symlinks hole is filed as a TODOS.md residual in this wave. Fixes #2204 * fix(redact): tolerate EEXIST from recursive mkdir in install-prepush-hook on bun/Windows (#2635) fs.mkdirSync(dir, { recursive: true }) is a no-op on an existing directory in Node, but bun on Windows throws EEXIST - crashing hook install on any repo whose .git/hooks already existed, leaving the repo unprotected. Add lib/fs-utils.ts mkdirpSync: swallow EEXIST only when statSync confirms the path is an existing directory; a regular file occupying the path, a stat failure, or any other errno still rethrows. Use it in installPrepushHook(). The regression test emulates the Windows bun fs semantics via a bun --preload fixture, so the exact crash path runs (and fails on the old code) on any platform, including CI Linux. Absorbed from PR #2641 with authorship preserved. Fixes #2635 * fix(bin): route remaining Windows-reachable mkdirSync sites through mkdirpSync Sweep follow-up to #2641's lib/fs-utils.ts helper: bun on Windows throws EEXIST from a recursive mkdir on an existing dir, so every unguarded recursive mkdirSync on a Windows-reachable path is a latent crash. Converted: bin/gstack-decision-log (unguarded, runs on every decision log — the second call on any machine hits the pre-existing projects dir), bin/gstack-evidence logsDir + ledger dir sites, and bin/gstack-redact-prepush's skip-log site (already try-wrapped, so its failure mode was a silent skip-log loss rather than a crash — the fix makes the log survive). The ~15 remaining gbrain/mac-lane sites are deliberately left alone. Regression: fs-utils.test.ts drives gstack-decision-log twice, the second run under the bun-Windows EEXIST preload fixture — the pre-sweep code exits 1 with EEXIST there; verified red against v1.68.3.0. * fix(setup-gbrain): warn about the ZeroEntropy sunset before Sept 4 ZeroEntropy was acquired by Notion and sunsets its hosted API on September 4, 2026. A gbrain configured with the zeroentropyai embedding recipe keeps importing pages after that date but embedding silently fails — pages land structurally with no semantic search, this repo's tracker P1 (TODOS.md NEXT PRIORITY). Nothing in gstack ever recommended ZeroEntropy (the dependency is gbrain-internal), so the gstack side is detection + advisory: the wireup helper warns when ~/.gbrain/config.json names the recipe (fail-open grep — a missing, unreadable, or other-provider config stays silent and never blocks a working setup), the setup-gbrain provider-default comments say never to select the legacy recipe for a new brain, and USING_GBRAIN_WITH_GSTACK.md gains a troubleshooting entry. The gbrain-side provider migration stays open upstream. Refs #2365 * fix(gbrain-source-wireup): first sync targets the registered source, not --repo The wireup registered a federated source by id, then ran 'gbrain sync --repo $WORKTREE' — which resolves against the brain's DEFAULT source and (on gbrain 0.46.x) rewrites that source's local_path anchor to our worktree. Net effect: the user's primary knowledge source silently repointed at the gstack brain worktree while the just-registered source got zero pages, and pages_synced still reported success. The sync now targets the registered id ('gbrain sync --source $id', the same form the repo's own troubleshooting documents). Because the script's stated floor is gbrain >= 0.18.0 and nothing proves --source exists there, support is probed via 'gbrain sync --help' first: an older gbrain keeps the wrong-but-working --repo call with an upgrade warning instead of converting it into a hard failure. The probe sits after the GSTACK_BRAIN_NO_SYNC early-exit and is unreachable in --probe mode. Regression tests (fail on v1.68.3.0): a no-skip sync case asserting the call log shows 'sync --source gstack-brain-<id>' and never 'sync --repo', and an old-gbrain fallback case (fake sync --help without --source) asserting --repo plus the upgrade warning. Fixes #2662 * fix(setup): --host slate exits informatively instead of silently installing nothing slate passed --host validation (added to the accept-list in v1.64.1.0) but never got a dispatch arm, and the all-INSTALL_*-zero fallback lives inside the auto branch — so './setup --host slate' configured nothing and exited 0, a silent no-op strictly worse than the original hard rejection. slate is now an informational arm (per docs/designs/SLATE_HOST.md it is blocked on the host-config refactor; Slate reads .claude/skills as a compatibility fallback, so the arm points at './setup --host claude'), and a defensive guard after the dispatch chain errors loudly (naming the host, the missing arm, and the valid targets, exit 1) if a future host is ever accepted without being wired. Regression tests (fail on v1.68.3.0): a dispatch-arm ratchet asserting every accept-listed install target has a matching dispatch branch — the exact drift class; a registry cross-check deriving both sides from hosts/index.ts and setup's case arms; a behavioral slate probe (exit 0, points at --host claude, never reaches the installer — on unfixed code it fell through into the installer); and a static pin on the guard's shape. Fixes #2361 * fix(make-pdf): resolve the sibling browse binary from execPath, not argv[0] In a bun-compiled binary process.argv[0] is the raw invocation string — often relative ('./pdf', 'pdf') — so dirname(argv[0]) yielded '.' and the sibling candidates (../browse/dist/browse etc.) resolved against the CWD instead of the install dir. Resolution was cwd-dependent: correct-by-luck when the fallbacks rescued it, wrong when a cwd-relative path matched. process.execPath is always the absolute binary path. The resolution step takes an injectable selfPath (defaulted) because under bun test the process path is the bun runtime and the compiled-binary shapes are otherwise unreachable. The issue's other half — pdf setup failing on newtab('about:blank') — was already fixed on main in v1.64.0.0 (browse/src/url-validation.ts exact-match allows about:blank; its comment names this exact smoke). This commit closes what remains. Regression tests (the sibling-via-selfPath case fails on v1.68.3.0 — pre-fix code ignores the seam and either resolves the global install or throws): sibling resolution from an install-shaped tree, and a decoy-browse-DIRECTORY case pinning that a directory never wins resolution. Fixes #2156 * fix(memory-ingest): store the normalized git_remote so unattributed pages hit the policy filter buildTranscriptPage wrote the normalized '_unattributed' sentinel into the page FRONTMATTER but stored the raw resolved remote ('' when unresolvable) on the page object. The policy filter fast-paths !p.git_remote, so under --include-unattributed an explicit '_unattributed → deny' (or read-only) policy never applied to exactly the pages it names — they ingested unpoliced. The stored value now matches the frontmatter. Regression test (fails on v1.68.3.0): seeds the REAL bin/gstack-gbrain-repo-policy store with '_unattributed → deny' through its own set verb, ingests an unresolvable-remote session with --include-unattributed, and asserts nothing reaches gbrain — pre-fix the '' remote bypassed the filter and the import ran. A fake echoing tiers would pass on both sides of the fix; the real helper prints 'none' for unknown keys, so only a genuinely applied deny distinguishes the two. Fixes #2353 * fix(land-and-deploy): MERGED recovery reconciles and reports remote-branch cleanup Step 4's merge commands carry --delete-branch, and the success path tells the user 'The branch has been cleaned up.' When gh exits non-zero AFTER GitHub already merged (routine in worktree layouts: gh's local cleanup runs git checkout <base> and fails), the §4a-postfail MERGED recovery re-established everything EXCEPT the branch deletion — and said nothing about it, so the discrepancy was invisible. The MERGED path now reconciles: git ls-remote --heads distinguishes branch-already-gone (exit 0, empty → 'already cleaned up', idempotent on re-runs) from branch-survived (offer confirm-first deletion, matching the section's worktree posture; -d not -D for any local branch) from check-itself-failed (non-zero exit → 'couldn't verify', skip the offer — never read a failed check as a clean branch). Template + regenerated SKILL.md + test extensions land in one commit (the md-sync assertion goes red otherwise). Regression assertions (fail on v1.68.3.0: no delete-branch reconciliation existed in test/ at all) pin the ls-remote check, the confirm-first delete, and the absent-vs-failed distinction. Fixes #2656 * fix(scripts): stop heredoc bodies deadlocking under Homebrew bash `./setup --help` can hang forever on macOS, printing nothing, with no way to tell it apart from a slow install. Eleven scripts carry the same latent hang, `setup` itself being the one every user hits first. bash 5.2+ delivers a heredoc body of 64KiB or less through a pipe: the forked child writes the entire body before exec, and nothing reads the other end until the command starts. Under macOS pipe-KVA pressure the kernel hands a fresh pipe a 512-byte buffer instead of the usual 16-64KiB, so any body of 512 bytes or more blocks write() permanently. The capacity check bash would need to notice (F_GETPIPE_SZ) is Linux-only, so it never fires here. It is pressure-dependent, which is why it reads as "worked on my machine" — the same script runs fine all day and then wedges. Homebrew bash is what `#!/usr/bin/env bash` resolves to on a Mac with brew on PATH, which is most of them. Apple's /bin/bash 3.2 predates the pipe path and is unaffected, so the bug is invisible to anyone testing with the system shell. The fix is `BASH_COMPAT=50` in each affected script, which restores the pre-5.2 tempfile path: $ bash -c 'probe() { [ -p /dev/stdin ] && echo PIPE || echo TEMPFILE; } probe <<EOF $(printf "x%.0s" $(seq 1 1000)) EOF' PIPE $ BASH_COMPAT=50 bash -c '...same...' TEMPFILE - Not a `#!/bin/bash` shebang swap: that pins the script to whatever bash lives at /bin (3.2 on macOS, absent on some Linux distributions) and is bypassed entirely by `bash script.sh` call sites. The variable survives both. - Not exported, so child processes keep their own compat level. - Placed below any `--help` sed range that reads $0, so usage output is unchanged (verified on all eleven). - Every guarded script is bash-3.2-clean — no associative arrays, case conversion, or mapfile — so compat level 50 costs them nothing. test/heredoc-pipe-deadlock.test.ts scans every tracked shell script for a heredoc body in the 512B-64KiB window and fails without the guard, and proves the mechanism at runtime on bash 5.2+ by asserting the body moves from PIPE to TEMPFILE. On older bash the runtime half is skipped, since the pipe path does not exist there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Absorbed from PR #2640 with authorship preserved. Wave adaptations: the pipe-probe test skips on minimal-/dev environments without /dev/stdin (it would report OTHER for an unobservable fd), and one caveat verified during review: on bash 4.3/4.4 (e.g. Git Bash), assigning BASH_COMPAT=50 prints a non-fatal 'invalid value' warning to stderr — those bashes are already on tempfiles, so the guard is a no-op there; windows-setup-e2e exercises this empirically. * docs: TODOS.md v1.69 wave close-out Move the slate P4 entry and the ZeroEntropy P1's gstack-side half to Completed (v1.69.0.0); reframe the ZeroEntropy NEXT PRIORITY entry around the remaining gbrain-side work; file the wave's four residuals with rationale — the prefixed-cleanup symmetric conversion, the #2163 legacy-slug checkpoint heal, the invited #2657 --reconcile contribution, and the table-driven setup host dispatch behind the new cross-check ratchet. * chore: bump version and changelog (v1.69.0.0) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Som Samantray <som.samantray@gmail.com> Co-authored-by: CommandCodeBot <noreply@commandcode.ai> Co-authored-by: Connex Client Access <paul@paulkortman.com> Co-authored-by: y$un_ <forrest.sun527@gmail.com> Co-authored-by: Lockyer <135391289+Lockyer228@users.noreply.github.com> Co-authored-by: Benjamin D. Smith <benjamin.smith@binarysword.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
85fd9db554 |
v1.68.3.0 fix(pairing): re-pair to narrow revokes the old grant on the spot (#2665)
* fix(pairing): reject reserved clientId 'root' at all token writers 'root' is the sentinel checkScope/checkDomain/checkRate and the server command gate use for the omnipotent caller, so a scoped token carrying it bypasses every enforcement path. Add ReservedClientIdError + a shared assertValidClientId; createToken/createSetupKey throw, restoreRegistry skips-and-logs (a corrupt state file must not brick boot). /pair and /token surface it as a named 400, and the CLI fast-fails --client root. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(pairing): release tab ownership on revoke tabOwnership cleared only on tab close, so after DELETE /token a same-name re-pair inherited the revoked agent's authenticated tabs (own-only access keys on owner === clientId). Add BrowserManager.releaseClientTabs and run it unconditionally in DELETE /token (ownership outlives the token, so an expired-token client can still own tabs); 404 only when both nothing was revoked and nothing released. Response now carries tabs_released. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * v1.68.3.0 fix(pairing): re-pair to narrow revokes the old grant on the spot POST /pair minted a new setup key but never touched the agent's live session, so re-pairing --client X --restrict read while X was connected (or whose 5-min key expired unexchanged) left the original full-access session, eval included, alive up to 24h. A reducing re-pair (fewer scopes, tighter domains, lower rate, stricter tab policy) now revokes the live session and releases its tabs before minting the new key (grantReducesAccess + revokeClientFully; superseded in the response). Non-reducing re-pairs keep the session and only drop stale PENDING setup keys, so a broaden/refresh never strands a working agent and a narrowing re-pair issued before the agent connects can't leave the old broad key exchangeable. Revoke happens before mint (revokeToken deletes all of a client's tokens). CLI prints a version-skew-safe supersede notice and warns when a re-pair-shaped call omits --client. Docs + CHANGELOG + VERSION. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(pairing): harden re-pair per adversarial review Adversarial review of the diff found four issues, now fixed: - Validate the requested grant BEFORE the supersede revoke: a reducing re-pair with a bad scope/rate no longer destroys the live session and then fails to mint a replacement (assertValidTokenOptions runs up front). - A re-pair with no live session releases tabs orphaned by an expired incarnation, closing the tab-inheritance gap /pair had (DELETE /token already released unconditionally). - Test the DELETE /token revoked=0/tabs>0 path and the /pair orphaned-tab release at the handler level (HTTP e2e can't, headless owns no tabs). - Test the CLI --client root fast-fail; fix its null-guard (parseFlag returns null when --client is absent). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Garry Tan <garry@ycombinator.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
51932eceef |
v1.68.2.0 fix: tunnel revoke exists and revokes everything — setup keys included, verified live (#2646)
* fix(browse): revokeToken deletes ALL tokens for a clientId, not the first Map hit
revokeToken deleted the first Map entry matching the clientId and returned
true. After a normal pairing, two entries share one clientId: the spent setup
key (kept by exchangeSetupKey for idempotent re-exchange) and the session
token, in that insertion order. Revoke ate the setup key, reported success,
and the live session survived: DELETE /token/<id> returned a false 200 while
/agents kept listing the agent. Worse, an unspent setup key created after the
session survived revoke, so a "revoked" agent could POST /connect and mint a
fresh session within the key's 5-minute validity window.
revokeToken now deletes every matching entry and returns the delete count
(truthy-compatible with the old boolean). The DELETE /token handler logs
"Revoked N token(s)" and returns tokens_deleted so the multi-token class
stays visible; revokeSkillToken wraps Boolean() to keep its documented
contract. Regression tests pin shapes a (spent-key shadowing), b (re-grant
hole), c (multiple pending keys), and bystander isolation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(browse): tunnel revoke/agents CLI with post-revoke verification
`$B tunnel revoke <name>` was documented in the instruction block,
pair-agent/SKILL.md, and REMOTE_BROWSER_ACCESS.md but implemented nowhere:
the CLI forwarded it to the daemon as Unknown command 'tunnel', and nothing
in the repo called DELETE /token/:clientId or GET /agents.
New pre-server short-circuit (#2254 pattern: tokens are memory-only, never
boot a daemon to revoke against it). `tunnel revoke <name>` DELETEs the
token, prints the deleted count ("(count unknown)" for old daemons that
answer {revoked} without tokens_deleted), then RE-READS GET /agents to prove
the agent is gone. The still-listed branch is the version-skew net: a new
CLI against a still-running old daemon with the first-match revoke bug exits
1 and says to re-run (each old-daemon call deletes the next match) or stop.
An alive pid with an unreachable port reports "Could not reach daemon"
(exit 1), never a false "no daemon". `tunnel agents` lists sessions plus
pending (unexchanged) setup keys, which GET /agents now exposes via
listTokens({includeSetup}) — without them the revocation view was blind to
a paired-but-never-connected agent. Setup-key tokens never leave the server.
DELETE /token/ now decodeURIComponents the clientId (400 on malformed
encoding) so CLI-encoded names round-trip.
Tests: subprocess CLI coverage (usage paths, no-daemon exit 0 without
spawning, live pair/connect/revoke loop, pending-key listing), stub-daemon
pins for the skew and unreachable branches, and e2e pins for revoke-all
semantics, percent-encoded ids, and the second-DELETE-is-404 regression.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(browse): CLI always sends explicit pair scopes via shared DEFAULT_PAIR_SCOPES
The effective pairing default lived in two places: the CLI omitted scopes
unless --restrict was passed, and the server filled in its own literal.
handlePairAgent now always sends an explicit scopes list and both sides
reference one exported constant, DEFAULT_PAIR_SCOPES, so the default cannot
silently drift again (pinned by a server-auth source tripwire).
Three input traps closed in the same surface:
- Bare --restrict (or --restrict swallowing the next flag) parsed as "no
restriction" and silently granted FULL access, the opposite of the user's
intent. validatePairAgentFlags rejects it pre-server, before any consent
gate, so an arg error never boots a daemon.
- A scopes list could smuggle the control scope past the explicit flag:
--restrict "read,control" minted a control-scoped session with no
--control. /pair now 400s on control in a scopes list without the control
flag, and the CLI points the user at --control.
- Option typos validated only at exchange time: createSetupKey stored any
scope string and any rateLimit, so /pair returned 200 with a poisoned
setup key whose failure surfaced to the REMOTE agent at /connect as a
misleading "Invalid request body". Shared validation now runs in both
creators and throws typed InvalidScopeError; /pair and /token 400 with the
message, naming the bad scope or negative rateLimit. Also
`opts.rateLimit || 10` became `?? 10` so the documented "0 = unlimited"
survives the /pair path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(browse): 403 hint stops recommending --admin; invariant names both scope defaults
The scope-denied hint told restricted agents to "re-pair with --admin for
eval/cookies/storage" — but --admin is a legacy alias for --control, so
following it over-granted browser-wide destructive commands on top of the
admin scope the default already carries. The hint now matches the CLI's
sibling wording: re-pair without --restrict for page access, --control for
browser control.
Registry invariant #2 claimed "admin scope denied by default" three releases
after
|