Repository navigation
v1.63.1.0 fix(ship): allocate from git when the PR queue is unreachable - #2545
Closed
CarringtonCreative wants to merge 3 commits into
Closed
CarringtonCreative wants to merge 3 commits into
CarringtonCreative wants to merge 3 commits into
Conversation
gstack-next-version set offline:true whenever `gh pr list` failed and returned an EMPTY claim set, and /ship's instruction was "fall back to local BUMP_LEVEL arithmetic". Local arithmetic cannot see a sibling's claim, so the fallback allocated a version another open PR already held. Reproduced against a fixture (base 0.1.66.0, sibling branch holding 0.1.67.0, gh failing): the old allocator returns 0.1.67.0 and collides; the new one returns 0.1.68.0. Git already knows what the API was being asked for, so offline now degrades the queue VIEW, not the ALLOCATION. Two sources, neither needing a token or network: - every remote-tracking branch's VERSION file — those ARE the open PRs' branches - versions already shipped in the base's commit subjects — catches a number that merged and was then re-picked, which the VERSION file cannot see because it only holds the newest value. Bounded at 400 commits, and it warns when it truncates instead of implying it read all history. New output field `fallback: "git" | null` so /ship can tell a git-derived pick from a host-derived one. Additive: the fallback only runs when the host query already returned nothing, so the online path is unchanged. Found while auditing a downstream repo carrying four duplicate version pairs in main across three weeks; the most recent was this exact failure.
CHANGELOG.md is synced from origin/main first so the entry lands above 1.62.0.0 rather than conflicting with it — this branch could not merge base directly because another session holds uncommitted edits to ship/SKILL.md and the golden fixtures, which d078622 also touches. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The incident that motivated this fix happened in a PRIVATE repo, and this file lives in a public one. The technical account is what carries the reasoning, not the repo's name — "a downstream repo" says the same thing without publishing it.
|
Merging to
After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here |
4 of 9 tasks
garrytan
added a commit
that referenced
this pull request
Aug 17, 2026
…ls, brain-sync integrity, 31 community PRs credited (#2604) * fix(test): host-config goldens self-provision .agents/.factory artifacts Fixes #2532. The codex/factory golden tests read gitignored artifacts that only gen-skill-docs.test.ts (serial tree-mutating phase) produces, so the file failed in isolation and on clean clones (the #2536 "3 failures then 0" symptom). beforeAll now generates a host's artifacts iff its ship SKILL.md is missing — never overwriting existing ones, so stale artifacts still fail the golden. The file is also classified TREE_MUTATING so its provisioning runs in the serial window, not racing parallel readers. Verified: full pass with .agents/ and .factory/ deleted (74/74 in isolation). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): exempt the live repo tree from hermetic-wiring's operator-~/.claude ban The skill-seeding tripwire asserted every seeded symlink target must NOT start with ~/.claude — but on the default global-git install the repo itself lives at ~/.claude/skills/gstack, so every CORRECT symlink (which must resolve into the live repo tree, as the very next assertion requires) carried the banned prefix. The test could never pass on a default install: pristine v1.64.1.0 (c118e240) fails it in any worktree under ~/.claude/skills/ and passes elsewhere (verified 2026-08-15). Exempt targets that realpath into the resolved repo ROOT before applying the operatorClaude ban — realpath both sides so a symlinked HOME can't dodge the tripwire. Genuine escapes (a target under ~/.claude but outside the repo) still fail with the escape message. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(gen-skill-docs): quote YAML inline scalars containing '...' (Bun strict parser breaks on bare ellipsis) A bare ... inside a plain YAML scalar is a document-end marker that strict YAML parsers (Bun.YAML among them) reject mid-scalar. catalog-trim truncation appends '...' to any description whose lead exceeds 200 chars, so any truncated description would generate a SKILL.md with unparseable frontmatter. Add the ellipsis test to toYamlInlineScalar's needsQuote so such scalars are emitted double-quoted, plus unit coverage for the quoting rules. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(gen-skill-docs): throw when a template contains {{PREAMBLE}} twice Hardens the #2508/#2362 class: a second {{PREAMBLE}} occurrence — even a prose mention, which is exactly how spec/SKILL.md.tmpl re-expanded the full ~12K-token preamble mid-document — now fails generation with the template path instead of silently shipping a doubled preamble. Pure exported guard (assertSinglePreamble) called from resolvePlaceholders, unit-tested with the original prose-mention shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): classify catalog-trim.test.ts as tree-mutating Discovered while landing the duplicate-{{PREAMBLE}} guard: importing scripts/gen-skill-docs.ts executes its top-level body, which regenerates the entire claude host (71 GENERATED files) at import time. catalog-trim.test.ts does that import from a PARALLEL shard — the same read-during-regeneration hazard class as #2532, invisible only because the regen is byte-identical on a fresh tree. Move it to the serial tree-mutating window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): prepush hook test builds PATH with a POSIX-only separator `test/redact-prepush-hook.test.ts` shadows `git` with a stub by prepending a temp dir to PATH, built as `${stubDir}:${process.env.PATH}`. On Windows the separator is `;`, so that produces one unparseable entry, the stub is never found, and the REAL git runs — the diff succeeds, `gitStrict` never throws, and the hook exits 0 where the test expects 1. It fails as a wrong assertion rather than as a portability problem, which is what made it hard to place. Replace it with a `prependPath` helper mirroring the one already in test/gstack-brain-context-load.test.ts, which handles both platform details: `path.delimiter`, and a case-insensitive lookup of the existing env key — Windows commonly spells it `Path`, and adding a second `PATH` alongside an inherited `Path` leaves the winner up to the spawn implementation. On POSIX the helper resolves to `{ PATH: binDir + ":" + process.env.PATH }`, byte-identical to the expression it replaces, so behaviour there is unchanged. Fixing the separator alone does not make the test pass on Windows, and it cannot: the premise is that a signal-killed child yields `spawnSync` status === null, and Windows has no equivalent (a force-killed process reports a non-zero exit code). The stub is also a `#!/bin/sh` file named `git`, which Windows will not execute, since process creation resolves through PATHEXT and ignores the shebang. A Windows variant would assert the non-zero-exit branch instead — a different branch than the test name claims — so the test is gated with test.skipIf(process.platform === "win32"), matching test/session-runner-timeout.test.ts and test/setup-emoji-font.test.ts. Windows before: 14 pass, 1 fail. After: 14 pass, 1 skip, 0 fail (3 consecutive runs). Unchanged on POSIX, where it should still run and pass — worth confirming in CI, since I can only verify the Windows half here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(artifacts): sync the decision store, which no allowlist glob matched gstack-decision-log enqueues projects/<slug>/decisions.jsonl after every write, but none of the 16 managed globs matched it, so compute_paths_to_stage rejected every one at its "must match at least one allowlist glob" check. The writer and the syncer disagreed silently: enabling artifacts sync backed up learnings, plans, designs and timelines -- everything except the durable decision ledger -- and nothing reported a miss, because a dropped path prints exactly what a synced one does when the queue is otherwise empty. Add the three decisions.* globs and class them artifact so they also sync in artifacts-only mode. The test reads the heredocs out of the script rather than executing it: gstack-artifacts-init.test.ts drives the real script through #!/bin/bash shims and a colon-separated PATH, so it cannot run on Windows -- the platform where the companion slug bug bit. * fix(windows): resolve the project slug natively when gstack-slug cannot spawn bin/gstack-slug is a `#!/usr/bin/env bash` script with no file extension. Windows honors neither the shebang nor PATHEXT for an explicit path, so spawnSync fails ENOENT and resolveSlug returned its literal fallback, "unknown". Every decision on the machine was therefore filed under ~/.gstack/projects/unknown/ -- one bucket shared by every project -- while the bash-side Context Recovery preamble resolved the real slug, found no decisions.active.json there, and skipped through a bare `if [ -f ... ]` with no else. Nothing failed. Both decision bins (log and search) missed identically, so writes and searches stayed consistent with each other, and the only component that resolved correctly was silent by design. Measured on one machine: 62 decisions accumulated over 10 days and 170 skill runs, surfaced zero times. shell:true is not the fix here, unlike #1731 -- cmd.exe cannot run a bash script either. Nor is re-spawning through `bash`: on Windows that frequently resolves to WSL, whose $HOME and /mnt/c paths yield a different slug AND a different cache directory, trading one split store for another. Instead, port gstack-slug's own three steps (cache -> git remote -> basename), keeping its alphabet and its MSYS-form cache key so both paths agree. The fallback is win32-gated, so POSIX behaviour is byte-identical. Tests exercise the fallback on every platform (only the gating is win32-specific), so POSIX CI catches a regression that would otherwise surface only on a Windows user's disk, plus a static gate pinning the platform check. * fix(security): guard brain-sync arithmetic against injected .brain-last-pull; sanitize _GBRAIN_HOST Re-derived from PR #2588 under the generated-file screening rule (resolver hunks taken; SKILL.md files regenerated, not accepted). A poisoned .brain-last-pull could reach bash arithmetic ($(( ))) — a code-execution vector from a writable state file; the timestamp is now validated numeric before use. _GBRAIN_HOST from ~/.claude.json is clamped to hostname-safe characters before echo. Ship goldens refreshed to the regenerated output. Co-authored-by: sneakygriff <89592870+sneakygriff@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(sync): run gstack-brain-sync through bash, not cmd.exe, on Windows The brain-sync stage failed on EVERY Windows run with "is not recognized as an internal or external command", so /sync-gbrain always reported ERR brain-sync among otherwise green stages. #1731 gave these spawns shell: NEEDS_SHELL_ON_WINDOWS. That is correct for the gbrain.cmd shim and does nothing here: shell:true routes through cmd.exe, which resolves .cmd/.bat via PATHEXT but has no concept of a shebang, so an extension-less bash script is rejected outright. A .cmd shim needs a shell; a shebang script needs an interpreter. The two cases look identical and are not. The failure was quiet rather than loud. artifacts_sync_mode defaults to pushing curated artifacts to git, so a Windows user's learnings piled up uncommitted in ~/.gstack indefinitely while the sync report showed one red line out of four. New bashScriptInvocation() resolves Git for Windows' bash explicitly and passes the script as argv[0]. It prefers Git bash over a bare `bash` on PATH because WindowsApps ships a bash.exe that is the WSL launcher, which would read C:\... as a Linux path; GSTACK_BASH overrides for unusual installs; forward slashes because bash treats backslashes as escapes; and it returns null when no bash exists so the stage says so plainly instead of surfacing an unactionable spawn error. The #1731 tripwire asserted the shape that does not work, so it now asserts the opposite (never a raw spawnSync(brainSyncPath, ...)) and six unit tests cover the resolver. Verified on Windows: the stage now reports "OK brain-sync curated artifacts pushed (4.2s)" and the artifacts repo committed + pushed on its own. Affected-test set unchanged at 14 pre-existing failures before and after, with 6 new passing tests. * fix(gbrain): quote cmd.exe arguments at a single gbrain invocation seam Fixes #2471. With shell:true on Windows, node/bun join argv into one cmd.exe string without quoting, so a repo path with a space — the default C:\Users\First Last\ layout — split into two arguments and every gbrain call carrying a path silently targeted the wrong location (worst: `sources add --path`). All gbrain CLI invocations now build their (cmd, argv, shell) triple through gbrainInvocation(), which quotes risky arguments for cmd.exe's re-parse (embedded quotes doubled). The four direct spawn sites in lib/gbrain-sources.ts route through the seam; the #1731 static invariant is upgraded for seamed files (any direct "gbrain" opener is the violation) and kept as-is for lib/gbrain-local-status.ts. POSIX behavior unchanged (shell:false, passthrough argv). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(brain-sync): classify queue entries, rewrite surgically, re-push stranded commits Fixes #2549 (P0 data loss). Every drain exit previously truncated the WHOLE queue (six `: > "$QUEUE"` sites), which (a) destroyed privacy/mode-held entries while misattributing them as "no allowlisted changes", (b) destroyed entries enqueued concurrently during the drain, and (c) left push-failed commits stranded locally with nothing ever re-pushing them until unrelated new work arrived. Now: compute_paths_to_stage classifies every entry (stageable / retained privacy-held / dropped skipped-invalid-unmatched-missing); rewrite_queue re-reads the LIVE queue at mv time and removes only this drain's processed paths (retained + concurrent appends + unparseable lines survive; atomic tmp+mv); an unpushed-commit detector at run start re-pushes stranded local commits (receipted fail-closed; a receipt refusal skips the retry rather than wedging the drain; guards missing origin/<branch>; runs inside the existing lock). Status lines carry counts; full drop paths go to a 0600 sidecar (.brain-sync-drops.json) so filenames stay out of transcripts. --drop-queue remains the one intentional truncation. Matrix added: privacy retention, unmatched/missing counted drops + sidecar mode, unparseable-line preservation, surgical same-drain retention, push-fail commit retention + detector re-delivery on an EMPTY queue, receipt-refusal skip. 35/35 in test/brain-sync.test.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(gbrain): make --full do a full code walk, not a delta one `runCodeImport()` walked with a bare `gbrain sync --strategy code --source X`. The strategy is right, but that walk is incremental: it only revisits files changed since the source's checkpoint. A file missed at the ORIGINAL import is therefore never revisited and stays out of the index indefinitely. The reindex-code pass below cannot rescue it. It re-chunks pages that already exist and never walks the filesystem — the same property the comment directly above already relies on when explaining why the walk has to run first. That fix landed one flag short: it made a fresh source get pages at all, but left `--full` unable to discover a file the first walk skipped. Net effect: `/sync-gbrain --full` did not perform a full walk, and re-running it never re-detected the gap. The failure is silent, which is what makes it expensive. Nothing errors, nothing warns, and the verdict block still reports OK while `gbrain search` and `gbrain code-def` answer out of a partial index. It reads as "gbrain is weak at code questions" rather than "the index is incomplete". Measured on two local code sources before and after this change, counting exported functions resolvable via `gbrain code-def`: one went from 61/201 (30%) to 180/201 (89%), importing 79 files that had no page at all; the other had whole source files missing entirely and reached 93%. Both had been serving search from a partial index for weeks. Scoped to `--full` so incremental runs stay fast. `--yes` because this spawns non-interactively and a full walk otherwise prompts to confirm import cost. Anyone can check their own brain without applying this: gbrain sync --source <id> --strategy code --full --dry-run and compare "N file(s) would be imported" against that source's page_count. Worth knowing while doing so: the default strategy is markdown and --strategy is per-invocation, never persisted on the source, so dropping the flag reports strategy=markdown and a handful of files. * fix(brain-cache): honest 'missing' instead of fabricated-empty digests on gbrain failure A gbrain-unreachable failure in fetchRecentDecisions and fetchSalience used to be converted into a cached 'successful' empty digest ("_No prior skill runs recorded._" / "_No salient pages in last 14d._") that refreshEntity stamped with last_refresh. The false negative then survived every subsequent TTL cycle, indistinguishable from a genuine zero-rows result. Now failure returns null, so cmdGet's existing missing/stale-fallback machinery reports the true state — matching what fetchGoals and fetchSimplePage already do on failure. Also adds an Array.isArray guard in fetchRecentDecisions so a malformed payload ({pages: {}} etc.) classifies as failure instead of crashing refreshEntity mid-refresh; a genuinely empty pages array still renders the honest empty digest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): give the schema-mismatch rebuild test a load-proof budget The rebuild path refreshes every per-project entity against the real gbrain CLI; with an unreachable brain each spawn runs to its own timeout, and under machine load the stack exceeds bun's 5s default (observed 5.2-5.4s, identically on pre-#2587 binaries — a load flake, not a regression). 30s budget matches the sibling brain-sync suite's convention. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(memory-ingest): parse the current Codex response_item rollout shape Fixes #2105. Codex rollout JSONL moved to { type: 'response_item', payload: { type: 'message', role, content: [...] } }; the parser's legacy payload.message branch never fired on it, so every Codex session imported as an empty shell (message_count: 0 — 243/243 sessions on the reporting machine). Both shapes now parse; non-message response_items (reasoning etc.) are ignored. parseTranscriptJsonl exported for direct unit tests (CLI path unchanged — import.meta.main guard). Note: #2104's staging-in-gitignored-tree half is already defended on main (--include-gitignored + GIT_CEILING_DIRECTORIES, #2144, plus the #2486 reconcile guard) — verified, no change needed; it moves to the close-only roster. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): refresh codex/factory ship goldens from post-#2588 regeneration The #2588 absorb refreshed all three ship goldens, but `bun run gen:skill-docs` regenerates the CLAUDE host only — the codex/factory goldens were copied from artifacts rendered before the resolver change and failed against a fresh external-host regen in the serial test phase. Re-rendered with --host codex / --host factory and re-copied. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(make-pdf): boolean flags no longer swallow the next positional argument Fixes #2514. The parser treated any non-flag token after a flag as its value, so `$P generate --toc essay.md` ate essay.md as --toc's value and failed with "missing input" — the skill's own documented usage only worked when two boolean flags happened to be adjacent. BOOLEAN_FLAGS enumerates the no-value flags; value flags (--watermark, --to, --title, ...) are unchanged. main() now runs behind import.meta.main so tests import the parser directly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(repo-mode): probe GNU stat before BSD so Git Bash stops crashing Fixes #2195. On GNU coreutils `stat -f` SUCCEEDS (filesystem status, not a format string), so the BSD-first fallback chain never fell over — it fed multi-word filesystem output into the cache-age arithmetic and crashed under set -u on Windows Git Bash. GNU `stat -c` fails cleanly on BSD/macOS, making GNU-first deterministic on both; the mtime is numeric-validated before arithmetic as a last line of defense. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(retro): point the prior-retros context query at files /retro actually writes Fixes #2552's live half. The gbrain context-query glob targeted ~/.gstack/projects/<slug>/retros/*.md — a directory and extension nothing writes — so prior-retro recall was dead on every brain-aware run. /retro saves to .context/retros/*.json (repo-local); the query now reads that. The issue's second defect (quoted-tilde orphan sweep) is already fixed on main — the preamble sweeps with "$HOME/..." — verified, no change needed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(sync-gbrain): remove the capability-check page file left in the user's repo Fixes #2503. On worktree-pinned brains `gbrain put` materializes the checked page as _capability_check_<pid>.md in the current directory (the user's repo), and `gbrain delete` removes the page but not the file — every /sync-gbrain run left a stray file in the repo root. The check now deletes the materialized file explicitly after the page delete. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(browse): warn that hover scrolls and the daemon tab persists across sessions Fixes #2445. Both behaviors are by design but produced confidently wrong verification output: hovering a below-the-fold element scrolls the page before a "rest state" screenshot (exit 0, wrong section), and the daemon's tab survives sessions so a bare `reload` can act on whatever earlier work left open. The screenshot-evidence section now names both traps with the concrete guards (assert window.scrollY; always goto before verifying). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(gitattributes): pin *.txt to LF .gitattributes pins LF for every other text format in the repo (*.md, *.tmpl, *.yml, *.yaml, *.json, *.toml, *.sh, *.ts, extensionless scripts, even the hash-pinned diagram-render dist files). *.txt is the one text format left unpinned. On Windows with core.autocrlf=true, that means the two tracked .txt files are rewritten to CRLF at checkout and then read as permanently modified: gstack/llms.txt +174 bytes make-pdf/test/fixtures/combined-gate.expected.txt +20 bytes git status is never clean, and /gstack-upgrade's 'git stash' step saves a phantom stash on every upgrade — one that pops back to an empty diff. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(setup): install every skill runtime asset for the Claude host On a fresh Claude install, link_claude_skill_dirs installed only SKILL.md (+ sections/) per skill. Every skill that reads a sibling runtime file at .claude/skills/<name>/<file> was broken out of the box: /review stopped at 'Read .claude/skills/review/checklist.md' (file never installed), and qa's templates/references, plan-devex-review's dx-hall-of-fame.md, gstack-upgrade's migrations/, and careful/freeze's bin/ hooks were all silently missing. Codex/Factory/OpenCode/Kiro installers already copied these; the primary host never did. Fix: a shared _link_skill_runtime_assets helper installs EVERYTHING a skill ships next to its SKILL.md, with an explicit exclusion list (F7): node_modules, dist, test, *.tmpl, hidden files. Exclusion-list polarity means a newly added asset installs by default instead of being silently dropped. Assets refresh unconditionally on re-run (rm + relink/copy), so Windows real-dir copies pick up changes after git pull. New free test runs the real installer functions against the live repo into a temp skills dir with a TWO-CLASS referenced-paths assertion (ENG-OV7): alias-relative refs (.claude/skills/<name>/<path>) must exist under the install; repo-anchored refs (~/.claude/skills/gstack/<path>) must exist in the tree modulo an explicit built-artifact allowlist (browse/design/ make-pdf dist + the compiled gstack-global-discover). Known-broken class-2 refs (#2250 bare bin names) are ratcheted: the test fails if they quietly start existing without the entry being removed. Fixes #2317 Fixes #2454 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(setup): alias skills install as rewritten copies, never symlinks The two back-compat alias dirs — _gstack-command (root router) and connect-chrome (→ open-gstack-browser) — symlinked the canonical SKILL.md verbatim, so each alias re-served the canonical frontmatter name:. Claude Code keys skills on that name and requires global uniqueness: the connect-chrome duplicate silently shadowed /open-gstack-browser (whichever readdir returned first won), and the _gstack-command duplicate could drop the ENTIRE personal-skills set — every /gstack command vanished until the user hand-deleted the alias dirs, and the next setup re-broke it. Fix: copy-then-rewrite. A shared _install_alias_skill_md helper reads the SOURCE SKILL.md and writes a fresh copy with name: rewritten to the alias dir's own name (_gstack-command / connect-chrome / gstack-connect-chrome). sed never edits in place: on Unix the old install was a symlink into the repo, and an in-place rewrite through it would have corrupted the generated source (eng review E2). bin/gstack-relink gets the same treatment for its root-alias helper, and its discovery loop now skips symlinked source dirs so the connect-chrome repo symlink can't re-mint the duplicate. Tests assert: installed aliases are NOT symlinks, carry their own unique names, all installed frontmatter names are globally unique, re-runs refresh cleanly, legacy symlinked aliases are replaced not written through, and the source files stay byte-intact. Fixes #2511 Fixes #2201 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(setup): Windows re-runs refresh installed skills for codex/factory/opencode hosts On Windows (Git Bash / MSYS2, no Developer Mode), _link_or_copy installs REAL directory copies. The install guards in link_codex_skill_dirs, link_factory_skill_dirs, link_opencode_skill_dirs, and create_agents_sidecar only ran the copy when the target was a symlink or missing — true on the first install, never again. Every subsequent ./setup after a git pull reported 'gstack ready (codex).' and exited 0 while silently refreshing nothing: users ran stale SKILL.md forever. (link_claude_skill_dirs already handled this; the other hosts never got the treatment.) Fix: all five guard sites bypass the symlink-or-missing check when IS_WINDOWS=1 — _link_or_copy rm -rf's the destination first, so the real-dir copy refreshes in place. Unix behavior is unchanged (symlinks still pass the guard via -L and serve updates without re-copying). The new bash-fixture test drives the REAL extracted functions through the install → upstream change → re-run cycle under IS_WINDOWS=1 (v1 must become v2), pins the sidecar-skip behavior, checks the Unix path stayed a symlink, and statically asserts the bypass at all five sites so factory/opencode can't regress. Registered in the Windows-safe curated list (KNOWN_WINDOWS_SAFE) so it actually runs on the windows-latest CI lane — the 'bin/' pattern hit is a fixture path segment, not a shebang spawn. Fixes #2444 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(uninstall): remove real-directory skill installs, gated on provenance On Windows, setup installs skills as REAL directory copies (cp -R via _link_or_copy). gstack-uninstall's per-skill loop filtered on [ -L ], so every copy was skipped: --force exited 0 and printed 'gstack uninstalled.' while leaving ~52 gstack-* directories plus _gstack-command/ behind in ~/.claude/skills. The same filter also missed the standard Unix shape (real dir + symlinked SKILL.md), which was left as a dangling-symlink husk. Fix: the loop now handles all three install shapes. Symlink entries keep the existing readlink check. Real dirs with a SYMLINKED SKILL.md are removed when the link points into gstack (same semantics as setup's cleanup helpers). Real dirs with a REAL-FILE SKILL.md — the Windows copy shape — are removed ONLY when both provenance gates pass (F8): (a) the directory name is in gstack's skill inventory (source dir names, frontmatter names, gstack- prefixed variants, and the alias dirs), and (b) the SKILL.md carries the existing generated banner '<!-- AUTO-GENERATED from' (ENG-OV10: every pre-v1.67 copy already carries it; a NEW marker would refuse to delete legitimate old installs, recreating the bug). Anything failing a gate is listed to stderr and never deleted — a user's own skill that happens to share a name with a gstack skill survives. Tests: a fake-tree fixture covers removed/kept/listed for every shape (including the F8 name-collision row), and a census test asserts every installable skill's generated SKILL.md carries the banner so the gate can't strand a bannerless skill. Registered in the Windows-safe curated list — the copy shape is exactly what windows-latest exercises. Fixes #2563 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(setup): wire --host cursor through the full install path './setup --host cursor' was accepted by the flag parser and then did nothing: no INSTALL_CURSOR branch existed, so the script built binaries, printed no 'ready' line, and installed zero skills — Cursor users had no way to install gstack at all. Full install slice, re-derived from PR #2547 by @szsunyuan onto the current installers: generate .cursor/ skill docs (host config already existed), create a minimal ~/.cursor/skills/gstack runtime root (root SKILL.md + bin/lib/browse assets + review checklist pair + ETHOS.md + supabase config — bin and lib travel together because bin scripts import ../lib), link the generated gstack-* skills, and plant the repo-local .cursor/skills/gstack sidecar WITHOUT ever wiping the generated SKILL.md files it shares a directory with (link-before-sidecar ordering keeps the generation fallback alive). Auto mode detects Cursor via the cursor binary or the ~/.cursor footprint. gstack-uninstall removes ~/.cursor/skills/gstack* and per-project .cursor/skills/gstack* — and never rmdir's .cursor itself, where Cursor stores user rules. Re-derivation deltas from the PR: the link guards carry the #2444 IS_WINDOWS bypass (re-runs refresh real-dir copies), lib/ and supabase/config.sh ride along like every other runtime root, and the hosts/cursor.ts sidecar field is omitted (HostConfig no longer carries one — sidecar behavior lives in setup). Fixes #1358 Co-authored-by: Yuan Sun <forrest.sun527@gmail.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(settings): include command in add-event dedup key (#2382) Fixes #2382. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(setup): render the gbrain :user variant to an out-dir — global installs stay git-clean On a global-git install with gbrain, ./setup and 'gstack-config gbrain-refresh' ran gen:skill-docs:user IN PLACE inside the install checkout, rewriting ~16 TRACKED SKILL.md files. The checkout stayed permanently dirty, every /gstack-upgrade 'git stash' saved a redundant snapshot of generated content, and the growing stash list invited a 'git stash pop' that would lay stale instruction markdown from an older gstack over the current version — a quiet wrong-rules failure mode. Fix, wired through machinery that already existed (gen-skill-docs --out-dir + the symlink install layer): brain-aware SKILL.md now renders into the untracked ~/.gstack/render/claude, and both Claude installers serve the render when present — setup's link_claude_skill_dirs prefers $GSTACK_HOME/render/claude/<skill>/SKILL.md, and bin/gstack-relink does the same so a later config change can't silently flip skills back to the blockless canonical source. setup wipes and rebuilds the render each run, repoints installed skills after a successful render, and removes a stale render (re-linking canonical) when gbrain is gone. gbrain-refresh renders to the out-dir and repoints via relink; its 'this dirties the install's git tree' caveat is retired because it no longer does. A one-time upgrade migration (gstack-upgrade/migrations/v1.67.0.0.sh, F12) restores the legacy dirt: unstaged modifications to SKILL.md / sections/ *.md files in the install checkout are git-checkout'd back to canonical; anything outside that footprint (user edits, untracked files, staged work) is left alone and reported. Idempotent, non-fatal, symlinked installs skipped. Tests: render-preference behavior for both installers, static pins that every executable :user invocation carries --out-dir and the caveat text is gone, migration fixture (restore/leave/idempotent/no-op matrix), and the existing out-dir render test now asserts 'git status --porcelain' gains zero new entries across a full :user render. Fixes #2569 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(redact): close the remaining #1946 fail-opens — detection coverage + one-time consent Two of #1946's reported gaps were still open after the v1.64 fail-closed work (the git-error and oversized-diff paths in bin/gstack-redact-prepush are already strict, chunked, and pinned by tests): 1. Detection fail-open: env.kv required an UPPERCASE name with an '=' assignment, so 'api_key=…', 'apiKey: "…"', and 'password: …' — the most common real config shapes — produced NO finding at all. The pattern is now case-insensitive, accepts ':' (YAML/JSON) as well as '=' assignment, and handles quoted JSON keys. It stays MEDIUM and entropy-gated per the calibration rule (a generic net that cries wolf gets bypassed), with pinned cases for each closed shape plus the placeholder/entropy negatives. 2. Install fail-open: nothing ever offered the guard, so a plain 'git push' scanned nothing and users believing themselves protected weren't. setup now asks ONCE for consent on a real interactive terminal (maintainer decision 6): an explicit answer is recorded to the existing redact_prepush_hook key and never re-asked; a timeout or non-interactive run changes nothing and keeps the hint-only posture. Default stays FALSE, and setup still never installs the hook itself — /ship owns the per-repo install (the wrong-repo invariant is pinned by the existing 'setup carries the hint only' test). Tests: per-shape pattern cases, prompt gating statics (key-absence + TTY + timed default-N read), timeout-persists-nothing, non-interactive stays hint-only with no key write, and recorded-answer-is-silent behavior runs. Contributes to #1946 (the pre-push guard's fail-closed scan paths landed in earlier releases; this closes the coverage and consent gaps it names). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(hooks): Stop hook closes dangling timeline entries — fail-open The preamble writes event:'started' to the project timeline at every skill start, but the matching 'completed' write lives in prose at the END of the skill workflow — unenforceable. An interrupted session, a context blowout, or an agent that simply stops leaked started > completed forever, and the leak was unrepairable after the fact (observed live in #2553). New hosts/claude/hooks/timeline-stop-hook (+ .ts, question-log-hook shim pattern): on Claude Code's Stop event it appends event:'completed' with outcome 'unknown' and source 'stop-hook' for every 'started' entry in the project timeline that has no matching completion. setup registers it via gstack-settings-hook add-event (Stop was already an accepted event) under its own source tag, idempotently; --no-team and gstack-uninstall remove it. FAIL-OPEN contract (F5), pinned by tests: ALWAYS exits 0 — corrupt timeline (bad lines skipped individually, valid ones still repaired), missing timeline, garbage/empty stdin, bun missing from PATH (the shim '|| true's), and an over-cap timeline (10MB skip) all repair nothing and block nothing; errors land in ~/.gstack/hook-errors.log best-effort. The write path is append-only with a ~2s internal budget, and a second Stop is a no-op (already-closed entries never re-close). Correlation is project-scoped by design — the preamble's session id is shell-local, so a concurrent same-project session's entry may close early as a traceable source:'stop-hook' row rather than a silent leak; the header documents the trade-off. Fixes #2553 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ios-qa: guard DebugBridgeTouch.m on DEBUG, not just TARGET_OS_IOS DebugBridgeTouch.m and its header both promise the code is DEBUG-only and never shipped: "Uses these private UIKit selectors (DEBUG-only; never shipped to App Store)" "DEBUG-only — never link in Release." Nothing enforced it. The only guard was `#if TARGET_OS_IOS`, so a Release build for iOS compiled the entire implementation in, private API and all. Measured on a real app (an iOS Release build, `nm -j` on the app binary): DebugBridge symbols 15 IOHIDEventCreateDigitizer 2 AXSSetAutomationEnabled 1 symbol, 2 strings IOKit.framework 4 strings including +[DebugBridgeTouch sendTapAtPoint:inWindow:] and _OBJC_CLASS_$_DebugBridgeTouch. That is a Guideline 2.5.1 private-API exposure in a shippable binary, and it fails Package.swift's own stated CI invariant: nm -j build/Release/<binary> | grep -q DebugBridge && exit 1 WHY THE EXISTING GUARD DOES NOT COVER THIS Package.swift documents the protection as `.when(configuration: .debug)` on the consuming target's dependency. That works for SwiftPM consumers. It cannot be expressed by an app that integrates DebugBridge as a local package inside an .xcodeproj: Xcode's Filters column under Frameworks, Libraries, and Embedded Content offers platform conditions only — iOS, macOS, visionOS — never build configuration. So for xcodeproj consumers the documented guard silently does nothing, which is precisely the case that was measured. The Swift targets were already safe: all four .swift files are `#if DEBUG` guarded and Package.swift defines DEBUG for them via swiftSettings. Only the Objective-C target, the one that actually links private API, was unguarded. THE FIX 1. DebugBridgeTouch.m.template now branches `#if !defined(DEBUG)` first and emits nothing at all in Release, falling through to the existing iOS and non-iOS branches only in Debug. 2. Package.swift.template declares DEBUG explicitly for the ObjC target: cSettings: [.define("DEBUG", .when(configuration: .debug))] The two Swift targets already did this. Relying on SwiftPM's implicit DEBUG for C-family targets is not worth betting a private-API exposure on. VERIFIED, by compiling the generated file for iOS both ways: xcrun -sdk iphoneos clang -c DebugBridgeTouch.m -arch arm64 ... Release (no -DDEBUG) 0 DebugBridge symbols, 0 private-API symbols, 448 B Debug (-DDEBUG=1) 7 DebugBridge symbols, 6 private-API symbols, 13104 B The harness is unchanged in Debug. Release now emits an empty translation unit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(ios-qa): bridges search front-most presented content first A presented sheet sits AFTER the screen it covers in window.subviews, so the elements walk emitted the covered screen first — a client taking the first match for a label activated a control the user cannot reach, and the agent saw a success (measured on a real app: the sheet's 'Create' button ranked 210th behind 35+ covered-screen entries). Menus, alerts and action sheets were worse: each gets its OWN UIWindow, so keying off isKeyWindow missed them entirely — absent from /elements, dropped from /screenshot, untappable via /tap. Re-derived from PR #2397 by @IDSTUK onto the current bridge templates (the SwiftUI tap-reliability rework had moved underneath the PR): ScreenshotBridgeImpl gains orderedWindows(in:) (visible windows front-most first by windowLevel then insertion order, PassThroughWindow overlays still filtered), frontmostWindow(), and searchRoots() (per window, the top-most presented view controller's view before the window itself). /elements walks those roots in order through the existing shared visited-set + budget, so overlapping roots emit each view once at its front-most position; /tap targets frontmostWindow() for both the accessibility-activation and synthesized-touch paths; /type and /swipe search the roots in order; /screenshot composites every window back-to-front at the existing 1x scale. The two now-dead private activeScene/activeKeyWindow copies in ElementsBridgeImpl and MutationBridgeImpl are removed. Fixture mirror synced byte-for-byte; verified with a full 'xcodebuild build -scheme FixtureApp-Package -destination generic/platform=iOS Simulator' (BUILD SUCCEEDED, DEBUG guard from the previous commit included). Co-authored-by: IDST UK <IDSTUK@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(setup-gbrain): invoke gstack-memory-ingest/gstack-gbrain-sync via bun run + .ts /setup-gbrain's transcript-ingest steps told the agent to run bin/gstack-memory-ingest and bin/gstack-gbrain-sync by BARE name. Neither exists — only the .ts files ship (mode 644, no bin alias) — so the agent dutifully reported 'script missing at install root' and the ingest/full- sync steps dead-ended on every host (hit live under Codex; the Claude render carries the same text). All four template sites (probe, silent-bulk, post-answer full sync, the preamble-hook incremental mention) and the four memory.md reference-doc sites now use the repo's established form: 'bun run <path>/gstack-memory- ingest.ts …' / 'bun run <path>/gstack-gbrain-sync.ts …' — matching what sync-gbrain already does. Generated SKILL.md regenerated from the template in the same commit. Re-derived from PR #2409 by @SomSamantray per the wave's screening rule (the PR edited the generated SKILL.md directly; the generated file must come from gen:skill-docs). The contributor's structural test rides along as-is: bare-invocation regexes with negative .ts lookahead and backslash- continuation coverage pin every site, so the drift can't return. The referenced-paths ratchet in test/setup-claude-skill-assets.test.ts drops its two #2250 known-broken entries — the class-2 assertion now guards these paths again. Verified against #2250's site list (template lines 690/735/784-area, all covered) plus a fresh grep: zero bare invocations remain in the template or memory.md; the one prose mention ('gstack-memory-ingest now persists…') is not an invocation and stays. Fixes #2250 Fixes #2393 Co-authored-by: SomSamantray <SomSamantray@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): update four main-side assertions to the T3 installer contracts Integration drift from the T3 lane: three static assertions pinned the OLD implementation shapes that T3 legitimately replaced — the gbrain-refresh branch no longer self-documents a reset --hard cycle (#2569 renders to an untracked out-dir instead; the test now pins THAT), setup's regen block renamed to the render form (re-anchored, same exit-code-propagation invariant), and sections/ linking generalized into _link_skill_runtime_assets (the _link_or_copy routing assertion moved into the helper). Fourth: the uninstall neutral-target test asserted against os.tmpdir(), which reads $TMPDIR at call time — a shard neighbor can leave it gstack-containing, making the "neutral" symlink target match the provenance substring; the test now falls back to a fixed neutral root and asserts neutrality explicitly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: whitelist engine-locked at all three gbrain-usable gates (#2456) #2194 taught the classifier to report a PGLite lock held by a live \`gbrain serve\` as engine-locked instead of broken-config, but none of the three "is gbrain usable?" gates accepted the new status — so the symptom moved from a wrong error to a quieter wrong suppression: gbrain-refresh stripped GBRAIN_CONTEXT_LOAD / GBRAIN_SAVE_RESULTS blocks out of every generated SKILL.md after every upgrade, on the RECOMMENDED /setup-gbrain default (PGLite + local-stdio MCP spawns gbrain serve at session start). engine-locked is the same class as timeout (#1964): the engine is installed and healthy, a legitimate holder has the lock. All three gates now agree: - bin/gstack-gbrain-detect --is-ok exits 0 on engine-locked - bin/gstack-config gbrain-refresh case arm renders instead of suppressing - scripts/gen-skill-docs.ts --respect-detection treats it as detected Test mirrors the existing timeout case in test/gbrain-detection-override.test.ts (engine-locked renders brain blocks; the sibling no-cli case still proves suppression works). Applies the reporter's patch + test from the issue. Fixes #2456 Co-authored-by: Mateus Moraes <mmoraes@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: detect bearer-token thin clients via host MCP registration (#2520) The #2051 thin-client fix keys detection on the remote_mcp marker in ~/.gbrain/config.json — but that marker is only written by the OAuth path (gbrain init --mcp-only). Bearer-token installs (gbrain connect <url> --token, gbrain's own recommended default for local/personal use) never touch config.json, so they fell through to the local probe, failed against the dead-or-absent local engine, and landed on missing-config / broken-db / broken-config / engine-locked — silently suppressing brain blocks for a fully-working remote brain. New evidence source: hasRemoteOnlyGbrainMcp() reads ~/.claude.json MCP registrations (user scope AND project scope) with the same classification rules as gstack-gbrain-detect's tier-3 fallback. File-read only — no subprocess, no network (a classifier network probe is the #1964 pathology). Wired at two sites in freshClassify: - missing-config branch: a bearer thin client may never have run a local init; if the host's only gbrain registration is remote-HTTP, that registration IS the brain → thin-client. - post-probe-failure demotion: broken-db / broken-config / engine-locked reclassify to thin-client when the only gbrain registration is remote. A local-stdio sibling registration blocks the demotion (federation guard: a user running a local engine plus a remote team brain keeps precise local statuses). "timeout" is excluded — already usable, and may be a genuinely healthy slow local engine. 7 new unit tests in test/gbrain-local-status.test.ts: user-scope, project- scope, engine-locked/broken-db demotion, federation guard, no-registration discriminator, end-to-end --is-ok gate (35 pass total in the file). Root-cause analysis by @d-danielsun in #2520. Fixes #2520 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: resolve GBRAIN_HOME with gbrain's parent-dir semantics (#2521) gstack treated GBRAIN_HOME as the config directory; gbrain's configDir() treats it as the PARENT and always appends `.gbrain` itself (the contract is explicit in gbrain's source: GBRAIN_HOME=/tmp/x → /tmp/x/.gbrain/ config.json). With GBRAIN_HOME set, gstack classified engine status from a file gbrain never reads — the probe's two halves (file checks vs the spawned `gbrain sources list`) looked at DIFFERENT installs, so any resulting status was arbitrary: missing-config/broken-config against healthy installs, or a thin-client marker gstack saw that gbrain itself reported as "No brain configured". New shared resolver `gbrainConfigDir()` in lib/gbrain-exec.ts is the single source of truth. All seven gstack sites route through the contract: - lib/gbrain-local-status.ts gbrainConfigPath (the classifier's file half) - bin/gstack-gbrain-detect GBRAIN_CONFIG + readRemoteMcpUrl - lib/gbrain-exec.ts buildGbrainEnv (the probe's DATABASE_URL seed — fixing only the classifier would have left the split-brain in the spawn half, flagged by the reporter) - lib/gbrain-guards.ts gbrainHome (clones-dir + autopilot-lock paths) - lib/gstack-memory-helpers.ts gbrainConfigPath (engine-tier fallback) - bin/gstack-gbrain-install pre-doctor config check (shell) Unit tests cover GBRAIN_HOME set (config found at $GBRAIN_HOME/.gbrain), the old flat layout explicitly NOT read (both classifier and buildGbrainEnv), and unset (~/.gbrain unchanged). Existing fixtures that encoded the deviant flat layout are updated to gbrain's contract. Root-cause analysis by @d-danielsun in #2521. Deviation from the 3-site plan spec: the same deviant resolution existed in four more sites (buildGbrainEnv, gbrain-guards, memory-helpers, gbrain-install); fixing only three would have left gstack disagreeing with itself as well as with gbrain, so the whole class moved to the shared resolver in one change. Fixes #2521 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: read project-scoped MCP registrations in gbrain detection (#2499) Claude Code registers MCP servers at two scopes in ~/.claude.json: user scope (.mcpServers) and project scope (.projects["/abs/path"].mcpServers — what `claude mcp add` WITHOUT --scope user writes). Every gbrain detection site read only user scope, so a correctly configured project-scoped brain was invisible: brain-aware blocks suppressed, remote-mode artifacts sync never recognised, and detectEndpointHash fell through to the 'local' literal — two different project-scoped brains hashed identically, so switching between them never invalidated the cache, the exact scenario the function's docstring says it exists to catch. Nothing errored; the features just quietly were not there. Two sites fixed: - scripts/resolvers/preamble/generate-brain-sync-block.ts: the shared detection block (rendered into every tier-2+ SKILL.md) now resolves the gbrain entry ONCE into _GBRAIN_MCP_ENTRY — user scope first, then the nearest-ancestor project entry for $PWD that actually carries a gbrain server (longest matching key with a path-boundary check: /a/repo never matches /a/repo2; a nested project WITHOUT gbrain doesn't shadow its parent's registration). _GBRAIN_MCP_TYPE and _GBRAIN_HOST extract from the resolved entry, so claude.json is parsed once per skill start. All SKILL.md files regenerated in this commit; the ship golden fixtures and three carve-guard skeleton caps (plan-eng-review, plan-devex-review, office-hours; ~1.5KB rendered growth per skill) are refreshed with measured values. - bin/gstack-brain-cache detectEndpointHash: same resolution order in TS (user scope, else nearest-ancestor project entry by cwd, both path separators for Windows keys). Tests: rendered-output tests in test/gen-skill-docs.test.ts pin the regenerated block (static markers + a FUNCTIONAL run of the exact rendered lines against a fixture ~/.claude.json with only a project-scoped registration, plus an outside-cwd discriminator); detectEndpointHash unit tests in test/brain-cache-roundtrip.test.ts cover project-scope resolve, path-boundary, nearest-ancestor distinct hashes, and user-scope precedence. Root-cause analysis by @samporter-31 in #2499. Fixes #2499 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: /sync-gbrain respects an existing valid .gbrain-source pin (#2417) /sync-gbrain always derived a new worktree-scoped source ID, even when the repository already carried a valid .gbrain-source pin created through the native GBrain source workflow — silently bypassing the selected source boundary, registering a duplicate federated source, and routing later dream/cycle checks to the wrong source. Now a local pin is reused when it passes the fail-closed identity checks: the ID is syntactically valid, the source is registered, and the registered path realpath-resolves to the current checkout (so a stale or copied dotfile can't redirect a sync into another repo's source). A confirmed pin is treated as user-managed — synced and attached without add/remove, legacy migration, or federation changes. Dry-run stays spawn-free (reads only the local marker for previews). Missing, invalid, stale, or unreadable pins fall back to the existing generated source ID. Absorbs PR #2417 by @exGeni (applied via git am -3; 42 tests pass in test/gstack-gbrain-sync.test.ts including the new pin-respecting coverage: spawn-free dry-run, symlink-equivalent registered paths, non-dry-run sync/attach with no add/remove, dream routing, unreadable markers, config-backed env use). Co-authored-by: Evgenii Lopatin <e75533@gmail.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: gstack-gbrain-install --dry-run no longer requires the network (#2540) The GitHub reachability probe (curl --head, 10s max) was gated only on --validate-only, so a --dry-run — which prints a plan and exits without ever cloning — could fail with exit 3 "cannot reach https://github.com" whenever the curl lost a race for sockets/DNS. Reproducible at ~15% by running 60 dry-runs concurrently, and the cause of intermittent red in the D5 detect-first tests, which call this exact path. The probe now also skips under --dry-run: requiring the network for a plan-print buys nothing and costs a real failure mode. Real installs still fail fast when offline rather than hanging git clone. Absorbs PR #2540 by @CarringtonCreative (applied via git am -3; 26 tests pass across test/gbrain-detect-install.test.ts + test/egress-receipt-wiring.test.ts). Fixes the offline/flake half of #2536. Co-authored-by: Carrington Dennis <carrdenn3@gmail.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: accept 3-digit semver + package.json version sources (#2501) Two version-source shapes failed CLOSED in a way that silently disabled /ship's queue-collision check: 1. A --version-path / .gstack/version-path target that is a package.json was read as raw text: the whitespace strip turned the JSON into '{"name":"frontend",... which parseVersion rejected, so every read — local, `git show`, and rival PRs' claims through the GitHub/GitLab Contents APIs — fell back to 0.0.0.0 and competing claims were dropped as "malformed". 2. parseVersion required exactly four components, so gstack-next-version exited 2 on EVERY invocation in a 3-digit repo. That CLI IS the queue-collision check; /ship then took its documented offline path of naive local arithmetic, two branches cut from the same base picked the same version, and git merged the duplicate without a conflict. New lib/version-source.ts holds the shared semantics so both CLIs agree by construction: parseVersion accepts 3- or 4-digit (3 pads the micro slot for uniform comparison), versionWidth/fmtVersion keep a 3-digit repo 3-digit through bumping and formatting, micro coerces to patch on 3-digit repos (with a warning in the output), and extractVersion reads a .json version-path as JSON (.version) from any byte source. gstack-version-bump treats a package.json version-path as that repo's single source of truth (written in place, DRIFT_* states can't arise — no second file to drift from). Detection is by shape, not new configuration. Scope per the wave plan's version-tooling end-state spec (decision 11, ENG-OV1): this is the READING capability + 3-digit acceptance ONLY. gstack's own VERSION file stays the 4-digit source of truth; nothing here flips authority to package.json. The PR's bundled fix for the .gstack/version-path pin being ignored by classify's base read lands separately (#2462) — these tests drive the JSON version-path through the explicit --version-path flag. Re-derived from PR #2501 by @YiftahR (73 tests pass across test/gstack-version-bump.test.ts, test/gstack-next-version.test.ts, test/ship-version-sync.test.ts). Fixes #2501 Co-authored-by: YR <work.yiftah.rottem@gmail.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: write/repair sync npm lockfiles' version fields (#2567) npm records the package version twice in its lockfiles — top-level `version` and, in lockfileVersion >= 2, `packages[""].version` (the entry describing the root package itself) — and `npm install` keeps both in step. gstack-version-bump write/repair updated VERSION + package.json but left the lockfile behind, so every /ship bump in an npm repo drifted one field per release until someone ran npm, dirtying the tree on the next `npm install` far from the cause. write and repair now mirror the version into package-lock.json AND npm-shrinkwrap.json (which shares the format and, when present, is what npm actually honors) as a pure JSON edit — no npm spawn, no dependency-tree churn, dependency entries untouched. Per the wave plan's version-tooling end-state spec (decision 11): synced ONLY when the file already exists, never created (gstack itself is bun-only). A failed manifest/lockfile write keeps the existing exit-3 half-write semantics so classify reports DRIFT_STALE_PKG on re-run instead of hiding the drift. Tests: 5 new cases in test/gstack-version-bump.test.ts — both lockfile version fields synced with deps untouched, repair heals a stale lockfile, lockfileVersion 1 (no packages map) doesn't crash, npm-shrinkwrap.json synced without inventing a package-lock.json, malformed lockfile exits 3 loudly (26 pass total in the file). Re-derived from PR #2568 by @ortonom under decision 11. Fixes #2567 Co-authored-by: ortonom <3261546+ortonom@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: subdirectory manifests + npm-valid version mirror (#2531) Two gaps in gstack-version-bump's manifest handling, resolved to the wave plan's version-tooling end-state spec (decision 11): 1. Subdirectory manifests. A repo whose only Node package lives in web/, app/, or frontend/ has no ROOT package.json, so join(cwd, "package.json") reported pkgExists:false and every bump silently wrote VERSION alone — leaving the manifest to be bumped by hand, which is exactly the drift this tool exists to prevent, in the one layout where it silently did nothing. All three subcommands now resolve the manifest as --package-json-path → .gstack/package-json-path → ./package.json (mirroring resolveVersionPath). 2. npm-valid mirror. VERSION is 4-digit MAJOR.MINOR.PATCH.MICRO; npm's semver is 3-component and rejects a fourth, so mirroring the raw form breaks `npm ci` in any repo npm actually manages. The manifest and its lockfiles now carry the npm-valid 3-digit translation (1.67.0.0 → 1.67.0) via npmVersion() in lib/version-source.ts. VERSION stays the 4-digit source of truth. classify judges drift against the TRANSLATED form — a correctly-synced `0.1.25` no longer reads as eternal drift against `0.1.25.0` — and grandfathers the pre-v1.67 1:1 four-digit mirror as in-sync (flagging it DRIFT_UNEXPECTED would hard-stop /ship on every existing repo on upgrade day; the next write migrates the manifest to the translated form). Lockfiles are synced beside the resolved manifest — including beside a pinned JSON version-path — and only when they already exist. classify output gains pkgPath and expectedPkgVersion for observability; write/repair report packageJsonPath + packageJsonVersion. The /ship Step 12 prose (ship/SKILL.md.tmpl) documents the resolution chain and the translation; SKILL.md files regenerated and ship golden fixtures refreshed in this commit. Tests: subdirectory pin + --package-json-path override, translated-form classify (FRESH/ALREADY_BUMPED, no false drift), grandfathered 1:1 mirror, genuine divergence still drifts, repair to the npm-valid form (33 pass in test/gstack-version-bump.test.ts; 526 pass across the five affected files including goldens and parity). Re-derived from PR #2531 by @CarringtonCreative on top of the 3-digit/ JSON version-source work, under decision 11 (which resolves the PR's lockfile-gated translation in favor of an unconditional npm-valid mirror). Co-authored-by: Carrington Dennis <carrdenn3@gmail.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: git-based version allocator when the PR queue is unreachable (#2545) When the host query (gh/glab) failed, gstack-next-version returned offline:true with an EMPTY claim set, and /ship's documented fallback was local BUMP_LEVEL arithmetic. Local arithmetic cannot see a sibling's claim, so the fallback allocated a version another open PR already held — observed in a downstream repo where two merged PRs both read v0.1.57.0 (and an audit found four such duplicate pairs over three weeks). New fetchGitClaimed() degrades the QUEUE VIEW without degrading the ALLOCATION: git already knows what the API was asked for. It reads every remote-tracking branch's pinned version file (through extractVersion, so JSON version-paths resolve on remote refs too and each branch's own digit width is preserved) plus the versions already shipped in the base's last 400 commit subjects (3- or 4-digit; the cap announces itself in warnings when it truncates). The fallback runs only when the host told us nothing — the online path is untouched — and the output gains a load-bearing `fallback: "git" | null` field that /ship can branch on, plus explicit warnings for both the recovered-from-git and the nothing-found cases. Tests: end-to-end stub-gh offline contract (fallback:'git' + a valid version + the warning), sibling-claim discovery from remote-tracking refs, the pick advancing past the sibling's claim, shipped-subject scanning, JSON version-path claims on remote refs, and non-repo degradation to a warning (45 pass in test/gstack-next-version.test.ts). Re-derived from PR #2545 by @CarringtonCreative under the wave plan's version-tooling end-state spec; the PR's own VERSION/CHANGELOG stamping is stripped (release stamping happens at /ship time, not per commit). Co-authored-by: Carrington Dennis <carrdenn3@gmail.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: version-bump honors the .gstack/version-path pin in versionRel (#2462) cmdClassify's current-version read already resolved the .gstack/version-path pin, but versionRel — the repo-relative path fed to `git show origin/<base>:<path>` — was derived from the CLI flag alone (`argVal(args, "--version-path") ?? "VERSION"`). In a pinned repo with no explicit flag, base and current therefore read DIFFERENT files: current from the pinned file, base from the root VERSION. On a repo with no root VERSION, the base always read 0.0.0.0 — and the pinned-JSON handling never engaged, so a pinned package.json was read as raw text (currentVersion 0.0.0.0) and `write` would have overwritten the manifest with a bare version string. New resolveVersionRel() resolves the pin's REPO-RELATIVE form once (flag → .gstack/version-path first line → "VERSION"); classify, write, and repair all derive both the relative and absolute paths from it, so base and current reads can no longer diverge. The old resolveVersionPath (which returned an absolute path `git show` cannot use) is folded in. Unit tests (the ENG-OV6 spec case plus write/repair coverage): pin set + no flag → classify reads base AND current from the SAME pinned file (plain-text sub/VERSION and pinned frontend/package.json, both against a real git base with NO root VERSION anywhere), write updates the pinned manifest in place without inventing a root VERSION, repair treats the pinned JSON as single-source, and the explicit flag still overrides the pin (38 pass in test/gstack-version-bump.test.ts). Re-spec'd per ENG-OV6 from the report in #2462 (the originally-filed classify-read hypothesis was already handled; the live bug was the :138 versionRel derivation). Same fix shape independently identified in PR #2501 by @YiftahR. Fixes #2462 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: diff-scope glob coverage, honest exit contract, dirty-tree visibility (#2526, #2455, #2299) Three silent-skip classes in bin/gstack-diff-scope, each of which quietly disabled scope-gated reviewers in /ship and /review: 1. Pattern gaps (#2526, #2455). `*/api/*` required a path segment BEFORE api/, so a root-level api/ layout (Vercel serverless, Next.js pages/api at root) never set SCOPE_API — 63 serverless functions in the reporter's payments repo, none ever classified, the API-contract specialist silently skipped on every payment PR (it found a CRITICAL when run by hand). Same for root-level migrations/. And the Rails data_migrate gem's db/data/ data migrations — arbitrary Ruby run unattended against production data — fell through to plain BACKEND, so the [NEVER_GATE] data-migration specialist never got the chance to run. Added: api/*, migrations/*, db/data/*, data_migrations/*. 2. All-false was indistinguishable from "could not look" (#2526). New contract: empty change set → all false exit 0; >=1 match → flags exit 0; changed files with ZERO matches → SCOPE_ERROR=unmatched + the unmatched paths as comment lines + exit 2 (a new top-level layout now trips loudly instead of invisibly disabling reviewers); unresolvable base ref (shallow CI checkout) → SCOPE_ERROR=no_base + exit 2 instead of a green that means "we could not look". Every output line stays a shell-safe assignment or comment for sourcing consumers, which tolerate the nonzero exit today (source ... || true / eval). 3. Uncommitted work was invisible (#2299). /ship detects scope in Step 9, BEFORE it commits in Step 15, so the common start-work-then-ship flow ran the classifier against an empty diff and skipped every reviewer. The change set is now the UNION of committed diff + working tree + untracked files. Also from #2299: the single first-match-wins case made the nine flags mutually exclusive (Button.test.jsx set FRONTEND but not TESTS; util.test.ts the opposite) — each category now gets its own case, with BACKEND deliberately still excluding frontend component/view files. And file listing is NUL-safe (git diff -z), so non-ASCII paths no longer defeat extension globs via octal quoting. Deliberate behavior change (flagged in #2299): with independent flags, a backend test file sets BACKEND and TESTS, which can trip the security specialist's SCOPE_BACKEND gate on test-only PRs — errs toward more review, not less. Table-driven tests cover every glob class (root api/, nested api/, controllers, openapi, root/nested/prisma/db-migrate/db-data migrations, dual-category test files, auth, prompts, docs, plain classes), the four-state exit contract, dirty-tree + untracked visibility, and the non-ASCII path case (39 pass in test/diff-scope.test.ts). Fixes shaped by the reporters' patches: @grant-ship-it (#2526),…
Owner
|
Thank you — this was absorbed on main (credited in the v1.6x CHANGELOG entries; roster in PR #2604). Closing. |
garrytan
added a commit
that referenced
this pull request
Aug 19, 2026
… tests hermetic Two new G2 pins: N unfetched claim branches resolve with exactly ONE fetch spawn (PATH-shimmed git counts invocations), and one unservable ref no longer poisons the batch — live claims resolve via the bounded retry while only the ghost warns UNKNOWN. The #2545 offline-contract tests now run the CLI in a local fixture repo instead of the repo's own checkout: the checkout path did a live ls-remote against the real origin (operator-network-dependent, and the CI shard-deadline hang). The online-contract test gains a succeeding gh stub, so fallback:null is asserted deterministically instead of only when the operator happens to be authed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
garrytan
added a commit
that referenced
this pull request
Aug 19, 2026
… and 21 issues closed with receipts (#2632) * fix(plan-tune): reject never-ask on one-way ids at --write --check already ignored those prefs; --write still stored them and --stats counted them as a working NEVER_ASK. Refuse the write and count leftover on-disk prefs as INERT_ONE_WAY. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix: gstack-config get returns "" with exit 0 for keys that have no default Skill preambles read configuration with VAR=$(gstack-config get <key> 2>/dev/null || echo "<default>") and that fallback only fires on a non-zero exit. lookup_default ended in a catch-all that echoed "" and returned 0, so for any key missing from the table VAR came back empty and the default written right there in the preamble was unreachable. The skill then branched on a value it never specified: "skip entirely if QUESTION_TUNING is false", reached with QUESTION_TUNING="". Four keys that skills actually read had no entry and took that path: question_tuning -> callers assume "false" repo_mode -> callers assume "unknown" team_mode -> callers assume "false" transcript_ingest_mode -> callers assume "off" Each default above is the value the call sites already substitute in their own `|| echo` fallback, so this only makes reachable what was already intended. The catch-all now returns non-zero. That is deliberately scoped to the unknown-key arm alone: keys whose default is intentionally empty still exit 0, because "" is their real answer and their callers depend on it -- cross_project_learnings ("unset triggers the first-time prompt"), redact_repo_visibility ("empty falls through to gh/glab detection"), salience_allowlist, user_slug_at_*. Making every empty answer an error would have broken those. test/gstack-config-defaults.test.ts pins the class rather than the four instances: it parses the case arms and asserts every `gstack-config get <key>` site in the tree is covered, so adding a read without a default fails CI. It also pins the exit-code contract in both directions. Verified failing against the pre-fix script, where it names exactly those four keys. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(redact): a typo'd subcommand no longer exits 0 having done nothing main() recognised exactly two subcommands and let everything else fall through to the stdin scan. On empty stdin that prints "(no findings)" and exits 0, so: $ gstack-redact install-prepush-hooks # plural typo gstack-redact scan — repo UNKNOWN (no findings) $ echo $? 0 No hook was installed, and the operator has every reason to believe the credential guard is armed. A guard that silently no-ops must never exit 0. Two smaller faults in the same dispatch, both of which lead people here: - There was no --help handler, so `gstack-redact --help` fell through to the scanner. Piping a credential to it scanned the secret and exited 3. - With no piped input and no --from-file, readInput() blocks on readSync(fd 0) until an EOF that an interactive terminal never sends. That prints nothing at all, so it reads as a hang rather than as "this is a filter, feed it". Now: --help/-h/help prints usage and exits 0; an unrecognised positional prints the offender and exits 1; a TTY with nothing piped in prints usage instead of blocking. "scan" stays accepted, because the human output header reads "gstack-redact scan — repo …" and that is what people type. Usage errors exit 1, deliberately not 2 or 3. Those mean MEDIUM and HIGH findings and callers gate dispatch on them, so a usage error exiting 2 would be read as "medium findings — prompt the user". A test pins that. Tests: 4 written failing first, then fixed. Full suite 7,722 pass / 0 fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(browse): one ambiguous ref no longer kills the whole annotated screenshot `snapshot -a` exits 1 with "Selector matched multiple elements" on most real pages, so /qa, /canary and /land-and-deploy silently produce reports whose screenshots do not exist. Plain `screenshot <path>` is unaffected. Refs are built as getByRole(role, {name}) and disambiguated with .nth() when role+name repeats. That disambiguation cannot fire for a node with NO accessible name: the locator degrades to getByRole(role) with no name filter, and the count driving .nth() is taken from the FILTERED aria snapshot while getByRole matches the unfiltered DOM. Measured on a live page: the tree surfaced 2 unnamed paragraphs, the DOM had 9. Landmarks (banner/main/contentinfo) and paragraphs are correctly unnamed per ARIA, so this is the common case rather than an edge case. boundingBox() then hits Playwright strict mode, and the catch allowlisted only timeout/closed/Target/Execution-context messages — so the strict-mode error was re-thrown and aborted every remaining annotation. Two changes: - `.first()` before boundingBox(), so an ambiguous ref draws a box on its first match instead of aborting. The heatmap path below has always tolerated this via a bare `catch {}`; annotate was the only path that could be killed outright. - the catch no longer re-throws on unrecognised messages. A box we cannot measure is a box we do not draw, never a reason to lose the rest of the page. Set BROWSE_DEBUG to see what was skipped. Also: `-o` passed without `-a`/`-H` was silently ignored (exit 0, no file), which reads as "screenshots are broken" rather than "you forgot a flag". It now warns and points at `browse screenshot <path>`. Verified by rebuilding both ways against the same page with 51 refs present: before — "Selector matched multiple elements", no file written after — exit 0, 229KB PNG Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(version-bump): missing or empty VERSION no longer repairs a fabricated 0.0.0.0 into package.json repair now fails with exit 2 when the VERSION file is absent or empty instead of folding to DEFAULT ("0.0.0.0") — which passed VERSION_RE and regressed package.json below where it started. classify gains an additive versionFileExists field so /ship can tell a real 0.0.0.0 from a fabricated one. Re-derived from PR #2612 under the generated-file screening rule. Fixes #2600 (repair half; the path-configurability half landed in v1.67 via #2531). Contributed by @Lockyer228 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(memory-ingest): --probe counts post-attribution, through the same gate --bulk uses probeMode previously stat'd every walked file, so setup-gbrain gated its silent bulk ingest on pre-filter counts that the write path would never ingest (#2394). The attribution decision now lives in ONE shared gate (sessionIsAttributable — cheap-parse: cwd extraction + memoized resolveGitRemote, never a full page build) used by BOTH probeMode and preparePages, so the two stages' post-attribution counts are structurally identical. ProbeReport gains skipped_unattributed; the probe prints what it excluded and --include-unattributed restores raw counts. The parity is pinned at the prepare stage (probe post-attribution == transcripts reaching import), deliberately NOT == final written. Re-derived from PR #2612 under the generated-file screening rule; the shared-gate design and the remote memo are additions from the plan review. Fixes #2394. Contributed by @Lockyer228 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(browse): allow CPU and network throttling for performance measurement Adds Emulation.setCPUThrottlingRate and Network.emulateNetworkConditions to CDP_ALLOWLIST. Motivation: diagnosing a real "uploads take 1-2 minutes" report, the only machine available was a fast developer workstation. Client-side processing measured 1.4s where the user experienced minutes, so the conclusion had to be reached arithmetically rather than observed. Throttling would have let the measurement reproduce the reporter's conditions directly. Both fit the existing posture rather than widening it: - Emulation already allows setDeviceMetricsOverride, clearDeviceMetricsOverride and setUserAgentOverride, which are equally mutating and scoped to the tab. - Neither method reads page content. setCPUThrottlingRate affects only timing; emulateNetworkConditions constrains traffic rather than inspecting it, so no request bodies, headers or cookies are exposed. Both are output: 'trusted' because they return no page-derived data. scope 'tab' for both, matching the surrounding Emulation entries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(session-update): lock pidfile records the live holder; hard TTL bounds every wedge (#2613) echo $$ inside the backgrounded subshell recorded the PARENT hook's PID — which exits immediately — so every subsequent session judged the lock stale and rm -rf'd a LIVE holder's lock, letting concurrent updaters run over each other. The pidfile now records ${BASHPID:-$(sh -c 'echo $PPID')} (macOS bash 3.2 has no BASHPID; the sh child's PPID is exactly this subshell). Staleness is now two independent detectors: PID liveness (as before, but against the real holder), and a 30-minute hard TTL on the heartbeat mtime — reclaimed regardless of kill -0, so a recycled PID or hung holder can't wedge the lock forever. The holder touches the pidfile after the pull and after setup, so a legitimately-slow run keeps itself alive. Empty and missing pidfiles are respected inside the TTL window (the mkdir→echo race) and reclaimed past it. Fixes #2613. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(browse): explicit windowsHide on every Bun.spawn site + census tripwire (#2575 residual) Bun.spawn sites were structurally outside the windowsHide census (it swept child_process bindings only). The runtime was already safe — native Bun hides consoles by default and bun-polyfill.cjs defaults windowsHide !== false since #2523/#2539 — but implicit defaults are exactly what regress silently. Every Bun.spawn/spawnSync in browse/src now carries the explicit flag (harmless on unix-only sites like Xvfb/xattr/open), and a second SWEEP in windows-spawn-hide.test.ts fails CI on any new flagless Bun.spawn site. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(gbrain): brain worktree advances on the daily sync — no more silently stale brains (#2516) The daily pull refreshed only ~/.gstack itself, never the detached worktree at ~/.gstack-brain-worktree that gbrain actually indexes — so after setup the brain served stale pages forever unless setup-gbrain/sync-gbrain happened to run. brain-sync --once now advances the worktree once per 24h behind an ATTEMPT stamp (.brain-worktree-last-advance — a persistently-failing advance warns once a day, not at every skill boundary), inside the existing run lock and before any ingest step touches the worktree. The new gstack-gbrain-source-wireup --advance-only is built for the unattended cadence: git-only (no gbrain prereqs), pins every operation to the managed worktree (refuses paths that are not worktrees of the artifacts repo), refuses dirty worktrees, and never runs the force-remove recovery — a cron path must not be able to delete local changes. A static pin keeps the force-remove out. docs/gbrain-sync.md stops overclaiming the old cadence. Fixes #2516. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(memory-ingest): honor the per-remote deny/read-only trust policy (#2392) Transcript ingest now respects the same trust store as code import — the gate existed only in gstack-gbrain-sync's runCodeImport, so memory-ingest happily ingested transcripts from deny-listed repos. preparePages filters prepared transcript pages through ONE batch policy lookup (new 'get --batch' verb on bin/gstack-gbrain-repo-policy — the script owns URL normalization; the client adds repoPolicyTierBatch, one spawn for all distinct remotes, so large corpora never pay a 10s-timeout subprocess per remote). Outcomes match code-import semantics: read-only → clean skip (skipped_policy_readonly), deny → counted refusal (skipped_policy_deny), corrupted/unreadable store → HARD ERROR before any write (state, staging, egress receipt, and import all untouched) with the recovery command named — policy corruption must never read as successful ingestion. Artifacts are never policy-filtered (their git_remote is a project slug, not a remote). Fixes #2392. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(config): repo_mode keeps its empty no-default semantics (#2611 follow-up) The ported defaults table synthesized repo_mode → "unknown", but EMPTY is load-bearing for that key: gstack-repo-mode treats any non-empty answer as a user override and skips its own repo classification — the synthesized default turned the classifier into dead code (REPO_MODE=unknown everywhere; caught by test/gstack-repo-mode.test.ts via the wave's cross-agent blame protocol). repo_mode joins the empty-is-real carve-outs (empty output, exit 0). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(pair-agent): consent before killing a healthy headless daemon The pair-agent headed switch spawned 'connect --force-restart' unconditionally — auto-killing a live headless daemon (open tabs, cookies, logins) in direct contradiction of the iron rule it sits beside ('only an explicit --force-restart may kill a live daemon'). The CLI now captures daemon liveness BEFORE ensureServer (which can itself boot a fresh daemon) and relaunches only when the user passed --force-restart to pair-agent; otherwise it prints the tab count and continues against the existing daemon. The /pair-agent skill gains a matching one-way-door consent question (template half rides the wave's template block). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(gbrain-status): MCP scoping is per-project, and project-local beats user scope hasRemoteOnlyGbrainMcp scanned EVERY project's mcpServers in ~/.claude.json, so one project's remote gbrain registration reclassified broken local engines as thin-client machine-wide. It now reads user scope plus only the cwd's nearest-ancestor project key. The precedence itself was verified empirically and hermetically (fake HOME + CLAUDE_CONFIG_DIR fixtures, claude 2.1.233): with both scopes defining gbrain, 'claude mcp get gbrain' reports Scope: Local config — PROJECT-LOCAL WINS. Both in-repo consumers assumed the opposite; brain-cache's endpoint resolution flips to nearest-ancestor-project-first, and the stale user-first pin in brain-cache-roundtrip now pins the verified precedence. (The user-first jq in the brain-sync preamble resolver gets the same swap in the template block.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(slug): gstack-slug matches remote-slug's owner-repo canonical form (live misfile bug) Found live during this wave's CEO review: bin/gstack-slug emitted SLUG=garrytan for this garrytan/gstack worktree while remote-slug correctly gave garrytan-gstack — decisions, timeline, ceo-plans, and learnings were filing into the wrong project store (observed polluting Context Recovery with another repo's decisions). Root cause: a stray empty ~/.git directory made the walk-up crown $HOME as the outermost project root; the remote lookup ran only against that root, failed silently, and the basename fallback cached 'garrytan' sticky. NOT worktree-specific — any strong marker on a non-repo ancestor triggered it. Fix: the walk now finds the outermost ancestor whose .git actually resolves an origin remote and derives owner-repo with remote-slug's byte-identical parse; marker-only ancestors keep anchoring the basename fallback but can no longer shadow a real remote. A new cache self-heal recomputes the poisoned shape (cached == basename of a marker root while a remote-bearing repo exists below), preserving legit #2212 stickiness. Nested-repo walk-up, no-remote and non-git fallbacks, and the SLUG=/BRANCH= eval contract are unchanged, pinned by a 10-case parity suite. Store migration for pre-fix data is tracked in TODOS.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(brain-sync): per-record spool dir — the enqueue/drain race dies structurally Producers appended lines to .brain-queue.jsonl while the drain re-read and os.replace'd it; the in-code comment admitted a lockless append between the re-read and the replace was lost. Locks and rename-rotation designs were both reviewed and rejected (each retained a tail race); the shipped design is a maildir-style spool: one FILE per record in .brain-queue.d/ (tmp + atomic rename), the drain snapshots filenames, processes, and deletes exactly what it snapshotted. Writer and drainer never share an inode — nothing to race. Semantics: at-least-once (a crash between process and unlink re-drains; downstream content-hash dedup absorbs duplicates); retained (privacy-held) records keep their files; unparseable records are kept + warned, never destroyed. Legacy .brain-queue.jsonl migrates atomically on the next drain (crash-leftover .migrating files recovered too); status/drop-queue count both surfaces; discover-new writes spool records and advances its cursor per-record-written. The preamble's queue-depth line switches to spool count in this wave's template block. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bin-context): native slug fallback walks up like bash gstack-slug slugFromEnvironment derived the slug from the INNERMOST repo's origin while bash gstack-slug walks to the outermost project root — nested/vendored repos split their stores across the bash/native boundary (win32 hits the native path constantly). The native fallback now ports _outermost_project_root faithfully (strong/weak markers, outermost-strong-wins, 64-depth cap, fixed-point termination) plus the full resolution order: env override → walk-up → sticky cache with the #1125 self-heal → remote get-url → basename. Twelve mirrored scenarios drive BOTH implementations against the same fixtures and pin identical slugs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(next-version): git fallback queries the live remote, never mutates, and keeps 3-digit width The degraded path counted every remote-tracking ref on every remote — stale experiment branches and second remotes inflated version allocation, and a failed base read flipped 3-digit repos to 4-digit slots. Now: ls-remote --heads origin first (GIT_TERMINAL_PROMPT=0, 5s timeout, zero local ref mutation); on failure, local refs/remotes/origin ONLY with an explicit stale-refs warning; a failed base read zeroes at the LOCAL version file's width so a 3-digit repo allocates 0.0.1, not 0.0.1.0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(setup): hooks register the global-install path and re-point stale ones Registering hooks from a dev worktree baked that worktree's absolute path into settings.json — deleting the worktree left a dead hook erroring on every session stop, and the presence-only dedup (list-sources | grep) could never re-point it. setup's hook paths now route through _hook_install_path (global install preferred, source dir fallback), and the new ensure-event verb on gstack-settings-hook compares the registered command payload against canonical: identical → no write, different → single atomic replacement (never zero or two registrations). The plan-tune hooks had the same stale pattern and get the same fix without re-triggering their consent prompt. Also hardened: bun 1.3.13 turns an uncaught sync fs error in bun -e into a SILENT exit 0 — the registrar's write path now catches, prints, and exits 1, so a failed update can never report fake-green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(preamble): learnings capture is unconditional at completion (#2402) 43 of 44 learnings entries came from explicit /learn — the completion-status prose read 'if you discovered a durable project quirk... log it', which models treated as optional. The step now ALWAYS runs: review the session for durable learnings, log each one, and state 'No durable learnings this session' explicitly when the review comes up empty — an empty result, never a skipped step. Re-derived from PR #2612 under the generated-file screening rule. Fixes #2402. Contributed by @Lockyer228 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(scrape): untrusted-content warning on the page-fetching skills (#2441) /scrape and /skillify consumed page content with zero injection guidance — the CHANGELOG claimed coverage the skills didn't have. The warning now lives in ONE exported const (UNTRUSTED_CONTENT_WARNING in resolvers/browse.ts), embedded in the browse COMMAND_REFERENCE as before AND injected standalone into both skills via the new {{UNTRUSTED_CONTENT_WARNING}} token — single source, wording can never drift between surfaces. Re-derived from PR #2612 under the generated-file screening rule. (Structural isolation for skillify-generated code is tracked as its own TODO.) Fixes #2441. Contributed by @Lockyer228 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(review): checklist paths resolve from the installed skill root (#2518) /review Step 2 read .claude/skills/review/checklist.md — a path relative to the TARGET repo, which only resolves in gstack's own checkout. Every checklist/greptile-triage/TODOS-format reference (six across five templates — two more than the issue named, same class) now uses the installed-root form ~/.claude/skills/gstack/review/... that the templates' other references already use. The install-root class itself (non-default install dirs) is #1882, deliberately its own PR. Fixes #2518. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(pair-agent): one-way-door consent question before a daemon relaunch (template half) The skill flow now checks daemon liveness before Step 4 and asks an explicit one-way-door question (tabs/cookies/logins are lost) before passing --force-restart — never proceeding on a vague reply. Pairs with the CLI-half commit that stopped pair-agent auto-killing live daemons. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(codex): resume does not amortize the ~21K session prelude (#2387) Measured (#2387): every codex exec call pays Codex's session prelude, and a resumed call came in slightly ABOVE a fresh one — resume buys continuity, never token savings. The skill now says so where the resume flow lives: prefer one codex call per skill, batch questions into it. Fixes #2387. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(upgrade): fast-forward first; reset --hard only behind a proved-safe gate (#2517) /gstack-upgrade went straight to stash + reset --hard origin/main. Now it tries git pull --ff-only --autostash first (the same policy session-update's auto-upgrade uses). The destructive fallback runs unprompted ONLY when both git status --porcelain AND git rev-list origin/main..HEAD are empty — a clean tree with unpushed local commits is NOT safe, reset destroys them. Anything else requires an explicit one-way-door confirmation that lists every dirty file and unpushed commit being discarded. Fixes #2517. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(preamble): brain-sync block counts the spool queue and resolves MCP project-first Two resolver halves deferred from earlier wave commits: the queue-depth line counts .brain-queue.d/*.json spool records (plus legacy lines until the drain migrates them), and GBRAIN_MCP_ENTRY_JQ swaps its operands to nearest-ancestor-project-first — matching the empirically verified Claude Code precedence (project-local beats user scope) instead of the backwards user-first assumption. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: regenerate SKILL.md docs + golden fixtures (single regen for the template block) Pure generator output for the six template/resolver commits above (learnings capture, untrusted-content warning, review paths, pair-agent consent, codex resume note, upgrade ff-only, brain-sync block) — bun run gen:skill-docs + --host codex + --host factory, with the three ship golden fixtures refreshed per the documented procedure. The three sidecar-path pins in gen-skill-docs.test.ts move to the new installed-root/$GSTACK_ROOT contract (#2518). Restores template freshness; full suite green from here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: TODOS.md — strike the six wave-fixed residuals, add two follow-ups The v1.67 adversarial-review residuals section shrinks to the one item the wave couldn't reach (iOS tap routing — needs real-device verification). New entries: skillify structural isolation (a prose warning is not a boundary for page-derived generated code) and the slug store migration (pre-fix sessions on stray-marker machines filed data under the degraded slug; post-fix reads go to the correct store, so history needs a merge/alias). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: align cross-cutting pins with the wave's contracts Three suites pinned pre-wave behavior: browse's gstack-config test asserted the old unknown-key ''/exit-0 shape (#2611 made it exit 1); the Windows-paths suite pinned O_APPEND enqueue atomicity (the spool design satisfies the same invariant via tmp + os.replace, one file per record — pinned in its new form); and nine carve-guard skeleton ceilings absorbed the #2402 unconditional-learnings prose (~450B per skill), bumped with measured values per the guard's own protocol. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: re-anchor the referenced-path scanner self-check to the gstack-rooted review refs The self-check pinned the review checklist as a class-1 alias-relative ref; #2518 moved those refs to the installed gstack root (class 2). The guard now proves the scanner sees them in their new class, so the class-2 assertion can't go vacuous. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: pin the wave's prose-tier behaviors (ship coverage-audit gap closure) The coverage audit found one regression-shaped gap: nothing pinned that the upgrade template's ff-only pull precedes the gated reset --hard (#2517) — a future template edit reverting to reset-first would fail nothing. Pinned: the ordering, the FF_OK gate, and the unpushed-commits check. Also pinned the two minor gaps: the {{UNTRUSTED_CONTENT_WARNING}} injection points in scrape/skillify (#2441) and brain-uninstall's spool-dir cleanup. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: pre-landing review round — 8 auto-fixes + 8 accepted findings hardened The ship review army (4 specialists + red-team + checklist, 29 findings) produced 8 mechanical auto-fixes and 11 decisions; the accepted set: - win32 slug parity completed: lib/bin-context.ts gains the remote-first outermost walk + degraded-cache self-heal the bash side got this wave — the two implementations now agree on the stray-marker live-bug shape, pinned by shared fixtures (multi-specialist 9/10 finding). - probe honors the plan's bounded-read decision: 256KB prefix, extraction semantics mirrored from parseTranscriptJsonl so probe/prepare can never diverge on the same file (>1MB transcript test). - policy normalize parity: bash normalize() now matches canonicalizeRemote on .git/-trailing and uppercase-.GIT shapes (7-shape corpus pinned two ways) — a deny for those shapes could previously slip the transcript gate. - session-update reclaim is TOCTOU-safe (atomic mv-aside on both branches). - settings-hook: unparseable settings.json errors instead of being replaced with {}; ensure-event keys on (event, source) so matcher changes update in place — never zero or two registrations. - dot-only slug guard at both parse sites (hostile 'url = ..' can't escape projects/); enqueue tmp-file janitor (1h TTL, inside the drain lock); brain-sync .migrating never clobbered; drop-queue/status count .migrating; snapshot -o warning correct + surfaced in diff mode; version-bump test order-dependence removed; uninstall clears the advance stamp. Deferred with record: slug heal-probe cost sentinel (P3 TODO), FF_OK conflation (noted, misdiagnosis-only). 270 pass / 0 fail across the 10 touched suites. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: adversarial round — the P0 finalize fail-safe and 12 hardened findings Three adversarial passes (Claude fresh-context, Codex chaos, Codex structured with P1 gate) on the full wave diff. Multi-source findings, all fixed: - P0: finalize_queue is now explicit-delete-only — a record is unlinked ONLY when classification proves it staged or dropped; a classifier crash, a missing class file, or a malformed pulled .brain-privacy-map.json (which previously nuked the whole snapshotted queue, remotely triggerable) now retains everything, warns, and re-drains next run. load_privacy_map treats corrupt maps as retain-all, never as empty. - next-version cannot silently drop a live claim: unreadable advertised refs get a targeted --depth=1 fetch + retry; still-unreadable claims surface as UNKNOWN warnings instead of duplicate-version silence. - session-update lock: ownership-checked EXIT trap (a TTL-reclaimed holder can no longer delete the new holder's lock) + a 5-min background heartbeat so a legitimately-slow pull/setup is never reclaimed while alive. - ensure-event collapses ALL same-(event,source) duplicates to one canonical entry; unique per-process tmp path; setup call sites surface (not swallow) the hardened refusals. - memory-ingest: --limit counts only policy-permitted pages (denied records no longer starve permitted ones); --probe applies the same policy filter as --bulk (skipped_policy_* fields on the report). - version-bump repair accepts a genuine literal 0.0.0.0 VERSION file. - slug heal restricted to the stray-.git shape — package.json-anchored wrapper roots keep their legit sticky identity (#2212 preserved). - brain-sync: idle fast path sees leftover .migrating records; unparseable spool records quarantine instead of warning forever; migration comment stops overclaiming the transition-window race. - CDP throttling justifications document override persistence (callers own restoration), pinned in the allowlist test. Deferred with record: deny retroactivity for already-ingested pages (P2 TODO, same semantics as the code-import gate); legacy-migration tail race (transition-window, requires pre-spool writers). 288 pass / 0 fail across the 10 touched suites. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: regenerate SKILL.md docs + goldens (Windows-separator jq fix) Pure generator output for the brain-sync block's jq ancestor match now accepting backslash-formed Windows project keys — previously project-scoped brains were invisible on Windows while the TS scope resolvers saw them. Golden ship fixtures refreshed per the documented procedure. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: codex verify-pass residuals — chunked cwd read, post-filter partial count, migrating depth The verify re-review passed the P1 gate (0 P1s) and left three residuals, all applied: transcriptCwdFromPrefix reads in chunks until one complete record (4MB cap) so a giant first prompt can't truncate mid-JSON and break probe/bulk parity; partial_pages derives from the FINAL prepared set instead of the whole scanned corpus; the preamble queue-depth line counts leftover .brain-queue.jsonl.migrating records like the status path does (regen + goldens included). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: bump version and changelog (v1.68.0.0) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: update project documentation for v1.68.0.0 BROWSER.md: fix the $B cdp example (positional JSON params, not --json; depth is the real CDP param) and add the new perf-throttling examples (Emulation.setCPUThrottlingRate, Network.emulateNetworkConditions) with their clear-override counterparts. USING_GBRAIN_WITH_GSTACK.md: the state-files table row for the sync queue now names the maildir-style spool dir .brain-queue.d/ that replaced .brain-queue.jsonl this release. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: align memory-pipeline probe pins with the #2394 stage-count contract The paid-tier E2E pinned the pre-fix contract (probe headline = raw discovered). Probe now counts post-attribution — the same gate --bulk uses — with an explicit unattributed-skip line. Adds the --include-unattributed companion pin so all 9 fixtures stay accounted for. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(next-version): batch missing-tip fetches — one bounded round trip, never a per-branch crawl The targeted-fetch retry for branches whose advertised tip has no local object ran ONE git fetch per branch (10s cap each). On a shallow clone against a busy remote that crawls the network for minutes — CI's shard deadline killed the free suite mid-file. Missing tips now collect into a single batched shallow fetch (15s cap); refs still missing after the batch (one unservable ref fails the whole transfer) get a capped per-branch retry, and anything past the cap warns as an UNKNOWN claim instead of fetching. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(next-version): pin the batched fetch + make the offline-contract tests hermetic Two new G2 pins: N unfetched claim branches resolve with exactly ONE fetch spawn (PATH-shimmed git counts invocations), and one unservable ref no longer poisons the batch — live claims resolve via the bounded retry while only the ghost warns UNKNOWN. The #2545 offline-contract tests now run the CLI in a local fixture repo instead of the repo's own checkout: the checkout path did a live ls-remote against the real origin (operator-network-dependent, and the CI shard-deadline hang). The online-contract test gains a succeeding gh stub, so fallback:null is asserted deterministically instead of only when the operator happens to be authed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(redact-cli): derive the synthetic AWS-key fixture — no contiguous credential literal in source The CI quality gate scans every ADDED diff line with the redact engine, so the #2610 port's raw fixture literals failed the very gate they exist to test. The fixture is now assembled at runtime; the scanner still receives the identical bytes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(next-version): pin the fixture's host via origin-URL sniff — kills the last environment dependence The hermetic offline-contract fixture had no origin remote, so detectHost() fell through to auth probes: a machine with glab authed passed via the gitlab path while a bare CI runner read host:unknown (offline stays false there) and failed. The fixture now pushes to a local bare origin at a path containing github.com — the URL sniff pins host:github identically everywhere, asserted explicitly in both tests, with every git call still local. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: y$un_ <forrest.sun527@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: benjamin beres <benjamin.beres@bienpreter.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Ricky <ricky@kinokostudio.com.hk> Co-authored-by: Connex Client Access <paul@paulkortman.com> Co-authored-by: henbima <henbima@gmail.com>
garrytan
added a commit
that referenced
this pull request
Aug 29, 2026
…m 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 3263fff vs this render, same harness as auq-verbose-vs-carved-ab): POST 7/7 format elements, substance 5 — identical to PRE. No degradation; the load-bearing-repetition hypothesis did not hold for these duplicates. Net: -236 bytes per tier-2+ skill (~9.7KB corpus). Golden ship baselines re-captured for the deliberate change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(evals): with-skill vs without-skill arm benchmark — measures whether gstack's behavioral layer earns its tokens Ponytail's honest-benchmark method pointed at gstack itself: 3 build-shaped tasks (native-platform over-build trap, CRUD endpoint, bug fix with planted decoys) x 2 arms, real claude -p sessions, scored on the git diff left behind. A research instrument, not a release gate — no assertion compares arm scores. Arms use the PROVEN project-scope pattern: the with-arm installs a build-discipline skill (extracted reuse-ladder + bounded-closer content, not whole-file copies) into the fixture's .claude/skills/ with a CLAUDE.md routing line and an explicit invocation; a live spike confirmed claude -p discovers and invokes project-scope skills via the Skill tool (3 turns, exact-output probe). Fixtures are git init + local bare origin; diff capture is three lines of git, no worktree machinery. Failure taxonomy: zero-diff arms are VALID scored cells (deterministic 0/none, no API call), harvest failures record harvest:null, judge_error cells are excluded from aggregates but named in the report — nothing drops silently. armJudge: fixed sonnet judge, 0-3 unrequested-structure rubric, must name the construct or say none, bounded retry-on-malformed; callJudge gains optional temperature/max_tokens (defaults unchanged). recordE2E now populates tokens_used for every E2E. Eval schema v2: harvest gains {insertions, deletions, net}, tolerant reads keep v1 runs comparable. Registered periodic in E2E_TIERS + touchfiles (with the auq-repetition-cut A/B); periodic detach timeout raised to the new shard-census floor. Free selftest (8 tests, zero API) pins fixtures, extraction, arm asymmetry, diff capture, judge plumbing, and the retry bound. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: absorb the ponytail-import wave into the guard fixtures — ceilings, schema pin, triad phrasing Skeleton ceilings re-captured for the 17 carved skills the wave deliberately grew (reuse ladder + bounded closer + shortcut trail, net of the gated -236B AUQ cut), each with its measured size in the comment per the carve-guards protocol. eval-store schema pin updated to v2 (harvest gains insertions/deletions/net). The AUQ prose-triad keeps its pinned per-choice phrasing ('explicit on EACH choice') while still deferring the score scale to the canonical Format rule — the shipped cut is strictly closer to the pre-cut text than the render that already passed the NOT-WORSE gate. Autoplan carve anchors follow the Phase 2.5 renumbering. Golden ship baselines re-captured. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: observability partial-file pin follows eval-store schema v2 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(test-runner): GSTACK_FREE_JOBS + opt-in flaky-retry pass for syscall-supervised sandboxes GSTACK_FREE_JOBS overrides the computed shard count (the free runner's analogue of the paid runner's EVALS_JOBS). On Vercel sandboxes, PID 1 installs a seccomp filter whose supervisor spuriously fails access(2) for busy processes — measured: 200/200 git-init probes fail 'Cannot access work tree: Permission denied' while the suite runs at 6 shards, 0/200 idle; statx succeeds while access fails on the same path in the same process. One serial mega-shard maximizes per-process pressure and fails too; 2 shards is the measured sweet spot. GSTACK_FREE_RETRY_FLAKY=1 (default OFF — dev boxes should see flakes) re-runs attributed failures once, serially, capped at 5 files; a clean retry downgrades to a loud FLAKY-PASS naming the offenders, a repeat failure stays red, timeouts and unattributed failures never retry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(browse): portable temp paths — TEMP_DIRS allowlist, tmpdir()-based test files Local path validation now accepts os.tmpdir() alongside the classic /tmp (new TEMP_DIRS in platform.ts): on macOS os.tmpdir() is /var/folders/..., and TMPDIR-honoring CI/sandbox environments point it elsewhere entirely — both are legitimate scratch space. Remote file serving (TEMP_ONLY) stays pinned to TEMP_DIR alone; no change to the exfil boundary. commands.test.ts drops 41 hardcoded /tmp literals for a tmpp() helper on os.tmpdir() (two message assertions now reference the same variable), and path-validation's symlink-escape test targets /etc/hosts instead of /etc/crontab — the target must EXIST for realpath to resolve the link (a dangling target falls back to the link's own path and passes vacuously), and /etc/crontab is absent on Amazon Linux. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(config): portable sha256 — Linux ships sha256sum, not shasum resolve-user-slug and endpoint hashing exited 127 on Amazon Linux (shasum is a macOS/perl tool). New _sha256_hex helper prefers sha256sum and falls back to shasum, matching gstack-verify-gate's existing pattern; both call sites converted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(next-version): only trust ls-remote when origin is actually configured Without the guard, git DWIMs the literal 'origin' as an ssh host/path; on hosts whose transport launders exit codes the probe 'succeeds' with zero branches and the allocator silently sees an empty queue — the exact duplicate-allocation failure (#2545) fetchGitClaimed exists to prevent. git remote get-url origin gates the probe; absence falls through to the existing local-refs path with its staleness warning. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(testing): sandbox-doctor — one command makes a cloud sandbox run the suite green Measured failure taxonomy for Vercel/Conductor sandboxes (missing /dev/fd, 64M /dev/shm, seccomp-supervisor access(2) EACCES under load, uid-1000 processes with FULL capabilities defeating chmod-denial tests, no X server, no git identity, Conductor git-shim exit-code laundering) plus the idempotent script that treats all of it and seeds the run recipe. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(config): converge on main's self-contained sha8_of — its tests extract the function standalone The merge kept a branch-local _sha256_hex helper; main's v1.72 landed the same portability fix inline WITH tests that extract sha8_of()'s text and run it under a shim-only PATH — a helper call can't satisfy that shape. Adopt the landed implementation at both hash sites. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: coverage for GSTACK_FREE_JOBS override and failingFiles attribution Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: coverage for TEMP_DIRS widening and remote-serving TEMP_ONLY asymmetry Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: coverage for gstack-shortcut marker grammar and retro harvest joint Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: coverage for sandbox-doctor shell syntax and idempotency guards Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test-runner): empty-shard outcome carries failingFiles; harden flaky-retry list The empty-shard early return omitted the (required) failingFiles field — tsc TS2741 — feeding undefined into the flaky-retry flatMap. Also drop the dead 'else if (worst !== 0)' guard (the enclosing if already pins it). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(release): version-bump write regenerates the version-stamped agents digest agents-digest/gstack-AGENTS.md embeds VERSION in its first line and is byte-freshness-gated (test/agents-digest.test.ts + Skill Docs Freshness CI), but nothing in the release path regenerated it — every version-bumping ship of this repo would land red. write now spawns the repo's own generator when present (agentsDigest true/false/null in the output JSON), and ship's evidence gate allow-lists the digest alongside VERSION/package.json. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(setup): instruction-tier explainer prints the script-anchored digest path $(pwd) printed a nonexistent path when setup ran from any other directory; both arms now share one print_instruction_tier() using SOURCE_GSTACK_DIR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(digest): broaden AGENTS.md writer tripwire; pin digest-resolver ladder lockstep The print-path-only guard now catches tee/install/rsync/dd/truncate, >> appends, and laundered variable-destination writes. New test ties the digest's hand-rendered reuse-ladder text to the preamble resolver so an edit to either fails CI instead of shipping drift. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(retro): shortcut harvest drops placeholder markers and convention docs The Step 11.5 grep matched documentation mentions (dec-<id>, dec-*) in checklists, resolver sources, and convention tests, reporting phantom debt rows on gstack itself. A trailing filter kills placeholder forms; prose tells the agent to discard convention-quoting hits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(review): advisory findings count in per-specialist stats Without this, simplification (all-advisory by construction) would log findings:0 every run and auto-gate itself into permanent silence after 10 dispatches. The advisory carve-out governs score and header only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(evals): arm-benchmark harvest and judge hardening - Harvest diffs against the recorded seed SHA (origin/main is movable by an agent that commits AND pushes; a recorded SHA is not). - Fixtures get a node_modules .gitignore and the git wrapper a 64MB maxBuffer, so a vendored-dependency arm is scored instead of killing the cell. - The judge diff cap is a named constant with loud truncation (log + judge_reasoning suffix). - Judge prompt block markers carry a per-call random sentinel, so a diff containing a faked closing marker cannot escape the data block. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(evals): AUQ A/B vendored pre-cut arm + judge-error inconclusive taxonomy - The PRE arm read a branch-local SHA (3263fff) that becomes unreachable on fresh clones after the squash-merge; the pre-cut render is now a vendored fixture. - A judge failure on one side no longer coerces substance to 0 (which fabricated DEGRADATION on POST-side failures and masked regressions on PRE-side failures): null substance = inconclusive, format still gates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: regression pin for the originConfigured guard vs laundering git shims On healthy hosts the guarded and unguarded paths behave identically, so a revert passes the suite; only a shim that makes 'git ls-remote' exit 0 with empty output (the Conductor wrapper's observed behavior) exposes it. Pins that the empty 'successful' probe is never trusted as an empty queue. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(sandbox-doctor): missing /dev/shm no longer aborts the doctor under set -eu Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(touchfiles): close dep-list gaps for the new evals - arm-benchmark entries gain ship/SKILL.md (buildBehavioralSkill extracts sections from the rendered ship skill) - review-army-simplification entries gain their planted fixtures + test file - auq-repetition-cut-ab gains llm-judge.ts and the vendored PRE fixture Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: re-capture context-budget fixture — lock the WS6-3 reduction and Step 9 deltas Per the ratchet protocol: the AUQ repetition cut shrank per-skill eager tokens but the fixture was never re-captured, leaving the win unlocked. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(release): digest regen is an explicit --regen-digest opt-in, not presence-sniffed code exec Review (security) caught the cycle-1 fix executing any repo's scripts/gen-agents-digest.ts on plain 'write' — arbitrary code exec from a hostile clone on a routine bump, contradicting the binary's own containment posture. The regen still runs the TARGET repo's generator (a 'trusted' copy beside the binary would false-red the freshness gate on version drift), but only under the flag: /ship passes it deliberately, in a repo whose code the operator already executes (its test suite). Plain write is side-effect-free again. Also: uniform output shape (agentsDigest: null on the JSON-manifest branch), a REAL generator round-trip test replacing the misnamed lockstep check, and land-and-deploy's evidence gate gets the same digest allow-path as ship so the two grading surfaces agree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test-runner): flaky-retry vetoes on ANY unattributable failure evidence The gate equated 'some failure attributed' with 'all failures attributed': a shard with one attributed failure plus a headerless failure, an unhandled error between tests, or a truncated run (no terminal summary) qualified for retry — re-running only failingFiles and masking the rest as FLAKY-PASS, re-opening the silent-truncation hole the strict classifier closes. FreeShardOutcome now carries unattributedFailures; nonzero vetoes the retry. Pins: mixed shard, truncated-with-attributed shard, empty-shard field values. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(next-version): a configured origin advertising zero heads is never trusted The originConfigured guard covered only the no-origin laundering case. With origin configured (the normal Conductor worktree state), the laundering shim makes a failed ls-remote exit 0 with empty stdout — read as 'the queue is empty', the exact duplicate-allocation bug (#2545) one layer up. A reachable remote always advertises at least its default branch, so an exit-0 zero-head probe now falls back to local refs/remotes/origin with a laundering-specific warning. Regression test shims git for both configurations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(sandbox-doctor): loud on git-shim patch drift; document the retry-contract override - The /conductor/bin/git patch was a silent no-op if the shim's bytes drift from the exact pattern — now warns that laundering is NOT fixed. - The bashrc block documents why GSTACK_FREE_RETRY_FLAKY=1 deliberately overrides the runner's default-OFF contract on this sandbox, and how to undo it. - Test pins the guarded shm form (missing /dev/shm must not abort set -eu). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(digest): pin the script-anchored explainer path; catch declaration-prefixed writers - Asserts $SOURCE_GSTACK_DIR/agents-digest path and forbids $(pwd)/agents-digest (the cycle-1 fix was revertible without failing anything). - The laundered-assignment arm now matches local/export/declare/readonly/typeset prefixed assignments — the likeliest in-function writer shape in setup. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(evals): arm-benchmark selftest runs FREE on every PR The selftest lived inside the paid skill-e2e-* file, so fixture-integrity and plumbing pins executed weekly at best — a broken fixture would ship past every gating check and be discovered when the periodic run burned money on a dead instrument. Harness extracted to test/helpers/arm-benchmark-harness.ts, selftest to test/arm-benchmark-selftest.test.ts (free suite). Touchfiles: harness added to the three benchmark dep lists; the auq-repetition-cut-ab tier comment now states the MANUAL re-run obligation honestly (periodic runs force EVALS_ALL, so dep lists cannot auto-trigger it). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: re-capture context-budget fixture after cycle-2 template deltas Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(sandbox-doctor): keep both heredoc bodies under the 512B pipe-deadlock window The cycle-2 additions pushed the python-patch and bashrc heredocs into the 512-65536B window test/heredoc-pipe-deadlock.test.ts guards (sh scripts get no BASH_COMPAT escape hatch). Same content, tighter prose; the drift warning now reuses the patch pattern variable instead of a second literal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(review): a gstack-shortcut marker only suppresses findings when its decision id resolves in the ledger Cross-model catch (Claude adversarial + Codex agreed): any diff author could fabricate a marker and silence Completeness review of that gap. Reviewers now resolve the dec-id via gstack-decision-search; an orphan marker is reported as a forged suppression, not honored as debt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(autoplan): define the B2 gate path — accepted premise challenges amend the plan and re-run Eng The final gate offered B2 (respond to User Challenges) but the option handler table omitted it, leaving accepted challenges with no amendment or Eng re-review path. B2 now walks challenges one at a time; an accepted one amends the plan and re-runs Eng (the gate always reviews the final plan), sharing D's 3-cycle cap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(evals): arm benchmark runs each fixture's functional oracle — correctness before LOC The plan's metric order is diff-quality FIRST, but cells never ran the fixtures' own run-tests.js, so a refusal, a broken implementation, and working code were indistinguishable in aggregates (Codex adversarial catch). Tasks with an oracle declare checkCmd; every cell records checks=pass|fail|none in the report line and eval store. Selftest pins the oracle declarations and that the planted bug fails its own check pre-fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ship): check the bump's agentsDigest result; state the --regen-digest trust envelope honestly A failed digest regen warned and moved on — ship now instructs re-running the generator and staging the digest with the bump (the freshness check stays red otherwise). The 'no-op everywhere else' phrasing oversold safety: the step now names what executes and why that is inside the envelope Step 5 already opened (the repo's own test suite). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test-runner): GSTACK_FREE_JOBS accepts digits only — parseInt truncation defeated the loud-failure contract '2abc' silently became 2 and '3.7' became 3 despite the error text claiming a positive-integer requirement. Strict /^\d+$/ pre-check; both shapes pinned. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(sandbox-doctor): atomic git-shim patch, :99-socket Xvfb check, dnf gate, non-interactive sudo - The /conductor/bin/git patch writes tmp-then-rename with a .orig backup — a concurrently spawned git can never exec a truncated shim. - Xvfb running-check looks for the :99 socket, not any-display pgrep. - Xvfb install is dnf-gated so non-dnf distros degrade to a warning instead of aborting the remaining fixes under set -eu. - The bashrc /dev/fd restore uses sudo -n || true — no password prompt at every shell start on non-passwordless machines. - BASH_COMPAT=50 keeps heredoc bodies off the bash pipe window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(build): a failed agents-digest regen fails gen-skill-docs instead of deferring the red to CI Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(browse): an untrustable TMPDIR (/, $HOME, a cwd ancestor) never widens the local allowlist TEMP_DIRS honors os.tmpdir() at daemon start; a daemon launched with TMPDIR=/ would have trusted the whole filesystem for local path validation for its lifetime. Subprocess pins cover /, $HOME, cwd-ancestor rejection and that a benign distinct TMPDIR (the sandbox recipe's $HOME/tmp) stays honored. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: zero-heads warning names the benign cause too; digest path declaration made load-bearing; ratchet re-capture - The ls-remote zero-heads warning no longer accuses an empty remote of running a laundering shim. - instructionTier.rulesFile now must equal the generator's DIGEST_RELPATH (and setup must print it) — the declaration fails with the real path instead of lying silently. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: file ship-time follow-ups in TODOS skillify HOME-override gate red (pre-existing, proven on main), the auq-verbose-vs-carved-ab branch-local ref, eval-store harvest union, evidence digest allow-path scoping, and the WS6-2 dead-frontmatter live-host verification deferral. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * v1.73.0.0 chore: version bump + CHANGELOG — ponytail import wave Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: raise ship skeleton parity ceiling — measured 75,592 after the v1.73 release-step prose The --regen-digest trust-envelope paragraph (Step 12) and the evidence-gate digest note (Step 16) grew the ship skeleton past the previous 75,420 ceiling. Re-measured per the deliberate-change protocol. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: update project documentation for v1.73.0.0 - README.md, docs/skills.md, AGENTS.md: /autoplan phase order corrected to CEO → design → DX → eng (eng always last); /review rows note the advisory simplification lens - docs/PROJECT_STRUCTURE.md: add agents-digest/, gen-agents-digest.ts, sandbox-doctor.sh, test-free-shards.ts to the annotated tree - CONTRIBUTING.md: document GSTACK_FREE_JOBS, GSTACK_FREE_RETRY_FLAKY, and the sandbox-doctor one-command fixer in the Tier 1 test section Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: apply cross-model doc-review fixes for v1.73.0.0 - README.md: host table gains the OpenClaw explainer arm row (setup has the arm; the table claimed to match setup) - docs/skills.md: /review completeness-gaps section documents the gstack-shortcut(dec-<id>) acknowledged-debt suppression and orphan-marker flagging; /autoplan deep-dive states the recommended-option default with the 6 principles as tie-breakers - CONTRIBUTING.md: host count 8 -> 10 (Hermes, GBrain), supported-hosts list completed - docs/TESTING_INTERNALS.md: sandbox recipe says to source ~/.bashrc after the doctor seeds it; GSTACK_FREE_JOBS wording fixed from "caps" to "overrides in either direction" (matches the un-clamped runner) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): temp-dirs asymmetry pins are topology-aware; TMPDIR probes are POSIX-only CI exposed two wrong assumptions in the new temp-dirs tests, neither a product bug: - The remote-serving asymmetry test assumed a distinct os.tmpdir() lies OUTSIDE TEMP_DIR, but the free-shard runner nests each child's TMPDIR inside /tmp on CI — a file there is under TEMP_DIR, so serving it remotely is legitimate. The test now pins the actual exfil boundary on every topology (a cwd project file is locally readable, never remotely servable) and branches the os.tmpdir() case on nested-vs-outside. Reproduced locally with TMPDIR=/tmp/nested-tmp before fixing. - The untrustable-TMPDIR subprocess probes set TMPDIR, which Windows os.tmpdir() ignores (reads TEMP/TMP) — and on Windows TEMP_DIR is DEFINED as os.tmpdir(), so the fixed+movable two-dir topology the guard filters does not exist there. Probes now skip on Windows with that rationale; the benign-TMPDIR assertion compares realpaths. Verified under all three POSIX topologies: TMPDIR=$HOME/tmp (outside), TMPDIR=/tmp/nested-tmp (CI shard shape), TMPDIR unset (identical). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(build): DIGEST_RELPATH is a forward-slash literal on every platform path.join built it with backslashes on Windows, so the wiring test's string comparisons against setup and hosts/*.ts (which carry the forward-slash literal) could never match there — windows-free-tests red. path.join(root, DIGEST_RELPATH) at the write site normalizes fine. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(sandbox-doctor): bashrc block re-heals the /dev/shm remount on sandbox restart The 4G remount does not survive restarts; a reverted 64M shm made the multi-tab browse handoff test fail consistently under suite concurrency (observed live: two consecutive full-run failures, green in isolation, green again after remounting). Same guarded arithmetic as the doctor body. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): close the cross-shard porcelain race that failed Windows CI Two-part fix for the gen-skill-docs-out-dir isolation-pin failure: - cookie-import-browser built its scratch cookie DBs inside the TRACKED browse/test/fixtures/ dir (created in beforeAll, deleted in afterAll), so they flash as untracked files mid-run — a concurrent shard's porcelain snapshot caught the window on Windows. The DBs now live in a per-run tmpdir; zero source-tree writes. - gen-skill-docs-out-dir is the free suite's only LIVE porcelain-snapshot test, so it joins TREE_MUTATING (the serial quiet window): any concurrent transient tree-write can race it, and its own spawned render rewrites llms.txt/agents-digest in place (idempotent on a fresh tree). The race is pre-existing; this branch's +5 test files reshuffled shard composition and exposed it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * v1.75.0.0 chore: queue-advance rebump — perth-v2 landed v1.74.0.0 on main The v1.73.0.0 slot this branch claimed was superseded when #2721 merged; same MINOR level relative to main per the versioning invariant. CHANGELOG entry renumbered (1.73.0.0 was branch-internal and never landed on main), digest restamped via --regen-digest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test-runner): duration-packed walls keep the per-file floor — predictions don't transfer across machines The committed duration seed is recorded on fast CI; a syscall-supervised sandbox replays the same files 2-4x slower. Observed post-merge: a 253-file shard predicted ~242s was wall-killed at its predicted-x3 725s wall while genuinely progressing (the old count heuristic guaranteed 1265s). Packed walls may be looser than the count floor, never tighter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
gstack-next-versionsetoffline: truewhenevergh pr listfailed and returned an empty claim set./ship's Step 12 then said "fall back to localBUMP_LEVELarithmetic" — and local arithmetic cannot see a sibling's claim, so the fallback handed out a version another open PR already held.Git already knows what the API was being asked for, so
offlinenow degrades the queue view, not the allocation.Before / after
Fixture: base
0.1.66.0, a sibling branch holding0.1.67.0,ghfailing.0.1.67.0— collides with the sibling0.1.68.0— bumps past itWhat it reads when the host is down
VERSIONfile — those are the open PRs' branches, whether or not the API can enumerate them.VERSIONfile cannot see because it only holds the newest value. Bounded at 400 commits, and it warns when it truncates rather than implying it read all history.New output field
fallback: "git" | nullso/shipcan distinguish a git-derived pick from a host-derived one. Additive — the fallback only runs when the host query already returned nothing, so the online path is untouched (verified:fallback: null, same version as before).Why this mattered
Found while auditing a downstream repo that had four duplicate version pairs in
main—v0.1.6.0,v0.1.9.0,v0.1.32.0,v0.1.57.0— spanning three weeks. The most recent was reproduced exactly:gh pr listfailed mid-ship, the fallback allocated a number an open PR already held, and both merged. Two commits now claim one version there, so a version read off a running container no longer maps to a single commit.Test coverage
test/gstack-next-version.test.ts: 34 pass (28 pre-existing + 6 new).New tests cover: a sibling branch's claim found via remote-tracking refs; the pick landing past that sibling (
0.1.68.0, not0.1.67.0); the shipped-subject scan; graceful degradation outside a git repo (warning, never a throw); and thefallbackoutput contract in both directions ("git"whenghfails via a stub,nullwhen online).Adjacent suites unaffected:
gstack-version-bump+ship-version-sync35 pass,skill-validation+parity-suite+parity-sectioned340 pass (13/13 skills parity).Self-review finding, fixed before commit
A comment claimed the 400-commit scan "says so" when it truncates. It didn't — the only warning in that block was the
git logfailure path, so a capped scan was silent. Made the comment true (warns when the log comes back exactly full), then verified by running a copy with the cap lowered to 5 against this repo's 118 commits: warning fires. At the real cap it correctly stays silent here.Not included, deliberately
/ship's Step 12 instruction still tells the agent to use local arithmetic. That edit lives inship/SKILL.md.tmpl, which currently also carries unrelated in-flight work in my working tree (the--package-json-pathmanifest resolution), and its regenerated artifacts collide withd078622. Rather than bundle someone else's unreviewed change, this PR ships the allocator only. The allocator is safe on its own — a git-derived pick is collision-safe whether or not the prose has caught up; the instruction just understates it. Happy to follow up with the prose change as a separate PR.Test plan
bun test gstack-next-version— 34 passbun run bin/gstack-next-version --base main --bump patch --current-version <v>— online:fallback: nullghonPATH—offline: true,fallback: "git", version still allocated, warning explains the degraded view