Repository navigation
v1.84.1.0 sync: upstream v1.84.1.0 (fork was v1.68.3.0) - #3
sfxconwurah wants to merge 21 commits into
Conversation
…orbed, tracker closed with receipts (garrytan#2666) * test(wireup): make gbrain-missing PATH fixture hermetic The gbrain-missing test appended the host PATH (and a hardcoded /opt/homebrew/bin) to the fixture PATH, so on any machine with a real gbrain installed the 'missing' case saw it, exited 0 instead of 2, and could never fail where the bug exists — a false green for a whole machine class. The fixture now keeps only root-owned OS dirs on the child PATH, and a new determinism check plants a host-like gbrain to prove it is unreachable. Absorbed from PR garrytan#2615 with authorship preserved; the PR-thread liveness screenshot (docs/images/gstack-pr-liveness-2255.png) is dropped — referenced by nothing in the tree. Fixes garrytan#2255 Co-authored-by: CommandCodeBot <noreply@commandcode.ai> * fix(evidence): stop bun's dotenv autoload from reaching the spawned command `bin/gstack-evidence` has a `#!/usr/bin/env bun` shebang, and bun AUTO-LOADS `.env`, `.env.<NODE_ENV>` and `.env.local` from the cwd into `process.env`. The wrapper then spawned the command with no `env` override, so every command run through it inherited those variables — and a repo `.env.local` routinely holds production credentials. Two things go wrong, and the second is worse than the leak: 1. Secrets reach a child that would not otherwise have them. `npm test` run by hand in the same shell sees none of them; the same command through the wrapper sees all of them. 2. THE COMMAND UNDER TEST BEHAVES DIFFERENTLY, so the ledger certifies a run that is not the run CI performs. Observed in a Next.js repo on 2026-08-20: four tests failed 4/4 through the wrapper and passed 5/5 without it, because app code branched on env vars only the wrapper supplied. Nearly an hour went into chasing a "flake" that was the measuring instrument. The wrapper exists to record trustworthy evidence, so silently altering the environment defeats its purpose. The fix builds the child env from `process.env` minus the keys bun injected, and detection is exact rather than heuristic: verified on bun 1.3.11, a dotenv file does NOT override a variable the shell already exported (the shell's value wins). So a key whose live value equals the dotenv file's value was injected by bun, and dropping it restores the environment the user's own shell would have given the command. A key whose live value differs is genuinely the caller's and survives. `BUN_DOTENV_FILES()` mirrors bun's precedence, including that `.env.local` is skipped when NODE_ENV is "test" — scrubbing a key bun never loaded would strip a variable the caller legitimately provided. Escape hatch: GSTACK_EVIDENCE_KEEP_DOTENV=1 keeps the old behaviour. When keys are scrubbed the wrapper warns with the KEY NAMES ONLY, so the diagnostic cannot become the leak it prevents. Tests: 6 cases, mutation-verified — removing `env: spawnEnv` reddens exactly the two leak tests and restoring it gives 30/30. Every leak test asserts the scrub warning fired, because `bun test` sets NODE_ENV=test and the first version of these tests passed vacuously against a `.env.local` bun had never loaded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Absorbed from PR garrytan#2652 with authorship preserved. Wave additions: a doc-comment on the ${VAR}-expansion limitation (bun expands refs, the reader compares raw text — those keys are left in the child env, failing open) and a regression pin for the unreadable-.env fail-open path with a functional DAC-override skip guard. Fixes garrytan#2624 * fix(setup): reap dangling skill dirs when the payload is gone cleanup_old_claude_symlinks derived its work list from the payload directory, so when the payload was gone — precisely when orphans exist — the glob matched nothing and the loop never ran; the -f guard also followed symlinks, hiding dangling SKILL.md links even with a payload present. The cleanup now scans the DESTINATION skills dir (-e/-L, so dangling symlinks are visible) and anchors SKILL.md provenance to path segments (gstack/*, */gstack/*, */.gstack/render/claude/*) instead of a bare *gstack* substring that would eat a user skill under ~/tools/gstack-fork/. The Windows real-file arm stays payload-gated: a real file has no provable owner. Absorbed from PR garrytan#2634 (2 commits squashed) with authorship preserved. The symmetric cleanup_prefixed_claude_symlinks hole is filed as a TODOS.md residual in this wave. Fixes garrytan#2204 * fix(redact): tolerate EEXIST from recursive mkdir in install-prepush-hook on bun/Windows (garrytan#2635) fs.mkdirSync(dir, { recursive: true }) is a no-op on an existing directory in Node, but bun on Windows throws EEXIST - crashing hook install on any repo whose .git/hooks already existed, leaving the repo unprotected. Add lib/fs-utils.ts mkdirpSync: swallow EEXIST only when statSync confirms the path is an existing directory; a regular file occupying the path, a stat failure, or any other errno still rethrows. Use it in installPrepushHook(). The regression test emulates the Windows bun fs semantics via a bun --preload fixture, so the exact crash path runs (and fails on the old code) on any platform, including CI Linux. Absorbed from PR garrytan#2641 with authorship preserved. Fixes garrytan#2635 * fix(bin): route remaining Windows-reachable mkdirSync sites through mkdirpSync Sweep follow-up to garrytan#2641's lib/fs-utils.ts helper: bun on Windows throws EEXIST from a recursive mkdir on an existing dir, so every unguarded recursive mkdirSync on a Windows-reachable path is a latent crash. Converted: bin/gstack-decision-log (unguarded, runs on every decision log — the second call on any machine hits the pre-existing projects dir), bin/gstack-evidence logsDir + ledger dir sites, and bin/gstack-redact-prepush's skip-log site (already try-wrapped, so its failure mode was a silent skip-log loss rather than a crash — the fix makes the log survive). The ~15 remaining gbrain/mac-lane sites are deliberately left alone. Regression: fs-utils.test.ts drives gstack-decision-log twice, the second run under the bun-Windows EEXIST preload fixture — the pre-sweep code exits 1 with EEXIST there; verified red against v1.68.3.0. * fix(setup-gbrain): warn about the ZeroEntropy sunset before Sept 4 ZeroEntropy was acquired by Notion and sunsets its hosted API on September 4, 2026. A gbrain configured with the zeroentropyai embedding recipe keeps importing pages after that date but embedding silently fails — pages land structurally with no semantic search, this repo's tracker P1 (TODOS.md NEXT PRIORITY). Nothing in gstack ever recommended ZeroEntropy (the dependency is gbrain-internal), so the gstack side is detection + advisory: the wireup helper warns when ~/.gbrain/config.json names the recipe (fail-open grep — a missing, unreadable, or other-provider config stays silent and never blocks a working setup), the setup-gbrain provider-default comments say never to select the legacy recipe for a new brain, and USING_GBRAIN_WITH_GSTACK.md gains a troubleshooting entry. The gbrain-side provider migration stays open upstream. Refs garrytan#2365 * fix(gbrain-source-wireup): first sync targets the registered source, not --repo The wireup registered a federated source by id, then ran 'gbrain sync --repo $WORKTREE' — which resolves against the brain's DEFAULT source and (on gbrain 0.46.x) rewrites that source's local_path anchor to our worktree. Net effect: the user's primary knowledge source silently repointed at the gstack brain worktree while the just-registered source got zero pages, and pages_synced still reported success. The sync now targets the registered id ('gbrain sync --source $id', the same form the repo's own troubleshooting documents). Because the script's stated floor is gbrain >= 0.18.0 and nothing proves --source exists there, support is probed via 'gbrain sync --help' first: an older gbrain keeps the wrong-but-working --repo call with an upgrade warning instead of converting it into a hard failure. The probe sits after the GSTACK_BRAIN_NO_SYNC early-exit and is unreachable in --probe mode. Regression tests (fail on v1.68.3.0): a no-skip sync case asserting the call log shows 'sync --source gstack-brain-<id>' and never 'sync --repo', and an old-gbrain fallback case (fake sync --help without --source) asserting --repo plus the upgrade warning. Fixes garrytan#2662 * fix(setup): --host slate exits informatively instead of silently installing nothing slate passed --host validation (added to the accept-list in v1.64.1.0) but never got a dispatch arm, and the all-INSTALL_*-zero fallback lives inside the auto branch — so './setup --host slate' configured nothing and exited 0, a silent no-op strictly worse than the original hard rejection. slate is now an informational arm (per docs/designs/SLATE_HOST.md it is blocked on the host-config refactor; Slate reads .claude/skills as a compatibility fallback, so the arm points at './setup --host claude'), and a defensive guard after the dispatch chain errors loudly (naming the host, the missing arm, and the valid targets, exit 1) if a future host is ever accepted without being wired. Regression tests (fail on v1.68.3.0): a dispatch-arm ratchet asserting every accept-listed install target has a matching dispatch branch — the exact drift class; a registry cross-check deriving both sides from hosts/index.ts and setup's case arms; a behavioral slate probe (exit 0, points at --host claude, never reaches the installer — on unfixed code it fell through into the installer); and a static pin on the guard's shape. Fixes garrytan#2361 * fix(make-pdf): resolve the sibling browse binary from execPath, not argv[0] In a bun-compiled binary process.argv[0] is the raw invocation string — often relative ('./pdf', 'pdf') — so dirname(argv[0]) yielded '.' and the sibling candidates (../browse/dist/browse etc.) resolved against the CWD instead of the install dir. Resolution was cwd-dependent: correct-by-luck when the fallbacks rescued it, wrong when a cwd-relative path matched. process.execPath is always the absolute binary path. The resolution step takes an injectable selfPath (defaulted) because under bun test the process path is the bun runtime and the compiled-binary shapes are otherwise unreachable. The issue's other half — pdf setup failing on newtab('about:blank') — was already fixed on main in v1.64.0.0 (browse/src/url-validation.ts exact-match allows about:blank; its comment names this exact smoke). This commit closes what remains. Regression tests (the sibling-via-selfPath case fails on v1.68.3.0 — pre-fix code ignores the seam and either resolves the global install or throws): sibling resolution from an install-shaped tree, and a decoy-browse-DIRECTORY case pinning that a directory never wins resolution. Fixes garrytan#2156 * fix(memory-ingest): store the normalized git_remote so unattributed pages hit the policy filter buildTranscriptPage wrote the normalized '_unattributed' sentinel into the page FRONTMATTER but stored the raw resolved remote ('' when unresolvable) on the page object. The policy filter fast-paths !p.git_remote, so under --include-unattributed an explicit '_unattributed → deny' (or read-only) policy never applied to exactly the pages it names — they ingested unpoliced. The stored value now matches the frontmatter. Regression test (fails on v1.68.3.0): seeds the REAL bin/gstack-gbrain-repo-policy store with '_unattributed → deny' through its own set verb, ingests an unresolvable-remote session with --include-unattributed, and asserts nothing reaches gbrain — pre-fix the '' remote bypassed the filter and the import ran. A fake echoing tiers would pass on both sides of the fix; the real helper prints 'none' for unknown keys, so only a genuinely applied deny distinguishes the two. Fixes garrytan#2353 * fix(land-and-deploy): MERGED recovery reconciles and reports remote-branch cleanup Step 4's merge commands carry --delete-branch, and the success path tells the user 'The branch has been cleaned up.' When gh exits non-zero AFTER GitHub already merged (routine in worktree layouts: gh's local cleanup runs git checkout <base> and fails), the §4a-postfail MERGED recovery re-established everything EXCEPT the branch deletion — and said nothing about it, so the discrepancy was invisible. The MERGED path now reconciles: git ls-remote --heads distinguishes branch-already-gone (exit 0, empty → 'already cleaned up', idempotent on re-runs) from branch-survived (offer confirm-first deletion, matching the section's worktree posture; -d not -D for any local branch) from check-itself-failed (non-zero exit → 'couldn't verify', skip the offer — never read a failed check as a clean branch). Template + regenerated SKILL.md + test extensions land in one commit (the md-sync assertion goes red otherwise). Regression assertions (fail on v1.68.3.0: no delete-branch reconciliation existed in test/ at all) pin the ls-remote check, the confirm-first delete, and the absent-vs-failed distinction. Fixes garrytan#2656 * fix(scripts): stop heredoc bodies deadlocking under Homebrew bash `./setup --help` can hang forever on macOS, printing nothing, with no way to tell it apart from a slow install. Eleven scripts carry the same latent hang, `setup` itself being the one every user hits first. bash 5.2+ delivers a heredoc body of 64KiB or less through a pipe: the forked child writes the entire body before exec, and nothing reads the other end until the command starts. Under macOS pipe-KVA pressure the kernel hands a fresh pipe a 512-byte buffer instead of the usual 16-64KiB, so any body of 512 bytes or more blocks write() permanently. The capacity check bash would need to notice (F_GETPIPE_SZ) is Linux-only, so it never fires here. It is pressure-dependent, which is why it reads as "worked on my machine" — the same script runs fine all day and then wedges. Homebrew bash is what `#!/usr/bin/env bash` resolves to on a Mac with brew on PATH, which is most of them. Apple's /bin/bash 3.2 predates the pipe path and is unaffected, so the bug is invisible to anyone testing with the system shell. The fix is `BASH_COMPAT=50` in each affected script, which restores the pre-5.2 tempfile path: $ bash -c 'probe() { [ -p /dev/stdin ] && echo PIPE || echo TEMPFILE; } probe <<EOF $(printf "x%.0s" $(seq 1 1000)) EOF' PIPE $ BASH_COMPAT=50 bash -c '...same...' TEMPFILE - Not a `#!/bin/bash` shebang swap: that pins the script to whatever bash lives at /bin (3.2 on macOS, absent on some Linux distributions) and is bypassed entirely by `bash script.sh` call sites. The variable survives both. - Not exported, so child processes keep their own compat level. - Placed below any `--help` sed range that reads $0, so usage output is unchanged (verified on all eleven). - Every guarded script is bash-3.2-clean — no associative arrays, case conversion, or mapfile — so compat level 50 costs them nothing. test/heredoc-pipe-deadlock.test.ts scans every tracked shell script for a heredoc body in the 512B-64KiB window and fails without the guard, and proves the mechanism at runtime on bash 5.2+ by asserting the body moves from PIPE to TEMPFILE. On older bash the runtime half is skipped, since the pipe path does not exist there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Absorbed from PR garrytan#2640 with authorship preserved. Wave adaptations: the pipe-probe test skips on minimal-/dev environments without /dev/stdin (it would report OTHER for an unobservable fd), and one caveat verified during review: on bash 4.3/4.4 (e.g. Git Bash), assigning BASH_COMPAT=50 prints a non-fatal 'invalid value' warning to stderr — those bashes are already on tempfiles, so the guard is a no-op there; windows-setup-e2e exercises this empirically. * docs: TODOS.md v1.69 wave close-out Move the slate P4 entry and the ZeroEntropy P1's gstack-side half to Completed (v1.69.0.0); reframe the ZeroEntropy NEXT PRIORITY entry around the remaining gbrain-side work; file the wave's four residuals with rationale — the prefixed-cleanup symmetric conversion, the garrytan#2163 legacy-slug checkpoint heal, the invited garrytan#2657 --reconcile contribution, and the table-driven setup host dispatch behind the new cross-check ratchet. * chore: bump version and changelog (v1.69.0.0) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Som Samantray <som.samantray@gmail.com> Co-authored-by: CommandCodeBot <noreply@commandcode.ai> Co-authored-by: Connex Client Access <paul@paulkortman.com> Co-authored-by: y$un_ <forrest.sun527@gmail.com> Co-authored-by: Lockyer <135391289+Lockyer228@users.noreply.github.com> Co-authored-by: Benjamin D. Smith <benjamin.smith@binarysword.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ision point (tripwire + gate E2E) (garrytan#2700) * fix(ship): name the /document-release subagent at every Step 18 decision point The v1.54.0.0 carve moved Step 18 (documentation sync) into ship/sections/pr-body.md and the Claude-host skeleton stopped saying "document-release" anywhere in the workflow body — the dispatch became invisible at exactly the moments an agent decides whether to open the section. Restore visibility at three touchpoints, all subagent-framed (never bare-slash-framed, which would invite an inline Skill invocation that bypasses the fresh-context subagent + JSON contract): - manifest trigger (renders into the section-index row AND the STOP pointer): "dispatching the /document-release subagent to sync docs (Step 18) and then creating or updating the PR/MR (Step 19)" - Step 17 handoff line names Step 18's dispatch explicitly - new hoisted doc-sync invariant beside the PR-title invariant: the dispatch itself is never skipped; only a failed subagent is non-blocking Pin it in carve-guards: 'the /document-release subagent' (all three touchpoints) + 'dispatches the /document-release subagent' (invariant) must stay in the skeleton; the carved imperative 'Dispatch /document-release as a subagent' must stay carved. Skeleton cap 91,600 → 92,300 (measured 91,764; trigger renders twice). Goldens regenerated for all three hosts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: pin the ship→document-release Step 18 wiring with a free tripwire Five substring/structure asserts across the carved section, the Claude skeleton's three touchpoints, the manifest trigger, and the codex/factory goldens (inlined Step 18 ordered before Step 19). Claude-golden asserts deliberately omitted: host-config.test.ts already enforces golden == generated byte-for-byte. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: gate-tier E2E proving /ship dispatches the document-release subagent New skill-e2e-ship-docsync: a live agent gets the sliced Step 17→19 tail of the generated ship skeleton in a bare-remote git fixture (Steps 0-16 "done"), under a fake HOME so the STOP pointer and the Step 18 subagent prompt resolve to planted copies, with a stub document-release skill that returns the empty-result JSON contract. Hard assert: an Agent/Task tool-call matching /document-release/i exists in result.toolCalls and precedes any `gh pr create`. Neutral prompt (no STOP-Read priming, no document-release mention — the prompt echoes into the transcript, so asserts read toolCalls only). Hardening from review: throw-on-marker-drift fixture slice; per-test GSTACK_HOME + .redact-prepush-prompted marker (routes Step 17's credential guard to its silent branch — the hermetic GSTACK_HOME pin defeats a HOME-only override); 480s/540s timeouts (nested subagent adds wall clock the 300s sibling never carried); 'timeout' accepted in exitReason only because the dispatch assert is independently hard; whole-file describeE2ETier('gate') composed with diff selection (keeps the file out of the periodic shard census, which sits at its ceiling, and under the hard tier-alignment invariant). Registered as 'ship-docsync' in E2E_TOUCHFILES + E2E_TIERS (gate) in the same commit — touchfiles.test.ts rejects either half landing first. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: fix stale document-release TODOS entry + three review-deferred items The SHIPPED entry still described the deleted Step 8.5 post-PR cat-delegation design from v0.8.4; replace with the current Step 18 subagent design and its test pins. Add the three P3 items deferred from the v1.69 plan review: dispatch receipt enforcement, land-and-deploy→canary dispatch-pin pattern, and the periodic shard-census boundary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: pre-landing review fixes Testing-specialist findings, all mechanical: (1) pin the E2E fixture's git branch (-b main / init.defaultBranch=main) and assert every setup command's exit status so operator git config can't silently corrupt a paid run; (2) tighten the dispatch matcher to Step 18-prompt-specific markers (document-release/SKILL.md | executing the /document-release workflow) so a subagent merely quoting section text can't false-pass the regression assert (verified against recorded burn-in transcripts); (3) replace the subsumed carve-guards anchor with three non-overlapping per-touchpoint anchors (gerund/imperative/3rd-person) so each touchpoint is independently enforced. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: red-team review fixes Five informational findings: TODOS shard-census arithmetic corrected (census is 67 with one free ungated slot; the SECOND ungated file trips the floor) and version pointer fixed (v0.18.2.0, not v0.18.1.0); the free tripwire now pins the two dispatch-matcher marker strings so a pr-body prompt reword fails the free suite instead of surfacing as a paid-tier mystery; the E2E matcher gains a section-paste exclusion (scaffold strings disqualify) — verified against all recorded runs; the E2E header documents the tierless test:evals invisibility tradeoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: adversarial review fixes Pin the E2E matcher's two EXCLUSION markers in the free tripwire (an unpinned 'Parent processing:' reword would silently deaden the section-paste guard while every test stayed green); add an ordering pin (the hoisted doc-sync invariant must sit above the pr-body STOP pointer — presence-only anchors can't catch drift below it); plant a third cwd-relative pr-body copy inside the fixture repo, gitignored so the agent never tries to commit test scaffolding. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: bump version and changelog (v1.70.1.0) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: CHANGELOG accuracy fixes from the doc-release review Three factual corrections the Step 18 doc subagent caught in the fresh v1.70.1.0 entry: 5 tripwire tests (not 6), cost floor $0.63 per the cited eval store (not $0.59), and the visibility claim scoped to decision points (the re-run checklist mention survived the carve). Plus the E2E header's stale pending-burn-in note replaced with the observed numbers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: raise bun-polyfill subprocess budget to 60s for degraded Windows runners The 50ms-sleep test blew the 20s budget on BOTH bun retry attempts on PR garrytan#2700's windows-latest runner (run 32989821401) — sustained AV/runner pressure, not just the documented cold-start. Same flake passed-on-rerun on the prompt-token-load-reduction branch yesterday. Budget only; every assertion still checks exact output. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ci): run the ship-docsync gate E2E in the evals matrix + silent-skip tripwire The evals.yml matrix is hand-enumerated and the Run step never exported EVALS_TIER, so the new whole-file-gated ship-docsync E2E would have self-skipped even with a row — a hollow green one layer deeper than the documented rehomed-monolith incident. Add the e2e-ship-docsync row with a row-level `tier: gate` property, exported as EVALS_TIER by the Run step (empty = unset for every existing row: all readers are `=== '<tier>'` or truthiness). New free tripwire test/evals-workflow-matrix.test.ts ratchets the class: matrix files must exist; gate-hosting files must have a row; whole-file-gated matrix files must carry a matching row tier; and the burn-down lists enforce their own cleanup. It enumerates the PRE-EXISTING holes found while wiring this (8 gate-hosting files with no row; codex/gemini rows running zero tests; the pty-plan-smoke row hollow since its files adopted describeE2ETier) — tracked in TODOS as the CI gate-lane hollow-coverage burn-down. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…d onboarding, 20 skill carves, CLAUDE.md trim (garrytan#2691) * feat(gen): strip gen-time-only frontmatter keys from Claude renders interactive + benefits-from are read from the .tmpl by buildContext at generation time; no runtime, host, or test reader consumes them from the generated SKILL.md (e2e-harness-audit reads .tmpl; benefits-from tests assert rendered prose). gbrain: stays (bin/gstack-brain-context-load reads it from the installed render); hooks: stays (Claude Code host wires PreToolUse from it). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(gen): regenerate SKILL.md — dead frontmatter keys removed Mechanical regen after hosts/claude.ts stripFields change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(test): context-budget ratchet — CI ceilings on always-on + eager token ledgers New free test grades the two ledgers nothing else guards: the full-frontmatter always-on catalog (aggregate) and per-skill eager tokens (SKILL.md + forced-read refs), via checkBudget from lib/context-bill.ts. Ceilings live in test/fixtures/context-budget.json with x1.05/x1.10 headroom; regenerate with bun test/helpers/capture-context-budget.ts. New skills fail until consciously budgeted; removed skills fail until the fixture is refreshed; reductions ratchet the ceilings down so wins lock in. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(todos): file output-template carve wave + plan-ceo doctrine revisit; mark preamble-carve P3 in flight Two follow-ups deferred from the approved token-reduction program (CEO review 'NOT in scope' list), filed with full context per TODOS format. The existing P3 preamble-carve entry gets a status update pointing at the program that supersedes it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): review findings — Windows path normalization, full totals rebuild, ratchet coverage Pre-landing review (5 specialists) found one critical: the ratchet test runs in the curated Windows lane, where path.relative yields backslash skill names that miss the test/ filter and mismatch every POSIX fixture key. Names are now normalized once in buildRatchetBill (toPosixName) and the fixture filter is tightened to test/fixtures/. All eight Bill.totals fields are rebuilt from the filtered list (no fixture-polluted perInvocation/totalMd numbers for future consumers). New coverage: Windows-separator normalization pins, a captureContextBudget round-trip against tree-a (headroom math exact), a stripFields regression pin (interactive/benefits-from absent from renders, hooks/gbrain preserved), and the ceilings test no longer double-reports stale-fixture entries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): adversarial findings — stable root key, symlink-alias dedupe, fixture-shape guard Adversarial review (Claude subagent) verified the fixture's root-skill key was the capture machine's checkout dirname: any non-gstack-named clone (every Conductor worktree) failed the free suite, and the documented re-run-the-capture recovery baked the local dirname into the committed fixture — silent corruption through the tool's own protocol. The root skill is now pinned to ROOT_SKILL_KEY ('gstack', its frontmatter name). Symlink aliases are realpath-deduped (census precedent): connect-chrome no longer gets its own ceiling, so Windows checkouts that materialize the symlink as a plain file can't fail the stale-ceiling set-equality test. New guards: fixture-shape validation (a string alwaysOnTotal can no longer silently disable the ceiling), a mutation pin that the filter shrinks the always-on ledger vs the raw bill, an alwaysOnTotal violation test (the branch was load-bearing with only under-budget coverage), and an atomic temp+rename fixture write. Fixture regenerated: 59 ceilings, alwaysOnTotal 6344. Deferred with a TODO: anchoring transformFrontmatter's denylist strip to the frontmatter block (latent, zero live collisions, pre-existing path). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: bump version and changelog (v1.69.1.0) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: update project documentation for v1.69.1.0 CLAUDE.md: Token ceiling section documents the context-budget ratchet as the third guard (test file, fixture, new-skill budgeting, capture command). CONTRIBUTING.md: Tier 1 guard list gains a Context-budget ratchet bullet; the Adding-a-new-skill checklist gains the budget-capture step. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: pin exact guard semantics for the context-budget ratchet in CLAUDE.md Doc-review finding: "a third enforced ceiling" undercounted the guard family (skill-size-budget floors and parity ratios also watch these ledgers, relatively). Rephrased to match the ratchet test's own header: absolute ceilings vs relative floors/ratios. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(changelog): heaviest-skill claim matches the fixture (land-and-deploy edges review by 0.2%) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(bin): gstack-skill-start + gstack-skill-end — the preamble runtime, consolidated Absorbs the ~13KB of bash every tier-2+ SKILL.md inlined twice over (bootstrap fence + artifacts-sync fence) and the skill-end telemetry/sync fences. Same KEY: value STATUS-line contract the prose interprets, plus SKILL_START_PROTO handshake (OV5), SESSION_ID/TEL_START echoes, GSTACK_HOME-normalized state paths (EOV7), --parent-pid session identity (EOV5: $PPID inside the script is the ephemeral tool-call shell), OV4 sanitization of passthrough output, and a receipted daily artifacts pull (_receipted_git, brain-sync class, fail-closed). Per-line || true error style throughout (F3) — a mid-script failure never drops later STATUS lines. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(gen): preamble resolvers emit a script invocation fence instead of inline bash generate-preamble-bash: ~6.3KB fence -> 4-line gstack-skill-start invocation (quoted-tilde pitfall handled: leading ~ interpolates through $HOME; env-var hosts keep $GSTACK_BIN) + degraded-mode prose (F1/EOV8: safe defaults, consent gates deferred-never-lost; OV5: proto rule). generate-brain-sync-block: ~6.8KB bash -> interpretation prose + the privacy stop-gate (stays inline until Phase 2's gated emission). generate-completion-status: telemetry fence -> one gstack-skill-end call with SESSION_ID/TEL_START handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(gen): regenerate all skills + golden fixtures — inline preamble bash removed Mechanical regen after the resolver change: −12,628 lines across 52 renders (corpus 952K -> 806K render tokens; tier-2 skills −11-13KB each). Golden per-host ship fixtures refreshed from the fresh claude/codex/factory renders. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: skill-start contract suite + preamble A/B eval + touchfiles registration test/gstack-skill-start.test.ts (11 free tests): STATUS-key contract vs the prose (F2), per-host fence resolution shapes (E1), proto-first, OV4 marker sanitization, --parent-pid identity, headless suppression, skill-end duration math + pending cleanup. test/skill-e2e-preamble-script-ab.test.ts (gate tier, OV7): inline-bash render (pinned from 2978597) vs script render with the fence redirected at the worktree bin (EOV2 — hermetic evals otherwise resolve the operator install and silently exercise degraded mode). 21 touchfiles dep lists gain the two bin scripts (EOV9) so future script edits select the preamble evals; selection-count pin updated 23->24. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: repin ~70 assertions to the script contract — every literal gets a successor Assertions that pinned inline-bash internals (update-check guard, _SESSIONS reaping, telemetry start/end blocks, routing probe, repo-strip producer, first-task gating, EXPLAIN_LEVEL/QUESTION_TUNING echoes, garrytan#2499 jq scope resolution, Issue-8 CONDUCTOR gate) now pin the same invariants in their new home: bin/gstack-skill-start / bin/gstack-skill-end file content for script internals, the invocation fence + interpretation prose for render-side behavior. No assertion deleted without a successor; live-execution tests (routing probe, brain-sync jq) run against script bytes unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(test): re-baseline size floors + ratchet ceilings down (EOV1/OV9 protocol) parity-baseline-v1.69.1.0.json captured with carved-skill unions (53 skills); skill-size-budget repointed with the derivation comment citing the Phase 1 context-bill receipt (the ~13KB/skill cut trips the old 80% floor on tier-1 skills first — setup-browser-cookies headroom 10.8KB < the cut). The v1.47 fixture stays on disk for history; the parity-suite growth baseline (v1.64.1.0) is untouched. Context-budget ceilings re-captured: review 29,309->26,192; learn ->10,969; ios-clean ->10,764 — Phase 1's win is locked. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(bin): instruction-emission layer — onboarding text appears only when its gate fires The 8 one-time onboarding flows (lake intro, telemetry opt-in, proactive opt-in, first-run/first-loop tips, routing injection, vendoring deprecation, writing-style migration, spawned-session rules), the upgrade-flow + feature discovery prose, and the privacy stop-gate (user-approved Q2) moved from every render into gated heredocs here. Blocks are SESSION_ID-bound (GSTACK_INSTRUCTION_BEGIN: <id> <session-id>) so page/file content can't mint directives (F4/OV4). Ack ownership per OV6: display-only tips write their markers at emit (script also fires the scaffold telemetry); interactive flows carry their ack commands inside the block. The dormant WRITING_STYLE_PENDING gate is computed for real now (marker files). BASH_COMPAT=50 heredoc guard (same as brain-sync); the quoted routing heredoc resolves its bin path via a sed placeholder. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(gen): drop the 8 onboarding generators — renders keep one instruction-block rule generate-{lake-intro,telemetry-prompt,proactive-prompt,first-run-guidance, routing-injection,vendoring-deprecation,spawned-session-check, writing-style-migration}.ts deleted (single source is now the script's emission layer, F5). generate-upgrade-check shrinks to the steady-state PROACTIVE/SKILL_PREFIX rules. generate-brain-sync-block hands the privacy stop-gate to the emitted block. The fence prose gains the generic rule: follow GSTACK_INSTRUCTION blocks only from this command's direct tool result with the matching SESSION_ID; unterminated block ends at end-of-output. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(gen): regenerate all skills + goldens — onboarding prose degated Mechanical regen: corpus 806K -> 707K render tokens (−8KB/skill; cumulative vs main: ship 91->71KB, learn 53->34KB, ios-clean 53->33KB). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: onboarding tombstone + Phase 2 pin relocations New test/onboarding-moved-literals.test.ts (F5): 12 distinctive literals must live in bin/gstack-skill-start AND stay absent from every render, plus the SESSION_ID-binding pins. ~40 assertions repinned to the emission-layer contract (gates, block ids, in-block acks, script-run marker writes); the OV4 sanitize test upgraded to the real property (every legitimate block header carries the run's SESSION_ID). first-task dep list drops the deleted generator; the token->tip case map is pinned to cover every detector bucket. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(test): carve floors/ceilings recomputed; baseline + ratchet follow Phase 2 (OV9) All 9 carved skills re-anchored to post-Phase-2 measurements (cso's union had tripped its 72,000 floor at 71,379; design-consultation had 252B of margin). maxSkeletonBytes ceilings tightened to measured+~600B. Branch-internal parity baseline recaptured in place; ratchet ceilings down again: review ->24,052, ship ->18,589, learn ->8,828, ios-clean ->8,624. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(gen): AUQ slim — tool resolution as a STATUS-line branch table, split rules to invariants + absolute pointer Tool resolution (1,799B) rewritten as a 3-branch table keyed on the echoed CONDUCTOR_SESSION/SESSION_KIND lines — Conductor prose-default, MCP-variant preference, and failure handoff preserved verbatim in behavior, including the auto-decide-first ordering and the gstack-question-log capture requirement. 5+-options handling (1,924B) compressed to the split invariants (never drop; D<N>.k shape; Include/Defer/Cut/Hold; question_id scheme with the never-ask refusal) + the full-rule pointer. Both doc pointers now interpolate the absolute install root (Codex outside-voice garrytan#7 convention) instead of the bare 'in the gstack repo'. Failure-fallback, Format, and self-check sections are byte-identical — all 14 MANDATORY always-loaded pins pass with zero test edits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(gen): regenerate all skills + goldens — AUQ slim Mechanical regen: −1.3KB per tier-2+ skill (ship 69.9KB, learn 32.5KB). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(test): baseline + ratchet follow Phase 3 (OV9); OV8 evaluated — shrink floor stays Branch-internal baseline recaptured; ratchet ceilings down again. OV8's floor-retirement question, evaluated as planned after Phase 3: the 80% shrink floor stays — it uniquely catches accidental body deletion in non-carved skills BETWEEN ratchet recaptures, and the capture command has amortized the fixture-refresh cost that motivated retiring it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(review): carve adversarial, plan-completion, and review-army into sections The three resolver macros ship already carves as siblings now load on demand for /review too: skeleton 100.2KB -> 55.0KB (-45%), union 93.4KB. Resolvers stay the single source of truth (sections wrap the macros). Step 0/1, scope drift, critical pass, confidence calibration, and fix-first stay always-loaded. Fixtures and pins follow the moved content (codex-hardening wrapped-sites, review-army E2E fixture builds skeleton+sections with an empty-fixture guard). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(codex): carve the three mutually exclusive modes into sections Review/Challenge/Consult mode bodies (34.7KB where at most one ever runs) load on demand: skeleton 81.0KB -> 55.2KB, union 1.04x the monolith. The mode dispatch, filesystem boundary, and a new always-loaded 'Synthesis recommendation (REQUIRED) — all modes' block stay skeleton-side (the AUQ per-skill pins pass unchanged); the plan-file report + exit gate render after the last section pointer per the gateAfterStop pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(land-and-deploy): carve first-run validation, readiness gate, and merge/deploy into sections The once-per-repo dry-run validation, the pre-merge readiness gate, and the merge + deploy-strategy steps (37.8KB) load on demand: skeleton 91.1KB -> 55.7KB. Step 1.5 keeps its detection bash as the dispatch; the first-run section's fingerprint-save block gained {{SLUG_EVAL}} so it is self-contained. Zero content lost (line-coverage checked against HEAD). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(ios): demote the four ios skills to preamble-tier 2 (Phase 5) They never consume the tier-3 sections (repo-mode ownership, search-before- building) but do fire AskUserQuestion, which tier >=2 provides — verified by grep before the plan review. -2.2KB per skill. Render assertions pin the demotion (tier-3 sections absent, AUQ format present). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(guards): register wave-1 carves; monolith invariants retire; baselines + ratchet follow CARVE_GUARDS gains review/codex/land-and-deploy (12 carved skills total); their MONOLITH_INVARIANTS entries retire (invariants now generate from the registry, cso precedent). Touchfiles: carve-section-loading covers the three new carves; the codex + land-and-deploy LLM-judge dep lists widen to their sections. Regen + goldens + branch-internal baseline + ratchet ceilings recaptured (review 24,052 -> skeleton-based ceiling; union floors hold). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(gen-skill-docs): review render pins read the carved union The review carve's readSkillUnion conversions (same pattern its neighbor carved-skill pins already use). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(autoplan): carve the four review phases + tasks aggregator into sections Phase bodies (CEO/Design/Eng/DX consensus flows) and the Implementation Tasks aggregator load on demand; Design and DX stay separate sections because each is independently conditional on scope. Skeleton 83.7KB -> 58.7KB (-30% always-loaded); the 6 decision principles, classification, sequencing, and explicit skip-condition dispatch stay always-loaded. The chain E2E's phase-complete markers now live only in sections, so its assertions double as section-read proof (behavioral: external). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(spec): carve the post-confirmation gate-and-file tail into one section Phases 1-4 are the turn-1 conversational spine — carving them would force the Read on the first user message for zero real savings. The mechanical tail (4.5/4.5a/4.5b redaction gates + Phase 5 filing + TTHW telemetry) fires only after draft confirmation: a genuine lazy boundary, kept as ONE section so the gh-issue-create bash can never load without the fail-closed redaction gate that precedes it. Skeleton 65.4KB -> 50.7KB; all ~85 phase-structure invariants migrated location-aware plus a new carve-shape suite (56 tests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(setup-gbrain): carve the branch-exclusive install paths into sections Brain-init (Paths 1/2/3/4 bodies), engine remediation, transcript gate, and CLAUDE.md persist load on demand — at most one install route ever runs. Skeleton 75.3KB -> 57.0KB; the Step 1 detect and Step 2 path dispatch stay always-loaded. New buildSetupGbrainFixture helper gives the periodic E2Es extract-don't-copy fixtures with a non-empty guard; the voyage-code-3 gate counts scan the tmpl union (the third init site lives in engine-remediation). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(guards): register wave-2 carves (15 carved skills); autoplan monolith retires; baselines follow CARVE_GUARDS gains autoplan (behavioral: external via the chain eval), spec, and setup-gbrain; autoplan's MONOLITH_INVARIANTS entry retires. Touchfiles: setup-gbrain periodic dep lists gain the section tmpls + fixture helper; the stale-brain-refs scan covers setup-gbrain/sections. Regen + goldens + branch baseline + ratchet recaptured. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(qa): carve QA patterns + health rubric into on-demand sections (68→48KB skeleton) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(browse): carve full command list + snapshot flags into sections/command-list.md (39→27KB skeleton) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(retro): absorb inline git/awk metrics into bin/gstack-retro-metrics + carve report format RETRO_METRICS_PROTO: 1 contract, local git reads only (fetch stays in the skill prose), degraded path documented in the skeleton. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: register wave-3 carves (qa, browse, retro) — guards, touchfiles, pins, baselines CARVE_GUARDS gains the three entries; qa's monolith invariant retires. auq-format carve-safety now keys on the skeleton+sections union shipping the AUQ block (first tier-1 carve: browse never renders it by design). Baselines: parity v1.69.1.0 at 18 sectioned skills; ratchet recaptured. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): drop stale generate-lake-intro import (generator deleted in the emission-layer move) Sol scope discipline stays pinned via the model overlay + completeness section; the lake intro is now a single script-emitted blurb. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(office-hours): carve Phase 2A/2B into mode-exclusive sections (81→67KB skeleton) A session runs exactly one mode, so a builder session never loads the 13KB startup diagnostic. Mode mapping and the vibe-shift upgrade rule stay in the skeleton. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(design): carve UX doctrine + Pretext patterns into read-on-demand sections design-html 57→49KB, design-shotgun 53→50KB. Sections wrap {{UX_PRINCIPLES}} so scripts/resolvers/design.ts stays the source of truth; the pretext-patterns STOP sits at the top of Step 3 so the read provably precedes the Write. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: register wave-4 carves (office-hours ext, design-html, design-shotgun) — 20 carved skills Both design entries carry requiredReads + loading-eval scenarios (D3A condition). office-hours phase sections are mode-exclusive, so only the always-reached design/handoff section is a deterministic requiredRead. Baselines and ratchet recaptured. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: trim CLAUDE.md 66.4→44.9KB — verbatim moves to docs/, pointers stay inline Moved: browser/sidebar/server internals, CHANGELOG release-summary format spec, project tree, hermetic-E2E detail, slop-scan reference, OpenClaw publishing. Kept inline: every hard behavioral rule (dist/ ban, redaction scan-at-sink, egress receipts, bisect commits, eval detach, CHANGELOG entry rules), the machine-managed GBrain block (byte-identical), and the '## Deploying to the active skill' header with gbrain-refresh in range (pinned by test/gbrain-refresh-install-render.test.ts). No voice rewrites. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): seed onboarding markers into the hermetic child GSTACK_HOME EOV7 made bin/gstack-skill-start honor GSTACK_HOME, so the operator-HOME seeding in e2e-helpers.ts no longer reaches hermetic children — the emission layer fired lake-intro/telemetry prompts that burned turns and stalled PTY tests waiting on an answer (observed: plan-mode-no-op derailed by the telemetry question). Onboarding-specific tests pin their own GSTACK_HOME per-test, which merges over this seed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: raise carve-section-loading wall clock to 480s SDK / 540s bun The heavy full-workflow scenarios satisfy their required section reads inside 60s but need 300-450s to finish the report on slower sandboxes; the 300s default read as a loading failure when the carve invariant held (traces: plan-eng-review read its section at 8s, office-hours all three at 24s, design-html both at 50s — all timed out mid-report). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(security): harden the skill-start trust boundary — review-army findings Session ID gains a urandom suffix (block binding unforgeable by reflected content); _sanitize also neutralizes spoofed SESSION_ID: lines; branch names are charset-clamped before JSON embedding (skill-start + skill-end); .brain-last-push reads first line only with a charset clamp; the artifacts URL echo routes through _sanitize; the privacy consent gate fires in interactive sessions only (spawned auto-choose could accept consent no human gave — emission order is not a safety property); the daily pull gets non-interactive + slow-network git guards and stamps only when the receipted path ran; ~/.claude.json gets a grep pre-filter before the jq parse. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(resolvers): question-log session_id becomes a substitution placeholder + stale-comment sweep The question-log block bound $_SESSION_ID, a shell variable the consolidated fence never sets — hook-less hosts logged empty session_id, breaking /plan-tune per-session grouping. It now uses the same substitute-from-the-skill-start-echoes contract as the telemetry block. Also: retired the pre-Phase-2 stop-gate docstring, repointed the gbrain-local-status cross-reference at the script's inline jq, dropped an orphaned section comment, documented retro-metrics' suffix-only census. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: regenerate renders for the question-log placeholder; goldens + baselines follow Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: hermetic update-check, onboarding gate sequencing, seeding parity The contract test's child did a live git ls-remote + curl to github.com on every bun run test (update_check config now gates it off); the headless test gets a fresh GSTACK_HOME so the suppression is actually exercised; a new OV6 test drives the script three times to pin ack-at-emit and gate sequencing; hermetic seeding covers the config-keyed privacy gate; the EVALS_HERMETIC=0 debug seeding reaches marker parity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(ci): demote the preamble A/B to periodic (OV7) and add it to the periodic matrix Post-Phase-3 demotion per the plan; the eval needs fetch-depth 0 (it git shows a pre-Phase-1 sha), which only the periodic workflow provides — and a static matrix entry so it can't silently never run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: bump version and changelog (v1.70.0.0) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: update project documentation for v1.70.0.0 ARCHITECTURE.md: the preamble section now describes the v1.70 runtime — the rendered {{PREAMBLE}} block invokes bin/gstack-skill-start and reads STATUS lines, gstack-skill-end logs telemetry, and one-time onboarding text arrives as gated GSTACK_INSTRUCTION blocks instead of riding in every render. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: doc-review fixes — repair moved-file links, drop unbacked session-count claim docs/BROWSER_INTERNALS.md: the two ARCHITECTURE.md anchor links broke when the section moved from repo-root CLAUDE.md into docs/ — now ../ARCHITECTURE.md. ARCHITECTURE.md: the preamble's session-tracking item claimed an active-session count and an "ELI16 mode" that no shipped code implements (the count computation was deleted with the inline preamble); describe the real touch-and-prune behavior instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(changelog): correct numeric claims against measured counts 50 of 62 installed skills dropped (fixture/alias entries have no preamble); 11 new carves + a deeper office-hours carve = 9→20; test counts match the files (13 / 11 / 3 / 7). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: repoint the preamble-runtime version reference after the queue rebump (v1.71.0.0) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(e2e-design): widen the Aesthetic synonym set — vocabulary variance, not a regression Both attempts in run 33090283032 produced judge-praised DESIGN.md files phrased as 'design principles'/'design language' without any of the four original literals; inputs were identical to the prior passing run 32899975845 (design-consultation untouched by the intervening merge). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): stage design-consultation's sections/ into the E2E fixture The skill has been carved since v1.57.0.0 — the DESIGN.md structure prescription (the AESTHETIC proposal template) lives in sections/proposal-and-preview.md behind a STOP-read. The fixture only copied SKILL.md, so the agent improvised structure from the skeleton and the section-synonym check has been a coin flip since the carve (CI run 33090283032 trace shows 'no sections dir'; the local eval store has the same failure on 2026-08-25 while that day's CI run passed on lucky vocabulary). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…arrytan#2710) * fix(browse): never chmod shared, symlinked, or foreign-owned dirs to 0700 restrictDirectoryPermissions unconditionally chmodded its target. On hosts where the process holds CAP_FOWNER (Docker as root, CI sandboxes) that chmod SUCCEEDS on root-owned /tmp whenever a state file is configured there (BROWSE_STATE_FILE=/tmp/x.json derives stateDir=/tmp), and a 0700 /tmp breaks access(2)-based checks machine-wide for every other process. The POSIX branch now refuses shared sticky dirs, world-writable mounts under root, foreign-owned dirs, and symlinked state dirs; refusals warn once per process instead of failing silent; owned-but-unreadable dirs keep their chmod self-repair; and the check-then-act race is closed with fd-anchored O_NOFOLLOW + fstat/fchmod on a single inode. Regression tests cover the sticky-dir, foreign-uid, mkdirSecure-reapply, and symlinked-dir shapes. * fix: hash with sha256sum before shasum on Linux (config slugs + setup verify) shasum is perl/macOS; coreutils-only Linux ships sha256sum. Two call sites hard-coded shasum: gstack-config's sha8_of/sha16 (so resolve-user-slug exited 127 for any Linux user with a git email, the Layer-3 fallback) and the generated bun-installer checksum snippet in the browse/qa NEEDS_SETUP flow (spurious "checksum mismatch" on the same distros). Both now resolve sha256sum first and fall back to shasum -a 256. New shim-PATH tests pin BOTH hasher branches of sha8_of to a known vector and cover the sha8->sha16 collision escalation end to end. * feat(contract): Aside is the recommended driver for third-party web actions The Third-Party Web Actions contract (ship, spec, office-hours, land-and-deploy, setup-deploy) now names the Aside AI browser as the recommended driver: it acts across the user's real logged-in sessions, which is what vendor-dashboard moments need. Supersedes the v1.65.0.0 de-Aside stance by explicit user directive (2026-08-27). Detection is a runtime probe (command -v + aside --version under a portable gtimeout/timeout/bare guard; nonzero exit = not detected). Consent options render per detection state with Aside recommended and the first-party stack ($B headed + handoff, GStack Browser) as the universal fallback. Absent on macOS, the contract mentions the aside.com download (macOS 15+) once per task; gstack never runs an installer and binary presence is never consent. Drive discipline: step-wise over whole-task delegation, vendor confirm mode on, vendor skill/--help text scoped to operational syntax only, secrets minimized (autofill / human-used copy buttons), Apple credential creation never a drive target in any skill, failure path quotes redacted errors and falls back only with fresh consent. test/third-party-actions.test.ts pins every load-bearing sentence (21 tests) plus repo-wide tripwires: an aside command allowlist (--version/--help only, code spans AND prose) and a ban on Aside installer invocations across all generated docs. Budget ratchet fixture and carve skeleton ceilings refreshed in this commit per the ratchet protocol. * chore: regenerate remaining browse-setup snippet consumers The sha256sum-first checksum fallback in the generated NEEDS_SETUP snippet renders into every browse-consuming skill, not just browse/qa. Mechanical regen of the other ten consumers; no template changes here. * test: consent-gate E2E suite + functional fs-capability probes Five hermetic gate-tier E2E cases (tpa-present / absent-linux / broken / absent-darwin / apple-ban) drive the real contract section through claude -p with PATH shims for aside and uname; the absent cases filter any REAL aside binary out of the child PATH and assert absence with Bun.which before spawning, so dev machines cannot leak into detection. Registered per-case in E2E_TOUCHFILES/E2E_TIERS with template-level deps (ship/SKILL.md.tmpl, gen-skill-docs.ts) and added to the evals.yml matrix with tier: gate. eval:bg:periodic's detach timeout rises to 36000s for the grown periodic shard census (floor-enforced by test/eval-detach-timeout-floor.test.ts); CLAUDE.md doc updated to match. test/helpers/fs-caps.ts adds canRevokeWrites/canRevokeReads functional probes; 13 chmod-based tests swap their uid-0-only guards for the probes so suites skip honestly on CAP_DAC_OVERRIDE containers (this sandbox: uid 1000 with full caps) instead of asserting revocations the kernel ignores. path-validation's symlink test targets /etc/passwd (exists everywhere; /etc/crontab is absent on Amazon Linux). * docs: file the Aside follow-ups in TODOS Phase-2 QA logged-in-evidence path (P3), a hostile-vendor-skill E2E for the contract's override sentence (P2), and fd-anchoring the file-level permission writes to match the directory hardening (P3). * chore: bump version and changelog (v1.72.0.0) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: update project documentation for v1.72.0.0 docs/skills.md: Third-Party Web Actions subsection under /ship (Aside recommended driver, consent rules, credential boundaries). BROWSER.md: "Aside and third-party drives" subsection under Real-browser mode + ToC entry, including the no-gstack-side-audit-trail caveat (ship adversarial finding 12). TODOS.md: mark the finding-12 doc note done. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: apply cross-model doc review fixes for v1.72.0.0 docs/skills.md: restore the /ship closing line above the new subsection. BROWSER.md: ToC label matches the heading; BROWSE_STATE_FILE env row documents the new dir-hardening refusal + one-time warning. CHANGELOG: correct the hasher precedence wording (sha256sum first, shasum fallback) and the fs-caps count (14 test files, verified against the diff). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: close cross-model doc-review gaps for v1.72.0.0 setup's manual bun-verify instruction gets the same sha256sum-first fallback the automated snippet got (coreutils-only Linux); BROWSER.md's BROWSE_STATE_FILE row now lists the under-root world-writable refusal; test-cost ceilings in CLAUDE.md/CONTRIBUTING.md updated for the five new gate E2E cases (~$4.20 E2E / ~$4.35 evals). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): gate the symlink-refusal test to POSIX and drop the umask assumption The symlink regression test exercised the POSIX O_NOFOLLOW branch but ran on Windows, where restrictDirectoryPermissions takes the icacls branch and stat has no POSIX modes (0o666 always) — windows-free-tests failed on mode 493 vs 438. Early-return on win32 like every sibling test in the file, and assert the target's mode is UNCHANGED (captured post-mkdir) instead of hardcoding 0o755, which a strict umask would also break. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…r speed (garrytan#2721) * fix(ci): free-tests lane actually runs the make-pdf e2e gates The 9 make-pdf/test/e2e gate tests probe make-pdf/dist/pdf, browse/dist/browse, and the diagram-render bundle, then self-skip when absent. The required free-tests lane never built any of them, so the gates silently skipped on Linux for their entire life (verified: 9 of 14 skip, exit 0). make-pdf-gate.yml's justification for deleting its Linux leg claimed the free lane covered this — it didn't. - new build:gates script: exactly the three artifacts the gates probe (full bun run build compiles five binaries; ~60-90s tax on the only required check is not warranted) - free-tests.yml: build:gates step + poppler-utils + fonts-noto-color-emoji (fonts must precede the first browse daemon launch — Chromium snapshots fontconfig at startup; verified live: a warm daemon renders tofu, a fresh one embeds NotoColorEmoji) - make-pdf/test/e2e/ci-prereqs.test.ts: GSTACK_EXPECT_BINARIES=1 (set by the workflow) inverts the skip polarity in CI — dropping the build step or poppler fails the lane instead of re-opening the silent-skip hole Pre-flight: all 9 gates green on Linux locally. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ci): kill the three zero-test eval jobs (hollow green) - delete the vestigial e2e-codex / e2e-gemini matrix rows: both files are whole-file periodic-tier, so with no row tier: they ran ZERO tests and reported green on every PR (~2 min of runner each, pure false confidence; the periodic lane owns those suites) - e2e-pty-plan-smoke gains tier: gate — its two files are whole-file describeE2ETier('gate'), so the job burned ~7 min of container setup then skipped every describe - KNOWN_TIER_UNSET burned down to empty; the ratchet stays armed so a future row/file tier mismatch fails the suite instead of shipping hollow green Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ci): least-privilege permissions + fork-safe concurrency keys - evals.yml / evals-periodic.yml evals jobs: explicit contents:read + packages:read (container-image pull) and persist-credentials:false — the jobs that execute PR-authored code with three provider API keys ran on the repo-default token grant with the token written into .git/config - permissions blocks for the 4 workflows that had none (skill-docs, make-pdf-gate, windows-free-tests, windows-setup-e2e) - fork-safe concurrency keys: actionlint, skill-docs, make-pdf-gate, windows-setup-e2e switch from head_ref to PR-number keying — a bare branch name carries no fork prefix, so same-name branches from two forks shared one group and cancelled each other's runs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ci): one bun version everywhere + drift tripwire Lanes disagreed four ways: 1.3.13 (free-tests, windows, Dockerfile.ci), latest (quality-gate, make-pdf-gate), unpinned (skill-docs, version-gate — setup-bun installs latest), 1.3.10 (.gitlab-ci.yml). Different Bun versions change the runner output shapes the strict classifiers regex-match, spawn semantics, and shell parsing — a lane on a different Bun tests a different product; Dockerfile.ci's own comment records this class biting once already (silent 1.3.13/1.3.14 drift). All surfaces pinned to 1.3.13; test/bun-version-drift.test.ts scans every workflow setup-bun stanza + Dockerfile.ci + .gitlab-ci.yml and fails on any mismatch or unpinned stanza. skill-docs also gains --frozen-lockfile (was bare bun install). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(ci): bind the three-way image-tag hashFiles() expressions evals.yml, evals-periodic.yml, and ci-image.yml each compute the CI image tag from hashFiles('.github/docker/Dockerfile.ci', 'bun.lock', 'patches/**') — synced by comment only (TODOS.md 'CI three-way image-tag drift'). If one input list drifts, that workflow computes a different tag for the same content: eval lanes silently rebuild the image every run, or ci-image prebuilds a tag nobody looks up. The test extracts each tag-computation site and fails on any mismatch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ci): ci-image stops rebuilding the identical image every ship - package.json out of the trigger paths: the tag hash deliberately excludes it (version bumps every ship), so every merge rebuilt and re-pushed the IDENTICAL tag (~2m26s for zero content change); patches/** added (it IS a tag input) - manifest existence check (mirrors evals.yml): tag already exists → skip the build - concurrency group: two rapid main pushes raced pushing the same :latest/:buildcache tags - cron staggered 06:00→04:00 Monday: it shared the exact minute with evals-periodic, which could race a half-pushed tag or duplicate the build - timeout-minutes: 30 (was unbounded → 360-min default for a hung docker build) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ci): quality-gate drops the 74s full-history checkout fetch-depth:0 cost 74 of the job's 92 seconds; the three gates it feeds take ~12s combined. Shallow checkout + exact-SHA fetches for the diff's base/head (an exact-SHA fetch, not a guessed depth — long-lived branches and merge queues still resolve), with a --deepen fallback for push events whose 'before' is unusable. timeout right-sized 20→10 min. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ci): small-lane batch — timeouts, right-sizing, windows cache warm-start - timeout-minutes on the 6 remaining unbounded jobs (actionlint 5, skill-docs 10, version-gate 10, make-pdf-gate 15, pr-title-sync 5, evals build-image 15) — a hung step sat on GitHub's 360-min default - right-size measured-over-long timeouts: dependency-review 10→5, windows-setup-e2e 15→10 - dependency-review: 2-core runner (28s API call on an 8-core box) and drop .github/workflows/** from its trigger paths (workflow edits have no dependencies to review) - windows caches gain restore-keys: a lockfile bump paid the 26s/43s restore for a guaranteed cold miss Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): scope GSTACK_HOME to each file's execution window Five files assigned process.env.GSTACK_HOME at module scope. Shard processes evaluate sibling modules before running their tests, so the assignment leaked into every other file in the shard — the damage was already visible in defensive workarounds (relink.test.ts:28 'fresh install test saw a neighbor's skill_prefix'; cdp-e2e's own comment documents a sibling's temp dir baked into artifacts). Pattern: save original, assign in beforeAll, restore in afterAll (cdp-e2e already restored but still assigned at load — its window now matches the others). GSTACK_TELEMETRY_OFF and GSTACK_PROJECT_SLUG get the same treatment where they rode along. Victim files' defenses stay in place (cheap insurance). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: tripwire against module-scope GSTACK_HOME assignments Column-0 assignment of GSTACK_HOME / GSTACK_STATE_ROOT in any tracked *.test.ts fails with the file:line and the fix (beforeAll + afterAll restore). Kills the cross-file env-leak class the previous commit swept. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): e2e-harness-audit derives its skill census from disk The hand-maintained 39-name SKILL_GLOBS list had drifted to 39 of 54 SKILL.md.tmpl on disk. No live gap today (none of the 15 unlisted skills is interactive), but the next interactive skill would have landed unguarded with zero signal. The audit now walks top-level dirs for SKILL.md.tmpl (statSync so symlinked dirs like connect-chrome count), so new skills are in scope the commit they appear. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(evals): judges honor the eval-model resolution chain + real 429 backoff callJudge inlined GSTACK_EVAL_MODEL_JUDGE || sonnet, silently ignoring the global GSTACK_EVAL_MODEL override every other eval call site honors via lib/eval-model.ts. New 'judge' kind in DEFAULTS (sonnet — the D1a pin-on-regressors calibration stands; model CHOICE unchanged) and callJudge resolves through it: explicit arg > GSTACK_EVAL_MODEL_JUDGE > GSTACK_EVAL_MODEL > default. 429 handling upgraded from one fixed 1s retry (reliably lost races at CI concurrency) to three jittered exponential retries (~1s/4s/16s), honoring the server's retry-after when present. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): the two expect(true) paid stubs become test.todo skill-e2e-spec-execute (600s budget) and skill-llm-eval-spec (300s) reported PASS on every periodic run while asserting nothing. Deleting them would remove the periodic-tier selector surface they exist to register (diff-based selection for spec/ changes), so they become test.todo — reported as todo/skip, never pass — with the v1.1 implementation specs kept in-file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): reactivate 5 quarantined browse tests (2 security) extension-sender-auth's two privileged-message denial tests (content script + missing sender.url — the extension's security boundary) and snapshot's three skips were quarantined 'pre-existing' failures. Root cause: machine-local state on the quarantining dev machines — the test and gate code are byte-identical between the quarantining commit (410b492) and HEAD, and all five pass deterministically on a clean checkout (68/68 across both files, multiple runs). No assertions weakened, no product changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(evals): activate the 4 paid test files that could never run anywhere carve-section-loading, codex-e2e-plan-format, codex-e2e-recommendation-substance, and llm-judge-recommendation gated on EVALS/tier (free suite loads them as describe.skip) but their names fell outside PAID_TEST_GLOBS, so no paid lane ever selected them — net execution zero, forever. The existing matrix tripwire filtered on isPaidTestFile() first, so it was blind to exactly this class (the same bug that hid the pre-split monolith's gate tests for ~8 releases). - PAID_TEST_GLOBS: codex-e2e* + skill-llm-eval* wildcards (replacing exact names) + llm-judge-recommendation + carve-section-loading; package.json's six test-script glob lists mirrored - codex-e2e-plan-format gains the explicit periodic tier gate its siblings carry (external-service rule) — without it the sharded runner's no-guard default would spawn Codex in the gate tier per PR - eval:bg:periodic --timeout 32400→37800: the census growth pushed the periodic worst case to 35910s; the old value had 270s of headroom BEFORE this change and would now kill healthy runs mid-flight - new test/paid-orphan-tripwire.test.ts: any EVALS/tier-gated test file outside the globs fails the free suite (reasoned SCANNER_EXEMPT for the gate helpers + meta-tests) — the class-killer - paid-shards pins updated: the four orphans now assert INSIDE the census Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(browse): restrictDirectoryPermissions warns and skips symlinked dirs Closes the Windows Free Tests red: recent lane failures showed a platform-unguarded POSIX mode-bit assertion ('Expected: 493' — a symlink-skip test) from PR-branch variants; the KNOWN_WINDOWS_SAFE force-include reason ('mode-bitmask hits are POSIX-branch only') did not hold for that shape, and main had neither the guard nor the behavior. - product: lstat first; a symlinked dir gets a warning and a skip on both platforms — chmod AND icacls dereference the link, so restricting through a symlink hardens an unvetted target (and /inheritance:r could lock out its real owner). All callers already treat hardening as best-effort (try/catch). - test: the symlink regression test, platform-aware — symlinkSync in the house try/catch skip pattern (Windows runners without Developer Mode can't create symlinks), mode-bit assertion guarded off win32, behavior assertions (no throw, warning text, target readable) everywhere; POSIX still proves the skip (0o755 unchanged, not 0o700) - KNOWN_WINDOWS_SAFE reason updated to the now-true premise 20/20 pass on Linux. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): unique tmp dirs for plan artifacts + audited live-repo cwd sites Six paid PTY tests wrote their expected plan artifact to a FIXED shared /tmp path ('/tmp/gstack-test-plan-<mode>.md') and rmSync'd it in finally — under --retry 1, EVALS_JOBS>1, or two concurrent worktrees, a sibling's cleanup deletes this run's artifact and the D19 'agent did not produce expected plan file' assertion fires spuriously. Each test now mkdtemps its own dir, interpolates the unique path into the agent prompt (fixture-sourced prompts get a replaceAll + drift guard that throws if the fixture's literal ever moves), and cleans up its own dir. The 18 cwd:-into-the-live-repo sites were audited: all deliberate (skill registry + hermetic pre-trusted dir, in-repo gen renders, git history reads, slug resolution) — each now carries a '// LIVE-REPO CWD: <reason>' comment so the next audit can tell deliberate from accidental. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): trim the seven over-wall 1700s timeouts to the 1500s physical ceiling 1,700,000ms (28.3 min) exceeded every wall these tests run inside: the 25-min CI job timeout and the 1800s sharded-runner wall (which also leaves --retry 1 zero room for a second attempt). Budget above the wall is fiction, not headroom — a test that actually used it produced a job-level kill (no bun summary, no artifact) instead of a clean per-test timeout. No recorded p95 exists for this family (they are being retiered to periodic in the re-platform wave); the trim stops at the physical ceiling rather than guessing lower. Final policy lands in the Wave-2 eval-budgets constants module. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(gen): main() guard — importing gen-skill-docs no longer regenerates the tree The generator's whole body executed at module load, so any import of it (test/gen-skill-docs.test.ts pulls assertSinglePreamble via require(); test/catalog-trim.test.ts imports helpers) regenerated all 71 SKILL.md in place — the root cause of half the TREE_MUTATING serial-shard entries (hazard class garrytan#2532). The body now lives in an exported main(): number behind if (import.meta.main). Semantics preserved exactly: failure exits are immediate (matching the old top-level process.exit), success leaves the event loop to drain so the llms.txt fire-and-forget IIFE finishes its write, and the module stays synchronous/require()-able. Proofs: byte-identical --host all output (git status clean), --dry-run stale-tree still exits 1 (the skill-docs freshness lane depends on it), and the new test/gen-skill-docs-import-purity.test.ts pins load-time purity via a subprocess probe (mtime-based, so a dirty worktree can't false-fail). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(gen): --out-dir renders every host, outputs-only --out-dir was Claude-host-only (gen-skill-docs.ts:842), which forced the codex/factory-regenerating tests (gen-skill-docs, skill-validation, host-config) to mutate the live tree — the reason they sit in the TREE_MUTATING serial shard. The flag now mirrors ALL outputs into the out-dir: external-host trees (.agents/.factory/... via processExternalHost), external section files, openclaw docs, and gstack/llms.txt (a catalog-mode render must never rewrite the tracked index). OUTPUTS ONLY — inputs (templates, sections/, host configs) are always read from ROOT, so an empty out-dir can never feed the render. rewriteSectionBase stays Claude-only (external hosts have their own path grammar). Proofs: in-place --host all is byte-identical (tree clean); --host all --out-dir <mkdtemp> renders the full multi-host tree with ROOT untouched; gen-skill-docs-out-dir tests + 415/415 gen-skill-docs.test.ts green (bin/dev-setup's claude rendering byte-compat). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(evals): every E2E key's dep list names its own declaring test file 129-of-177 keys omitted their own test file, so editing only a test's prompt or assertions selected NOTHING — the changed test never ran on the change that changed it. 135 keys self-registered (110 E2E + 25 LLM-judge), resolved by strict declaration evidence (testName:/ testIfSelected/judge call sites), with skill-name false positives excluded. e2e-tier-alignment's warn-only branch for unregistered files is now a hard failure with a 4-entry KNOWN_UNREGISTERED ratchet (template- literal testNames, fail-open-safe) + a burn-down test so the set only shrinks. Selection sanity: a one-file diff on skill-e2e-qa-workflow now selects its 4 tests (was 0); skill-llm-eval 0 → 25. Known follow-ups (filed): 15 E2E + 2 judge PHANTOM keys select tests that exist nowhere; codex-e2e-plan-format's testIfSelected names have no map keys (run-all only). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(evals): ratchet the 8 newly-visible gate-matrix gaps The self-registration sweep made these eight files' gate-tier keys visible to the census for the first time — their gate tests run in NO CI lane today (pre-existing hole, newly measurable). Ratcheted into KNOWN_MATRIX_GAPS with the burn-down note: the paid-lane re-platform runs every gate file by construction and retires this ratchet class. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(test): duration-aware LPT shard packing for the free suite Hash sharding balances file COUNTS (1.15x spread) but not cost — the Playwright-launching files landed 4/3/4/1/2/1 across 6 shards, giving a measured 28s–97s shard spread and ~40s of idle tail on every run. Full-suite mode now packs by recorded per-file durations (longest-processing-time-first) when the committed seed scripts/free-test-durations.json exists. - ONE store, no overlay: the seed is refreshed occasionally via the new --record-durations mode (each file timed in its own child — exact, and immune to bun's stream buffering, where silent passers print no header to timestamp); GSTACK_FREE_TEST_DURATIONS overrides the path for experiments; CI never records - seed is a hint: missing → silent hash-shard fallback; corrupt (bad merge) → one warning + fallback; unknown files → 75th-percentile pessimism so a surprise long-runner can't recreate the tail - packed shards get duration-aware walls (max(base, predicted x 3)) — LPT decouples count from cost BY DESIGN, so the 5s/file heuristic would undersize a shard holding few expensive files - one log line per shard (files + predicted seconds) so packing regressions are diagnosable from any run log - the --shard CI-matrix path is untouched: stable hash indices are its contract - successor note in-code: bun >=1.3.14 ships native --timings/--shard LPT — swap this packer when the repo unpins 1.3.13 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): decouple slop:diff from bun run test; quality-gate runs it per PR 'bun run test' silently appended up to two 120s npx slop-scan runs plus a git worktree add/remove after the suite (2>/dev/null || true) — invisible in the documented '~90-100s' timing and pure friction in the pre-commit loop. Decoupling is not coverage removal: quality-gate.yml now runs slop:diff on every PR (advisory, matching its in-repo 'never blocking' contract), and /review already invokes it explicitly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(test): eval-budgets timeout tiers + fit/ceiling policy test Five named tiers (JUDGE 120s / CAPTURE 300s / CAPTURE_LONG 600s / PTY 900s / PTY_LONG 1200s) replace hand-ratcheted sprawl (46x300s, 46x120s, 44x360s, 44x180s, 27x240s, 19x150s, 13x420s, 12x600s...), much of it inflated to paper over the old 40-way in-shard concurrency that the sharded runner's 1-file-per-shard model kills. Policy test pins: every tier fits the shard wall minus 120s overhead (the structural fix for budgets-above-the-wall fiction), tiers stay ordered, and no paid literal exceeds PTY_LONG x1.25 — oversized tests get split, not budgeted past the wall. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(test): shared runBin helper for bin-script unit tests ~36 free test files each carry a near-identical local run() (spawnSync + utf-8 + {status, stdout, stderr}) differing only in env composition, cwd, and timeout. runBin absorbs the invariant core; options carry the variance (gstackHome sets BOTH GSTACK_HOME and GSTACK_STATE_DIR — the config-precedence trap several locals rediscovered independently; home for $HOME-anchored bins; input/trim/timeout/maxBuffer). Free-test-only by design so it never becomes a de facto global touchfile. Migration of the 36 call sites lands separately (mechanical batches). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): runBin trim assertion — trim shapes stream ends, not interior Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(test): mechanical sweep — 298 paid-test timeouts onto eval-budget tiers 69 files, both shapes (trailing bun-test budgets and runner timeout/timeoutMs options), ROUND-UP ONLY so nothing that passed can start failing: 75 → JUDGE_MS, 137 → CAPTURE_MS, 74 → CAPTURE_LONG_MS, 9 → PTY_MS, 3 → PTY_LONG_MS. Raw >=60s literal count in the paid scope: 395 → 97, of which 51 are non-timeout noise (fixture dates, run IDs) and 46 are enumerated justified holds (comment-carrying calibrated budgets, poll-loop constants, utility spawn waits, and the seven physical-ceiling 1_500_000 sites). The eval-budgets policy ratchet keeps the residue from regrowing. Known collapse: where an inner runner budget and its enclosing test budget now share a tier, the old stagger is gone — an overrun surfaces as a bun test timeout instead of a graceful runner timeout (diagnosability trade, not a correctness one). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: coverage fill — 95 tests for six zero-coverage surfaces - eval CLI family (eval-list/compare/summary + eval-select smoke): the primary interface to eval results had no tests; isolation via a fake gstack-slug under a mkdtemp HOME (the scripts' real resolution path — they do NOT honor GSTACK_EVAL_DIR; only EvalCollector does). Pinned current behavior: eval-list does NOT exclude _partial runs (documented improvement candidate) - slop-diff (runs on every /review + quality-gate): fixture git repo + first-on-PATH npx stub (never downloads real slop-scan); no-diff early exit, missing-scanner fallback, fingerprint line-insensitivity, merge-base worktree scan - bin/gstack-code-intelligence CLI arg surface (lib was covered, the 284-line CLI wasn't): select/consent/suggest/index/search gating; pinned: --help routes to usage failure exit 1 (no handler) - browse media-extract: the page.evaluate callback exercised in-process against a mock DOM (no exports added) — lazy-src fallback chain, HLS/DASH detection, bg-image url() parsing, 500-element cap - browse session-cookie-store: factory contract (cookieName/ttlMs/ maxSessions eviction, cross-store isolation, mint→validate round-trip); store is in-memory — no fs cases exist - lib/version-source direct unit tests (gstack-version-bump.test.ts spawns the bin, never imports the lib): parse/format/cmp/bump coercion, npm 4→3 translation, garrytan#2501 mangled-JSON regression class All hermetic (mkdtemp homes, runBin child isolation); windows curation correctly partitions the six. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(test): first runBin migration batch (3 of ~36 run() duplicates) explain-level-config, benchmark-cli, evidence move onto the shared helper; each file's remaining special-case spawnSync sites (raw-buffer probes, env-scrub probes) stay put deliberately. 55/55 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(evals): paid shards spool to disk + shared runShardChild lifecycle - runPaidShard no longer buffers whole 30-min stream-json streams in RAM (x concurrent jobs): every byte tees to a per-shard log file (slug-named, path printed at START for mid-run inspection and on the FAILED terminal line); failures print a 64KiB tail read back from disk; passing shards stay quiet (the file is the record) — the free runner's proven contract. Classification unchanged: the strict classifier still sees every byte first. - the ~35 duplicated spawn/group-kill/wall-timer/finally-reap lines move into runShardChild in test-strict-output.ts (detached-per- platform spawn, signal forwarding, SIGKILL group kill at the wall, drain-before-verdict); designed so the free runner can migrate later - expectedFiles drift fixed toward ENFORCEMENT: the injected-command exemption is gone — a fake command exiting 0 without bun's terminal summary now reads FAILED (pinned: silent-pass → failed) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(evals): parent-computed selection propagates to shard children The sharded runner computed diff selection once, then each of its 48-73 children recomputed it at module load — including, on touchfiles-diff branches, a per-child bun subprocess evaluating the old data file (20s timeout each). The parent now serializes {version, selected, reason} as EVALS_SELECTION_JSON into the shard env; e2e-helpers adopts it at load. Fail-open preserved: any parse/shape violation → ONE stderr warning + local recompute; absent env → silent local compute (non-sharded entrypoints unchanged). Drift test pins parent→child round-trip to identical selection decisions plus the malformed/absent cases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): kill the four worst fixed sleeps (300s/30s/30s/20s) - watchdog.test: the 20s blind wait for one production parent-watchdog tick becomes BROWSE_PARENT_WATCHDOG_INTERVAL_MS=250 (new env knob in server.ts, NaN-safe, production default unchanged) + polls for the boot line and the tick's stay-alive log — strictly stronger (the old form never proved a tick observed the parent death). 24s → 3.6s. - stop-dead-daemon / terminal-agent-owner-watchdog: the 300s/30s stand-in child lifetimes become stdin-EOF-bound — the child can never self-exit mid-test on a slow runner (spurious-failure class) and self-reaps instantly if the test dies (no 300s orphans). Node-compat stdin APIs (owner-watchdog runs on the Windows lane). - browser-skill-commands: the sleeper fixture's 30s self-time becomes 8s (no stdin pipe exists in runToFiles) — far above the 1s product timeout it must outlive, below the test ceiling, so a timeout-kill regression fails on clean assertions instead of an opaque bun timeout; added: stdout must NOT contain 'done'. 45/45 green across the four files + server tripwires. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): gen-skill-docs + catalog-trim leave the serial mutator shard gen-skill-docs.test.ts's 15 in-place generator spawns now render into mkdtemp out-dirs (gitignored-artifact reads repointed; the handshake scan's silent console.warn degrade became a hard assertion); its tracked-tree reads (freshness dry-run, SKILL.md content pins) stay reads. catalog-trim needed no change beyond the earlier main() guard — its import is now side-effect-free (pinned by the import-purity test). Both TREE_MUTATING entries deleted in this commit, per the transition rule: an entry leaves in the same commit as the file's last in-place write. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): skill-validation renders codex host into an out-dir Its 3 in-place --host codex regeneration sites collapse into one module-level --out-dir render; assertions untouched. TREE_MUTATING entry deleted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): host-config self-provisions goldens (ordering dependency severed) Its goldens were 'produced by gen-skill-docs.test.ts' with a when-missing beforeAll fallback that wrote the live tree — an inter-test ordering dependency the serial shard hid. It now renders codex+factory UNCONDITIONALLY into its own out-dir and reads goldens only from there (the Claude golden deliberately keeps reading tracked ship/SKILL.md — a read; out-dir claude renders repoint section-base paths by design). TREE_MUTATING entry deleted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): gbrain-detection-override drops mutate-then-git-restore regenAndSnapshot renders --host claude --out-dir <mkdtemp> (+ --respect-detection) and snapshots probes from the out-dir. The git-restore machinery is deleted outright — it restored only PROBE_FILES of the 71 files each call wrote, so a stale tree kept the other 68 dirty (the partial-restore bug), and its 'no output-path arg' comment had been false since --out-dir landed. TREE_MUTATING entry deleted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): catalog-mode-full renders to out-dir; restore machinery deleted The full-catalog smoke no longer rewrites all 71 SKILL.md then regenerates to restore (with its 'CRITICAL: failed to restore' prayer path) — it renders into a mkdtemp and additionally asserts tracked ship/SKILL.md is byte-unchanged. TREE_MUTATING entry deleted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): idempotency proof strengthens to two-out-dir recursive diff Two renders into two separate out-dirs, EVERY file diffed byte-for-byte (claude-only and --host all; normalization only for each dir's own sanctioned section-base repoint; presence-sanity lists guard against a vacuous empty-dir pass) — strictly stronger than the old in-place double-regen that sampled 5 files. TREE_MUTATING entry deleted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): spec-template-sync compares an out-dir render, not an in-place one TREE_MUTATING entry deleted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(test): the serial tree-mutating shard dissolves — TREE_MUTATING is empty Zero mutators remain (all eight render into out-dirs now), so the four ratchet READERS (parity caps, size budgets, carve parity/ordering) get a quiet tree by construction in any shard and rejoin the parallel phase. The ~35-40s serial tail on every full-suite run is gone. The mechanism stays: a future test that genuinely must write shared artifacts in place earns an entry with a reason and is serialized again; the census pin still fails on renamed keys. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(gen): out-dir byte-identity + tree-clean pins for external hosts codex render: porcelain unchanged AND out-dir gstack-ship/SKILL.md byte-identical to a fresh in-place render (+openai.yaml presence); --host all render: exit 0, porcelain unchanged, claude + .agents + .factory + llms.txt + openclaw docs all present in the out-dir. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(test): commit the initial free-test durations seed (496 files) Recorded via --record-durations on a quiescent tree: 479s serial total, p50 92ms / p90 1.8s / max 31.4s — the top-heavy cost shape LPT packing exists for. A hint, not a contract: refresh opportunistically with bun run test:free --record-durations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(evals): planner/executor/report modes — the CI re-platform surface One PLANNER computes diff selection + the slice plan ONCE and writes a manifest (--emit-plan <path> --slices K); K executors consume it (--plan <path> --slice i), never self-selecting, and write slice-result artifacts; a REPORT reconciles results against the manifest (--report <dir>) fail-closed: a slice whose artifact never landed is a FAILURE, a planned shard nobody reported fails, wrong-slice/duplicate/cross-tier results fail. Kills per-slice selector divergence and hollow-lane aggregation at the root. - hollow-shard guard: under EVALS_ALL, exit 0 with ZERO executed tests (bun's 'Ran N tests' now captured by the classifier — additive) is 'passed-empty' and fails the run; selective runs keep it 'passed' with one warning (in-file diff/tier self-skips are legitimate there); unknown counts are never guessed hollow - retry parity: --retry 1 default + RETRY_OVERRIDES literals for the three files whose old matrix rows earned retries: 2 (stale entries pinned against disk) - live smoke: gate plan = 48 shards across 6 slices; report mode exits 1 on a fabricated missing slice, 0 when complete Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(ci): sliced paid lane (planner -> 6 executors -> fail-closed report) The parity-phase re-platform: evals.yml gains a second, sliced lane driven by scripts/test-paid-shards.ts — the SAME engine local eval:bg:gate uses, so CI and local share one selection engine. - plan-slices: ONE planner (fetch-depth 0 — the only job needing history) emits the manifest; selection fails open to run-all, never per-slice (the divergence class is structurally dead) - eval-slices: 6-way matrix consuming the manifest; PTY seed + skill-registration steps run unconditionally (idempotent — a sliced lane cannot key them on suite names); aggregate spawn budget 6 x EVALS_JOBS=2 x EVALS_CONCURRENCY=2 = 24 lane-wide (the matrix's 40-way per row queued session startup behind 39 siblings — the timeout-flake family root); slice results + spooled shard logs uploaded as artifacts - slices-report: reconciles slice artifacts against the manifest FAIL-CLOSED via --report — a slice whose artifact never landed, or a planned shard nobody reported, is a failure, not an absence - sequenced needs: evals so provider concurrency never doubles while both lanes coexist; the matrix + its ratchets are deleted after demonstrated parity (intersection + expected-additions comparison) - workflow_dispatch gains evals_all (default true) for parity runs and post-merge smokes — a dispatch can never silently select zero Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(ci): weekly periodic lane runs EVERY periodic test + gate census backstop evals-periodic.yml re-platforms onto the sharded runner: planner manifest → 6 executor slices → FAIL-CLOSED report. This IS the coverage contract: all ~70 periodic-tier files weekly (EVALS_ALL=1), killing the silent-rot class where a hard-coded 9-file matrix left ~57 files running NOWHERE (the autoplan E2E rotted invisibly for months). - test/helpers/periodic-exclude-data.ts: reasoned exclusions in their OWN literals file (deliberately not touchfiles-data — map-diff evaluates old versions of that file standalone). Every entry carries reason + tracking with a re-entry condition; the runner surfaces each exclusion per run; policy test pins real-file + non-empty fields. Initial: ship-idempotency + brain-privacy-gate (documented-red, never green) and skill-e2e-ios (manual hardware). The TODOS 'sidebar E2E trio' turned out already deleted — only tombstone tests remain. - gate-census job: weekly EVALS_ALL gate-tier run — PR lanes are diff-billed, so without this the full gate census might never execute anywhere; with the hollow-shard guard it is a census-health check (exit 0 + zero executed tests fails), not just a test run. - failure notification is a concrete gh issue UPSERT (one tracking issue, commented per red week — never issue-per-week spam), with issues:write scoped to the report job. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: TESTING_INTERNALS covers the 2026-08 runner overhaul LPT-packed free suite + --record-durations, the emptied TREE_MUTATING mechanism, the sharded paid runner as the single selection engine, CI planner/executor/report with the fail-closed report and hollow-shard guard, the weekly coverage contract + exclusions policy, and the eval-budgets timeout tiers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(CLAUDE.md): testing prose matches the overhauled runners - bun run test: duration-packed shards + --record-durations; the trailing serial tree-mutating shard no longer exists - two-tier system: the sliced CI lanes (one engine local+CI), the weekly all-periodic coverage contract + exclusions, the gate census - periodic detach timeout 32400 → 37800 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(TODOS): close the absorbed test-infra items, file the overhaul follow-ups Closed with receipts: the periodic coverage contract (implemented as full weekly coverage + exclusions), the eval-harness observability P1 (verified already landed: heartbeat, incremental _partial persistence, live stderr + eval-watch), and the sidebar trio (already deleted — tombstones remain). Filed: matrix deletion after parity, the required-check maintainer decision, browse /tmp-namespace hardening, PTY boot-readiness waits, the single typed test registry, bun-native LPT swap, runBin/free-runner migrations, eval-list partial exclusion, phantom key cleanup, duration-weighted slicing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * v1.73.0.0: test/CI overhaul — green means green, suites restructured for speed Version + release notes for the audit-and-overhaul branch: every silently-skipping or never-running test class fixed and tripwired, the free suite duration-packed with the serial mutator shard dissolved, the paid lane re-platformed onto the sharded runner (planner/slices/ fail-closed report, parity phase), the weekly all-periodic coverage contract, eval-budget timeout tiers, and 95 new coverage tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ci): first-live-run fixes — executor history + two environment-blind assertions The sliced lane's first run (PR garrytan#2721) did its job: the planner and report worked, the manifest governed, and every failure had a name. Three were fixable on the spot: - executor + gate-census checkouts get fetch-depth: 0 — files with SELF-derived selection (the LLM-judge map, routing) walk git at module load, and selection is deliberately fail-closed on git errors, so the shallow checkout crashed those shards ('ambiguous argument main...HEAD'). The manifest still governs WHICH shards run. - landscape --toc gate: the exact toBe(3) landscape-page count was font-metric-dependent (3 on Amazon Linux, 2 on ubuntu CI — the same disease the file's own page-index comment warns about). Now a comparative invariant: --toc must not CHANGE the landscape count vs a baseline render. - paid-run-manifest parse test builds its manifest under EVALS_ALL so it never walks git (proven with GIT_DIR=/nonexistent). Remaining first-run failures are newly-exposed rot in gate files that had never executed in CI (skillify D1 refusal, session-intelligence context-restore, one tpa-apple-ban retry flake) — being probed separately; they are the lane WORKING, not the lane failing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(TODOS): file the three first-execution findings from the sliced lane's live run Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * v1.74.0.0: queue-advance — garrytan#2722 claims the v1.73.0.0 slot The version gate caught a live queue collision (its whole job); same MINOR bump level, next free slot per bin/gstack-next-version. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): per-shard CHROMIUM_PROFILE — the collision class duration packing exposed Nine test files launch in-process persistent contexts or daemons that default to the SHARED ~/.gstack/chromium-profile. Two concurrent shard processes on one profile dir kill each other's browser — observed live on CI once duration packing recomposed shards: handoff's launchPersistentContext died 'Target page, context or browser has been closed' (--user-data-dir=~/.gstack/chromium-profile in the call log) while a sibling shard's daemon logged 'Chromium process crashed'. Hash sharding had masked the collision by chance placement; handoff passes standalone everywhere. Fix at the runner, not per file: each shard child gets CHROMIUM_PROFILE=<shard-state>/chromium-profile (the documented env knob, same isolation idea as the existing per-shard TMPDIR). Files within a shard run serially, so sharing the per-shard profile is safe; config.test's resolution-order tests save/restore the env around their assertions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): landscape --toc gate asserts promotion PRESENCE, not counts Two rounds of CI receipts: the exact toBe(3) was font-metric-coupled (3 on Amazon Linux, 2 on ubuntu), and the baseline-comparison repair then failed 2-vs-3 across renders SECONDS apart in one CI job while the sibling no-toc test saw 3 — per-render image-promotion timing makes any count assertion here a coin flip. The sibling test owns exact promotion counts; this test's actual invariant is that --toc does not break the promotion machinery: >=1 landscape page + the TOC rendered. Also drops the second render (halves the test's runtime). Flaky per-render image promotion itself is worth its own look — noted in TODOS with these receipts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(TODOS): file the per-render image-promotion nondeterminism (receipts from PR garrytan#2721) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): per-FILE Chromium profiles for the nine in-process launcher files Completes the profile-isolation work: the per-shard CHROMIUM_PROFILE stopped cross-shard kills; these nine files launch in-process persistent contexts and could still collide with a lingering daemon a sibling file spawned on the SAME shard profile. Each now scopes a mkdtemp profile via beforeAll/afterAll (the module-scope-tripwire-safe pattern), cleaned up per file. All nine green solo and in combined runs, except the pre-existing commands+snapshot pairing — proven identical WITH and WITHOUT these edits (baseline receipts) — which is the daemon-lifecycle follow-up now extended in TODOS with this session's receipts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(browse): Chromium-crash exit is daemon-only — embedded launches never kill their host handleChromiumDisconnect unconditionally process.exit()ed. Correct for the standalone daemon (its supervisor/user must notice); suicidal when a TEST launches BrowserManager in-process: a mid-suite Chromium death exited the whole bun shard with no terminal summary — the exact truncation class the strict runner flags (observed live: CI shard 1 on eb23329 died at cache-concurrent-refresh right after a daemon-spawning gate test; with this fix the same pairing runs to completion and REPORTS instead of dying). The standalone entrypoint opts in via markDaemonProcess() under server.ts's import.meta.main gate — the same embedder contract its signal handlers already use (gbrowser phoenix keeps its own handlers). Embedded contexts now get the disconnect log line and continue. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): context-restore assertion is evidence-based, not prose-matching The test failed twice per run in TWO CI cycles while passing locally 4/4: the prompt said 'present the content' and the check grepped the FINAL message for exact phrases — local runs quoted the file, CI runs paraphrased ('the most recent context is from branch-b...') and the substring check lost the coin flip. - prompt now demands machine-checkable output: the newest file's '## Working on:' heading VERBATIM + a literal 'RESTORED: <filename>' marker (the mtime-scramble and cross-branch subject matter untouched) - assertion ordered strongest-first: RESTORED marker → legacy content phrases → tool-call corroboration (Read/Bash input naming the newer file, credited ONLY when the older file was never read — a both-files run must still present the right one) - the older-file negative got STRONGER: an explicit RESTORED marker naming the older file fails even if wintermute words appear elsewhere - sibling scan: context-recovery-artifacts got the additive prompt-side treatment only (quote the matched literals verbatim); its lenient 1-of-6 assertion deliberately unchanged 3/3 consecutive local green with all evidence classes firing (marker=true, content=true, toolNewer=true, toolOlder=false). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): skillify family — HOME==cwd broke project-skill registration Root cause (forensically pinned from stream-json init events + a kill-after-init probe): with HOME set EQUAL to the child's cwd, claude resolves <cwd>/.claude/skills as the PERSONAL skills directory and the seeded project-tier skills never register — the Skill tool returned 'Unknown skill'. The provenance-refusal test then improvised a refusal whose wording missed the regex (the deterministic CI+local red); the happy-path and approval-reject siblings passed only because their agents self-recovered by Reading SKILL.md manually — silently not exercising the Skill-tool path at all. All three tests now use HOME=<workDir>/home (a fresh subdir keeps the override's intent: child ~/.gstack writes land in the assertable sandbox, without the cwd collision). Refusal test additionally: a 'not registered/unknown skill' tripwire (a not-loaded skill can never pass as a refusal) and the refusal regex now matches assistant text only — the skill BODY echoed into the transcript contains the exact refusal message, so the old full-surface match could pass vacuously once the skill loaded. Sibling disk assertions sweep both $HOME/.gstack and cwd .gstack roots (positives and negatives). Verified paid: refusal 2x consecutive green with the skill's EXACT message rendered ('Launching skill: skillify' in-transcript), then the full file 5/5 green (~$1.35) with both siblings driving real Skill calls (25-27 turns each). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(TODOS): two of three first-execution findings fixed (skillify family, context-restore) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): context-restore gets a private home — the REAL root cause was fixture sharing The evidence-based assertion fix was treating a symptom. The slice artifact's embedded transcript showed the CI agent restoring 20260829-context-save-skill-test.md — the checkpoint the SIBLING context-save test wrote into the SHARED gstackHome checkpoints dir, which by filename-prefix ordering genuinely IS the newest. The agent behaved CORRECTLY; the test's fixture set was open to concurrent sibling writes, and bun --concurrent ordering differs between CI (save finished first) and local (restore listed first) — the entire local-green/CI-red split explained. The restore test now uses its own .gstack-restore-home (the whole home moves, not just the handed path — an agent deriving the dir from GSTACK_HOME/projects/<slug> must land in the closed set too). Full file 4/4 paid green with all evidence flags firing. Also: the on-failure shard-log artifact glob uploaded nothing — the Fix-bun-temp step points TMPDIR at /home/runner/.cache, so the spool lands there, not /tmp. Both eval workflows now glob both locations (this gap is why diagnosing THIS failure required digging transcripts out of the slice-results artifact). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(evidence): carry the real index mtime onto gstack-wtree's temp copy The stat-cache seed (cp of the real index) stamped the temp index "now", which defeats git's racy-git protection: an entry is only re-hashed when its cached mtime is not older than the index file itself, so a same-size rewrite landing in the same second as the last real index write looked non-racy, kept its stale stat-cache entry, and vanished from the fingerprint — evidence stayed FRESH after a source change. This is the CI flake in test/evidence.test.ts "allow-paths carve-out" (sub-second alignment on fast runners: expected STALE exit 1, got FRESH exit 0). touch -r restores the original index timestamp, reinstating the exact racy window git itself uses. Deterministic regression pin in test/review-log.test.ts reproduces the miss with pinned zero-nsec timestamps (fails on the old script, passes now); receipts: manual probe shows the fresh-stamped copy returning the clean tree for a same-size 'hello'→'howdy' rewrite while the mtime-carried copy detects it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): landscape gate bounds the promotion count instead of pinning 3 The alt-hinted image promotion rides the per-render measurement race already filed in TODOS (2-vs-3 landscape pages on renders seconds apart — CI receipts from PR garrytan#2721, now reproduced locally). Pin the two deterministic promotions as the floor and the three promotable blocks as the ceiling (anything above 3 means the veto leaked); the veto/portrait assertions remain exact. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Test <test@test.com>
…m benchmark, reuse ladder, instruction-tier digest (garrytan#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 (garrytan#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 (garrytan#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 garrytan#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>
…ssions reachable (garrytan#2733) (garrytan#2741) * feat(session-kind): explicit GSTACK_SESSION_KIND override; skill-start spawned gates keyed on kind (garrytan#2733) Claude Code subagents inherit the parent env byte-for-byte, so ambient markers classify them as the parent's kind and the spawned classification was unreachable outside OpenClaw. GSTACK_SESSION_KIND=spawned (step 0, spawned-only by design) lets a dispatching skill mark its subagent per command. skill-start now keys SPAWNED_SESSION and the spawned-session instruction block on the resolved kind (was raw OPENCLAW_SESSION), suppresses CONDUCTOR_SESSION for spawned sessions, gates all 11 interactive-onboarding blocks plus their ack-at-emit marker writes on kind != spawned, and adds a destructive-gate carve-out to the spawned block (conservative-continue, never prose-STOP). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(hooks): spawned-session escape in Conductor AUQ deny; override coverage in AUQ-error fallback (garrytan#2733) Hooks inherit the harness env, so a per-command GSTACK_SESSION_KIND prefix inside a subagent's bash can never reach them. Levers added: a deterministic [conductor][spawned] auto-choose deny for env-level spawned sessions (OPENCLAW_SESSION or session-wide GSTACK_SESSION_KIND), and a spawned escape sentence appended to both hooks' prose directives so a marked subagent that slips and calls AUQ resolves to auto-choose instead of prose-STOP. The sentence lives in one shared constant (hosts/claude/hooks/spawned-directive.ts) so the two paths can never drift; destructive semantics are unified to conservative-continue. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ship): Step 18 marks the document-release subagent spawned — env prefix + auto-choose prompt (garrytan#2733) The dispatch prompt now (1) frames the run as a SPAWNED subagent whose LAST line is machine-parsed, (2) instructs prefixing the preamble's gstack-skill-start invocation with GSTACK_SESSION_KIND=spawned on the same command line (template bash blocks don't share exports), and (3) resolves every AUQ gate to auto-choosing the recommended option, conservative on no-recommendation, never destructive. The JSON contract gains a required "decisions" array (auto-chosen gates, printed to the ship console — never embedded in the public PR body) and a placement clause so the skill's own doc-health summary stops competing with the LAST-line JSON. Tripwire pins added; codex/factory goldens refreshed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(auq-format): proactive SESSION_KIND=spawned rule ordered above the Conductor rule (garrytan#2733) The spawned classification previously existed only in the failure-fallback branch — a spawned session was invited to call AskUserQuestion and reach auto-choose via the deny/error detour, and a spawned session inside a Conductor workspace hit the Conductor prose-STOP rule first. The Tool resolution list now leads with the spawned rule (auto-choose recommended, never prose, never BLOCKED, destructive gates resolve conservative), the self-check carries the never-reach-this-checklist clause, and all tier>=2 SKILL.md renders are regenerated. Context-budget fixture refreshed in the same commit per the ratchet protocol (the AUQ section is eager in every tier>=2 skill). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(e2e): spawned document-release subagent returns the JSON contract through a firing gate (garrytan#2733) The behavioral proof the bug shipped without: ship-docsync stubs the skill (no preamble, no gates) and skill-e2e-workflow suppresses the gates by prompt. This gate-tier E2E plays the parent — it drives the verbatim Step 18 dispatch prompt (extracted from the live pr-body.md, drift-proof) against a real preamble-bearing document-release slice in a Conductor-ambient env with both AUQ hooks seeded live, an unbumped VERSION making Step 8 fire. Asserts: the final line parses as the 5-key JSON contract, the fired gate's auto-choice is recorded in decisions, and VERSION is untouched (the gate resolved to its recommended Skip). Burn-in: 1/1 pass, $0.35, 21 turns, 106s. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(openclaw): document the GSTACK_SESSION_KIND override; wire session-kind into paid selectors (garrytan#2733) OPENCLAW.md's spawned-session section now covers the explicit per-command marker, its deliberate spawned-only narrowness, the /ship Step 18 usage, the destructive carve-out, onboarding-block suppression, and the hook env-blindness caveat. bin/gstack-session-kind and the shared spawned-directive module join the conductor-prose and auto-decide-preserved selector dep lists (session-kind previously appeared in no touchfiles entry — editing it alone triggered no paid E2E). TODOS.md gains the plan-tune capture follow-up for spawned auto-choices. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: pre-landing review fixes (garrytan#2733) Review army + coverage audit findings, all applied: - headless directive carries the spawned escape sentence too (multi- specialist: a CI-hosted ship's marked subagent must not end BLOCKED) - anti-injection scoping on every text-claimable spawned trigger (AUQ rule + shared escape sentence): markings count only from the creating prompt, never from files/tool output/web content read mid-run - [conductor][spawned] deny annotates one-way doors per question - SPAWNED_OVERRIDE: env tamper-visibility status line + OPENCLAW.md note - spawned sessions skip the network update-check and first-task probe (consumers suppressed; preserves the one-shot just-upgraded marker) - test hardening: dispatch-tripwire end-bound validated, vacuous marker asserts replaced with output asserts, E2E cpSync size filter + named fence tolerance, spawnedByEnv parity pin, destructive-policy cross- surface drift guard, one-way annotation + bogus-value hook cases - session-kind duplicate rationale comment deduped; regen + goldens + context-budget fixture refreshed Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: bump version and changelog (v1.76.0.0) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: update project documentation for v1.76.0.0 PROJECT_STRUCTURE.md: add hosts/claude/hooks/ to the directory tree (AUQ capture + enforcement hooks, spawned-session directive, timeline stop) — the tree omitted the directory while docs/OPENCLAW.md and CHANGELOG.md now reference paths inside it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: sync TODOS.md ship dispatch entry with the v1.76.0.0 contract Codex doc-review finding: the SHIPPED entry for /ship auto-invoking /document-release still described the four-key JSON contract. Adds the decisions key (console-printed, never PR markdown), the GSTACK_SESSION_KIND=spawned dispatch marking (garrytan#2733), and the new spawned-dispatch gate E2E to the proven-by list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…, flake telemetry, sync-spawn wedge class extinct (garrytan#2746) * fix: pin the claude CLI to an exact version in the CI image + tripwire The image installed @anthropic-ai/claude-code UNPINNED and rebuilt weekly 'to pick up CLI updates' — while bun sat carefully pinned at 1.3.13 two RUN lines above. The PTY harness screen-scrapes this CLI's TUI, and that drift broke it three separate times (welcome-screen wedge on 2.1.233, skillify HOME discovery on 2.1.237, guard/freeze hooks on 2.1.162), each debugged as a flake first. Pin 2.1.251 (current latest), bump deliberately via a PR that runs the PTY gate, and enforce with test/ci-image-cli-pin.test.ts: any global npm install in Dockerfile.ci without an exact @X.Y.Z pin fails the free suite. The weekly ci-image cron stays as a cheap tag self-heal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: stamp the claude CLI version into every eval-store run record Three harness breakages were traced to claude-CLI TUI drift only after long flake hunts, because no run record said which CLI it actually exercised. EvalCollector now stamps claude_cli_version (claude --version, cached once per process, 'unknown' when the binary is absent) into both partial and finalized records — schema-additive optional field, no SCHEMA_VERSION bump. Correlating a flake wave with a CLI release becomes a grep over ~/.gstack/projects/<slug>/evals/ instead of archaeology. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: give the spinning-shard kill test load headroom (30s -> 90s) The test spawns and group-kills three real children (one a busy-loop burning a full core) while five sibling shard processes compete for eight vCPUs. Under full-suite load it blew bun's default 30s per-test ceiling at 30,009ms — while passing in isolation in 1.4s — and red the only required lane. Every assertion in it is event-based (statuses, group-kill proof, heartbeat lines); the sole latency claim is the <30s kill-deadline sanity bound, which stays. Explicit 90s headroom, not a weakened oracle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: green-by-skip census — skip counts in the classifier, all-skipped labeling in the paid runner bun's 'Ran N tests' line COUNTS skipped tests, so a codex/gemini shard whose every test self-skipped (binary absent on the runner — true of every CI runner today) exits 0, dodges the hollow-shard guard, and reads as coverage in the weekly census. The classifier now parses bun's ' N skip' / ' N pass' recap lines; ShardOutcome carries skippedTests; formatSummary and the fail-closed slices report label an all-skipped pass explicitly: 'all N tests SKIPPED — verified nothing'. Status stays 'passed' (external service availability is host state, not a repo regression) but the census can no longer mistake absence for coverage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: extract composite actions for eval-lane setup; surviving lanes gain the fail-fast registry verification 'Fix bun temp' x3, 'Restore deps' x5, 'Seed claude interactive config' x3, and 'Register gstack skills' x3 were byte-near-identical copies across the legacy matrix, the sliced lane, and the periodic lane — and only the MATRIX copy of register-skills carried the 19-line dangling-symlink + frontmatter fail-fast loop written after a silent 'Unknown command' + 35-min-timeout incident. Extract all four into .github/actions/ composites; the register composite carries the verification loop (generalized over the skill list), so the sliced and periodic lanes — the lanes that SURVIVE the matrix deletion — now inherit the check they had silently dropped. Matrix-job inline copies are left untouched: that job is deleted next. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: delete the legacy 17-row eval matrix — the sliced lane is the only paid lane Every PR paid twice: the hand-enumerated matrix (18 test files, 22.6 min, ~$21 API measured on run 33263204465) ran serialized AHEAD of the strictly superior sliced lane via 'needs: evals' — 35.5 min wall and ~2x paid spend for the same diff. 14 of 17 rows carried no tier:, so periodic Opus benchmarks leaked into every PR (the e2e-plan row alone: 12/12 tests, 21.7 min, $7.28 — the wall-clock bound of ALL of CI). Parity receipt (static, pre-deletion): the sliced lane's gate census (49 files, derived from the runner itself) strictly contains all 18 matrix test files, plus 31 files the matrix never ran. Pure deletion — one revert restores it. The PR comment moved into slices-report (same '## E2E Evals' upsert marker, now sourced from slice artifacts + carrying the fail-closed reconciliation verdict). plan-slices loses the needs edge; the dead workflow-level EVALS_TIER env goes with it. test/evals-workflow-matrix.test.ts (and its KNOWN_MATRIX_GAPS / KNOWN_TIER_UNSET burn-down ratchets — retired: the sliced census makes 'every gate file runs' true by construction) is rewritten as test/evals-workflow-wiring.test.ts: matrix stays deleted, planner/executor/ report tier + slice-count agreement, both surviving lanes on the shared register-skills composite with its fail-fast verification loop, PR comment survival. Expected: PR eval wall 35.5 -> ~13 min, per-PR paid spend ~halved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: provider-runner timeouts kill the whole process GROUP; codex/gemini inherit the orphan-drain hardening All three provider runners (claude/codex/gemini) killed only the direct child on timeout: tool subprocesses the CLI spawned survived as orphans holding our pipes open and burning shared API rate (observed: a 600s timeout stretching past 1400s; a stalled run once burned a core for 15 hours). gstack-detach's watchdog had the same shape one level up — killpg SIGTERM, 5s grace, then a direct-child proc.kill() that orphaned grandchildren. Fix: spawn provider children via node:child_process with detached (own process group) and killProcessGroup(SIGKILL) in the timeout handler — runShardChild's proven pattern, EPERM/ESRCH fallbacks included. The codex and gemini copies also gain the reader.cancel() + stderr Promise.race hardening only the claude copy had (they still carried the blocked-drain hang it fixed). gstack-detach's watchdog now group-SIGKILLs after the grace. Regression net: test/session-runner-groupkill.test.ts drives the REAL runSkillTest against a fake claude shim (PATH override) that spawns a grandchild and wedges — the run must classify timeout within budget and leave neither shim nor grandchild alive — plus source pins on all three runners (detached + killProcessGroup, no bare timeout kill, no Bun.spawn reversion). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: skill-e2e-opus-47 renders SKILL.md fixtures into a mkdtemp — never the live tree mkEvalRoot ran gen-skill-docs with cwd=ROOT, regenerating every in-repo SKILL.md mid-run while concurrent paid shards copyFileSync those same files in their beforeAll (EVALS_JOBS>=4 locally, 2 per CI slice) — a sibling could capture a half-regenerated or opus-rendered SKILL.md, and a timeout before afterAll stranded the whole tree at the wrong model for every later shard. A cross-shard race that could flake ANY concurrent paid test. Render via the --out-dir flag gen-skill-docs grew for exactly this reason (mirrors the repo layout, which is all the fixture reads), read the skill heads from the render dir, delete it, and drop the afterAll restore-regen entirely. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: claude CLI version resolves in the runner parent, never on a test thread Eng-review finding: getClaudeCliVersion's fallback is a SYNCHRONOUS spawnSync on the same thread that polls concurrent PTY/session tests — the judgePtyState blocking class this overhaul kills elsewhere. The paid runner parent now resolves it once (cached) and stamps GSTACK_CLAUDE_CLI_VERSION into every shard's env; eval-store short-circuits on the env var, and the fallback spawn's budget tightens 10s -> 3s (bounded one-time stall, records 'unknown' on a slow CLI). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: wire skippedTests end-to-end through runPaidShard The census unit tests hand-built outcomes and the classifier tests parsed strings; nothing proved a real child's ' N skip' recap flows into outcome.skippedTests and the formatSummary label. A commandFor fake now prints the recap shape and the test asserts the parsed counts, the all-skipped predicate, and the 'verified nothing' label. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: make the setup composites rerun-safe (codex diff-review hardenings) restore-deps: 'cp -r SRC node_modules' with an existing node_modules NESTS the copy and leaves stale deps active — rm first. register-gstack-skills: 'ln -snf' hard-errors under set -eu when a REAL directory occupies the gstack slot — clear a non-symlink leftover first. CI workspaces are fresh today; a reusable composite must survive dirty reruns. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: sweep — every sync spawn in the test trees carries a timeout (436 sites, 157 files) spawnSync/execSync/Bun.spawnSync BLOCK the main thread, so bun's in-process per-test timeout can never fire while one waits — a hung child (stdin read, network probe, dead daemon) wedges the whole shard until the runner's external wall-clock SIGKILL. This exact class reached main: free-tests run 33262077256, test/gstack-memory-ingest.test.ts (normally 2.3s) held shard 2 at the 360s wall while its five siblings finished in ~65s. Mechanical sweep in two waves (12 + 4 fan-out agents, every edit verified against its call site): default timeout: 30_000 (matches the free runner's per-test budget), 120_000 for genuinely slow ops (installs, builds, playwright, provider CLIs), helper wrappers fixed ONCE where call sites route through them. Sites that only LOOK like calls (string fixtures, grep needles, comments) were skipped with reasons — the enforcement commit that follows marks them exempt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: sync-spawn timeout tripwire — the wedge class stays extinct Free scanner over all test trees (test/, browse/test/, design/test/, make-pdf/test/, ios-qa, browser-skills): every spawnSync/execSync/ Bun.spawnSync call site must carry a timeout within a 30-line options window, or an explicit '// tripwire-exempt: <reason>' marker. Comment lines are skipped; exemptions are counted and ratcheted shrink-only (ceiling 6 = the 6 string-fixture/grep-needle sites where the pattern is CONTENT, not a call — marked in this commit). A scan-sanity test pins that the scanner still sees >100 real call sites so it can never rot to a vacuous green. Companion to the 436-site sweep in the previous commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: paid-lane flake telemetry — record-level attempts, flaky_retries, report surfacing bun --retry leaves a retried pass INVISIBLE in its output: a fail-then-pass prints the error detail but no (fail) result line and recaps as a clean pass (probed live on 1.3.10). So attempts are recorded where they cannot lie: EvalCollector.addTest stamps a 1-based attempt on same-name re-records (a retried test runs its body again and re-records), finalized runs carry flaky_retries, printSummary warns loudly, and the fail-closed slices report lists every passed-only-on-retry test — recorded and ranked, never blocking and never silent. Cross-model confirmed (codex reached the same don't-parse -the-stream conclusion independently). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: free-lane flake ledger — retry ON in CI, flaky-passes recorded and uploaded The runner's attribution-gated flaky-retry pass (cap 5, truncation veto) was OFF in the required lane and its FLAKY-PASS evidence was console-only — so a single timing flake red the merge gate while repeat offenders stayed unenumerable. free-tests.yml now sets GSTACK_FREE_RETRY_FLAKY=1 and points GSTACK_FLAKE_LEDGER at runner.temp; every flaky-pass appends a JSONL entry (SINGLE writer: the parent runner — no concurrent-append hazard by construction; fail-open with a loud warning so a broken ledger can never red the lane) and the artifact uploads UNCONDITIONALLY — a flaky-pass run is green, which is exactly when the evidence matters. Wiring pinned by free-tests-workflow-wiring; ledger behavior unit-tested incl. the fail-open path. Matches 2026 industry practice (retry for data, quarantine out of merge-blocking but never out of logging) with the repo's own receipts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: eval:flake-rank — the flake-telemetry dial Aggregates per-test series across every finalized eval-store run (shard dirs included) plus the free flake ledger: runs, fails, RETRIED PASSES (the flake signature), avg duration — ranked retries-first. This is the readable dial behind two policies: a flaky pass never blocks a merge but is always ranked here, and the WS16 required-check promotion needs weeks of clean flake-rank, not vibes. --json for machines, --dir for downloaded CI artifacts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: two-phase session timeout — silent APIs die at the startup grace, named The single spawn-armed timer charged API queue latency to the work budget: the recurring '0 turns / $0.00 / x3 attempts' failure with four budget-bump receipts (180->300s, 240->360s, 300->420s, 90->300s). Split: startup phase (no NDJSON byte yet) kills EARLY at min(grace, timeout) with the distinct exitReason 'timeout_startup' — an availability verdict, not transcript archaeology — and the work phase arms on the first byte for the REMAINING budget, so total wall never exceeds the timeout (tier envelopes are margin-free: tests pass timeout: CAPTURE_MS and bun-budget the same tier). Local grace 90s (observed queue latency 60-90s), CI floor 300s (TODOS-filed; shared runners queue harder), both pinned by the new grace tests with fake -claude shims covering the late-first-byte and silent-API paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: census integrity — 17 phantom selection keys deleted, reverse invariant added, gitignored dep patterns replaced, local map forks derived The merge-blocking gate census counted tests that could not run. Deleted (critic-verified against both quoted-occurrence and dep-registration liveness): 7 *-prosons-format keys with no declaring test, ship-plan- completion/-verification, review-plan-completion, design-shotgun-path/ session/full, autoplan-core (dead ~10 months), e2e-harness-audit (its namesake is a FREE-suite file), plus 2 dead LLM-judge keys and 2 free-file keys (budget-regression-pty, global-discover) misplaced in the PAID maps. Census: 191 -> 174 keys, gate 86 -> 78 honest. The new reverse invariant in touchfiles.test.ts makes the class structurally impossible: every key must be quoted in a living paid test file OR registered to an existing paid test file via its dep list (the constructed- name binding the 2026-08 self-registration sweep established) — zero exceptions needed today, with a live-file check on any future exception. Also: '.agents/skills/**' dep patterns replaced with the generator (scripts/gen-skill-docs.ts) — .agents/ is gitignored, so those patterns could NEVER match a git diff and review-template edits silently stopped selecting codex/gemini tests; the codex/gemini local touchfile maps are now DERIVED from the canonical map (loud throw if a key vanishes) instead of hand-forked copies that had already drifted. ios-qa-e2e demoted gate -> periodic: its gate declaration was never executable in CI (hardware exclusion only applies at tier=periodic), so every Linux PR planned a hollow shard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: routing journeys lose their answer key and end at the routing decision The journey tests exist to catch skill-DESCRIPTION regressions (touchfiles: */SKILL.md.tmpl), but the fixture CLAUDE.md shipped an explicit prompt->skill lookup table — with the answer key in context, a badly regressed frontmatter description still routed correctly, so the tests could not fail on the exact class they select for. The fixture now carries only the generic invoke-skills nudge; the frontmatter carries the routing load. Also capped all 10 journeys at maxTurns 2 / tools [Skill, Read]: only the FIRST Skill call is asserted, so 5 turns of Read/Bash/Glob/Grep was pure spend — roughly halves each journey's cost. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: retire decided A/B experiments; vendor the pre-cut fixture; ban raw-SHA fixtures Three one-shot decision experiments kept re-running weekly as N=1 stochastic comparisons — flaky by construction with near-zero remaining information: skill-e2e-auq-repetition-cut-ab (its own header: gate "passed pre-landing, approved 2026-08-25"), skill-e2e-preamble-script-ab ("demoted post-Phase-3"), and opus-47's fanout arm-vs-arm (parA >= parB across two SINGLE stochastic runs — a coin flip). Deleted, with their selection keys; the SDK overlay-harness stays as the maintained instrument for the next experiment, and opus-47 keeps its routing-precision cases. verboseSkill() now reads the VENDORED test/fixtures/auq-pre-cut-...-SKILL.md instead of `git show ab66193^:...` — a branch-local ref that dies on branch prune and already failed on shallow clones. New free tripwire (test/git-ref-fixture-tripwire.test.ts) bans the raw-SHA fixture class outright: quoted SHA:path rev-specs and gitRef-style hex defaults in the test trees fail the suite with the vendor-instead instruction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: demote plan-ceo-review-expansion-energy to periodic Opus generator + a subjective 2-axis >=4/5 LLM-judge threshold sat in the MERGE-BLOCKING gate — the exact class its sibling posture tests were demoted for, with a receipt (a +21-line preamble change once flipped the score). CLAUDE.md's own tiering rule: Opus model test -> periodic. The weekly lane keeps the regression signal; merges stop paying a judge- temperament tax. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: paid shards get per-shard TMPDIR + CHROMIUM_PROFILE isolation and a kill-path cleanup backstop The free runner treats this isolation as MANDATORY (two concurrent shards on one Chromium profile kill each other's browser; shared tmp cross-contaminates) — the paid lane had none of it. Doubly load-bearing here: a shard that hits its 30-min wall is group-SIGKILLed, so per-test afterAll cleanup never runs; the rmSync backstop is the only thing keeping wedged runs from accumulating full git-repo workspaces in the shared tmpdir forever. This is the DAG prerequisite for raising EVALS_JOBS (next commit) — more concurrency on shared state amplifies exactly the shared-tree race class opus-47 exhibited. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: paid-runner defaults 4x4 -> 8x2 — halve the local gate worst case 39 of 75 skill-e2e files hold exactly ONE test, so within-shard concurrency was dead weight for most shards: 4 jobs x 4 concurrency yielded only ~4-6 real in-flight sessions and a 13-wave local gate worst case (~6.5h). 8 jobs x 2 gives ~10-13 in-flight — under the documented-safe ~15 — and ~7 waves (~3.3h worst case). CI lanes keep their explicit EVALS_JOBS env (2 per slice; 4 for gate-census); this changes local defaults. Rollback trigger: sustained 429 storms in the WS1 telemetry across 2 PR cycles. test/eval-detach-timeout-floor.test.ts recomputed green (the raise LOWERS the worst-case floor). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: SHA-pin every action in the secrets-bearing eval lanes evals.yml and evals-periodic.yml execute PR-authored code with three provider API keys in env, yet rode mutable action tags (@v7/@v8/@v2/@v4) — while quality-gate.yml, osv-scanner.yml, and dependency-review.yml already model the SHA-pin pattern. All 30 uses sites across both lanes now pin the exact commit (tag noted in a trailing comment); dependabot's github-actions ecosystem keeps them fresh via PRs instead of silent tag moves. Pulled forward from the plan's endgame on the CEO-review + outside- voice agreement: supply-chain pins on secret lanes go first, not last. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: sweep wave 3 — the execFileSync family gets timeouts (90 sites, 17 files) The tripwire's regex covered spawnSync/execSync/Bun.spawnSync but not execFileSync — an entire blocking sync-spawn API family that could reintroduce the shard-wedge class undetected (ship review army). Same mechanical recipe as waves 1-2: timeout: 30_000 default, 120_000 for slow ops, shared wrappers fixed once, string-needle sites skipped with reasons. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: review-army + adversarial test hardening - Tripwire scans execFileSync too (ceiling 8: two more grep-needle string exemptions); merge-introduced timeout-less spawnSync in question-preference-hook fixed — the tripwire caught a site that landed on main AFTER the sweep, on its first day. - gstack-detach gains TWO watchdog kill regression tests: TERM-immune grandchild (the killpg-after-grace escalation) and the leader-dies variant (the pgid-at-spawn fix — the case the first test cannot see). - eval-flake-rank gets its unit suite (final-attempt accounting, artifact exclusion, shard recursion, recency bound). - Groupkill/startup-grace shim markers are per-run unique (pid-suffixed sleep durations): sibling Conductor worktrees run free suites with no machine lock, and fixed markers let one run pgrep/pkill the other's shims — a cross-run flake inside the anti-flake tests. - flake-ledger test pins the project-scoped local default; stale empty section headers in touchfiles-data deleted (they invited entries under deliberately retired categories). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: adversarial-review runtime fixes across the telemetry + kill paths - session-runner: exit-labeling keys off 'exit', not 'close' — an orphan holding the pipes could relabel a REAL exit (auth failure) as 'timeout_startup' availability noise; the kill path still always group-kills and cancels the reader (labeling and unblocking are separate concerns). Work phase arms on a flag, not firstResponseMs===0 (a same-ms first byte left the startup timer live all run). The CI startup grace is now a real FLOOR (Math.max), matching its name and pinning test. - gstack-detach: pgid captured AT SPAWN (== child pid under start_new_session) — resolving it after the grace raised ESRCH once the leader died on SIGTERM, orphaning TERM-immune grandchildren forever. - test-free-shards: ledger entries carry branch + git_sha (rev-parse split: '--abbrev-ref HEAD HEAD' printed the branch twice and recorded it as the sha); local ledger default is per-PROJECT, not the machine-global tmpdir. - eval-flake-rank: per-LINE ledger parse (one torn JSONL line vanished the whole series), 60-day recency bound (transcript-bearing files are MBs), shared isFinalizedEvalResultFile predicate (the artifact-taxonomy rule lived in three places); eval-store exports the predicate and finalize stops computing flakyRetries twice; paid-shards cleanup uses async rm (a SIGKILLed shard's git-workspace teardown blocked every sibling's stream classification on the parent event loop). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: CI trust-boundary + fail-closed repairs (adversarial findings) - Token/exec separation restored: slices-report (runs PR-authored code: bun install + the reconcile runner) drops to contents:read; the PR comment moves to a NEW slices-comment job holding the write token with ZERO repo code — no checkout, no bun, only downloaded artifacts + jq/gh. $GITHUB_ENV/BASH_ENV persistence is job-scoped, so the split is the boundary. The matrix-era report job had this property; the consolidation had regressed it. Pinned by the wiring test. - Reconcile exit captured via PIPESTATUS[0] in BOTH lanes: GitHub's default run-step shell has no pipefail, so `$?` after `| tee` was tee's exit — the fail-closed gate was silently fail-open. Wiring test pins it. - PR comment: final-attempt accounting restored the dropped COST accumulation (the dial read $0 forever), flaky passes render as the warning they are (never as failures), and a malformed tests[] artifact skips that file instead of aborting the whole comment under bash -e. - Remaining mutable action tags pinned (free-tests upload-artifact, ci-image checkout/docker trio — the image publisher holds packages:write and feeds the secret-bearing lanes). restore-deps fallback installs --frozen-lockfile; register-gstack-skills validates skill names before its rm -rf. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: bump version and changelog (v1.77.0.0) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: update project documentation for v1.77.0.0 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: cross-model doc-review fixes — flake-ledger env knobs, CI retry-on note, stale version comment Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: correct CHANGELOG receipt numbers to measured values Gate census keys: 78 -> 77 (bun-imported E2E_TIERS count). Sweep receipt: 586 sites/176 files -> 499 sites/146 files, measured by running this branch's spawnsync-timeout-tripwire against origin/main (exit 1, 499 violations across 146 unique files; green on this branch). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: slices-comment creates the PR comment via REST — the write-token job has no git context The token/exec split gives slices-comment NO checkout by design, and gh's pr-comment subcommand resolves the repo FROM git — it died with 'not a git repository' on PR garrytan#2746's first run (the update-existing PATCH path was already explicit-repo REST and worked). Create now posts through gh api repos/.../issues/N/comments, and the wiring test pins that no git-context-requiring comment call can creep back into the job. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: startup-grace probes clear CI for local semantics; new probe pins the floor clamp The two shim probes pass explicit 2s/4s graces, but in CI the runner clamps any explicit grace up to the 300s floor (deliberate adversarial-review fix), so 'silent API killed at the grace' died at the 30s work cap instead of 2s — a deterministic red on every CI run, green locally. The probes now pin LOCAL semantics with CI cleared (same save/restore pattern as their PATH shim), and a fourth probe pins the clamp itself: CI=1 + 2s grace + 6s timeout must kill at the 6s cap, still in the startup phase — proof an explicit low grace cannot bypass the floor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…n from 105, 18 community PRs absorbed, upgrade path can't eat installs (garrytan#2752) * fix(auq): spawned trigger is objective — explicit declaration or STATUS echo, never inference (periodic-lane AUQ collapse) The v1.76 spawned rule's parenthetical '(or your dispatch prompt marks this session as spawned)' let the model INFER spawned status from a scripted-looking prompt in a CI-looking session and silently auto-choose every review-phase question: reviewCount=0 across the plan-review periodic E2Es (weekly run 33363624506, 9 of 14 failed shards; reproduced locally, zero AUQ fingerprints). Env and hook paths were excluded by inspection: hermetic children echo SESSION_KIND: interactive (CLAUDE_CODE_ENTRYPOINT=cli beats CI markers) and the question-preference hook isn't installed there. The trigger is now objective: the echoed SESSION_KIND: spawned STATUS line, or an EXPLICIT dispatch-prompt declaration ("you are a SPAWNED subagent") — declared, never inferred — with an absence-safe interactive fence: CI env vars, scripted-looking or pasted prompts, and write-to-this-exact-file instructions are NOT spawned markers. The prose channel stays because Task-tool subagents inherit the parent env (no spawned prefix) — their dispatch prompt is the only signal; #2733's env-prefix channel is untouched. 19 carve skeleton ceilings re-pinned with measured values (+~440 bytes/skill); ship goldens refreshed for all three hosts; resolver pins extended with the no-inference regression tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: mktemp failure aborts loudly at all three skill-content sites; failed upgrade swap restores the backup (#2679) An empty $(mktemp) result silently disabled the redaction pass (redact-doc resolver, ship pr-body) and made /gstack-upgrade's vendored path destructive: clone lands at "/gstack", the swap mv fails, and rm -rf then deletes BOTH the live install's backup and "". All three sites now guard the assignment with a loud exit; the vendored block additionally restores the backup when the swap fails (same failure class — backup deletion after a failed mv) and the GitLab MR path sends the SCANNED file's bytes instead of re-rendering an unscanned heredoc. bin/gstack-redact rejects an explicit empty --from-file path instead of silently falling through to stdin. Receipts: 6 of 8 new regression checks fail on a v1.77.0.0 scratch worktree. Fixes #2679 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(auq): the interactive fence classifies the session — it never nudges ask-count Burn-in run 1 of the periodic repro overshot the review band (reviewCount=8 > CEILING=7) with the fence's 'when unsure, ask' tail: that phrasing is a quota nudge, not a classification default. The fence now states it only classifies the session and never changes how many questions the skill asks. Pin added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ci): OSV suppression config actually loads — explicit global --config + expiring, reasoned ignores The ignore file was inert from v1.65.0.0: OSV-Scanner only auto-discovers configs named osv-scanner.toml (no leading dot) and applies them per-directory, so the root config never covered lib/diagram-render/bun.lock either way. The workflow now passes --config=.osv-scanner.toml globally. Every IgnoredVulns entry carries a reason with an upgrade trigger and an ignoreUntil expiry (~90 days) so suppressions must be re-justified. A wiring test pins flag ↔ filename ↔ entry hygiene so the file can never silently go inert again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(deps): dependency wave — 105 OSV advisories → 3 reasoned suppressions, all lanes verified on the pinned scanner Root: overrides pin ip-address 10.3.1 (defeats BOTH nested nodes — socks' range pull and express-rate-limit's exact 10.1.0 pin, which a top-level bump provably cannot reach) and sharp 0.35.0 (GHSA-f88m, HIGH; transformers still pins ^0.34 upstream — smoke-tested round-trip); marked ^18.0.11; full in-range lockfile refresh clears hono, fast-uri, protobufjs, qs, body-parser, nanoid, uuid, immutable and friends. lib/diagram-render (via its own build-script contract: exact pins edited, fresh lock, dist rebuilt): mermaid 11.16.1, @excalidraw/excalidraw 0.18.1, @excalidraw/mermaid-to-excalidraw 1.1.2 → 2.2.2 — the 1.x line exact-pinned mermaid 10.9.x and dragged the entire duplicate mermaid-10 advisory chain (dompurify 3.1.6, nanoid 3.3.3, lodash-es); the bundle shrinks 9.96 → 7.59 MB with the duplicate mermaid gone. Nested exact pins that survived get scoped overrides (nanoid 5.1.16, lodash-es 4.18.1). Verification: clean-worktree frozen-lockfile installs (root + nested) + the SAME osv-scanner release the action pins (v2.3.8) with the workflow's exact scan-args → exit 0, 'No issues found'. Smoke tests cover the override surfaces (sharp round-trip, ip-address lockfile assertion, marked parse); socks + diagram-drift suites already pin the rest. Supersedes #2695 (its own lockfile kept socks/ip-address@10.2.0; @anupamme's report credited for the parallel diagnosis). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(gbrain-sync): stub pgrep so the pin case is hermetic The only non-dry-run --code-only child hits #1734's PATH-resolved autopilot probe. A live host daemon is a correct refuse; the test cannot inject processRunning. Neutralize pgrep in the fixture bindir instead of adding a production env hatch. Co-authored-by: Cursor <cursoragent@cursor.com> * test(gbrain-sync): blank inherited GBRAIN_HOME in the pin child Lock paths are checked before pgrep. Spreading process.env let a runner GBRAIN_HOME with a live lock refuse the case before the stub ran. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: point ship design-checklist at installed gstack/review path The /ship Design Review step skipped the checklist because the generated path omitted the gstack/ install segment. Sync the generated skill doc and pin a regression assertion. Co-authored-by: Cursor <cursoragent@cursor.com> Wave-amended: goldens regenerated against the wave tree (author's golden commit 8e7a03ca superseded) * fix(codex): a CLI that cannot execute no longer reports CODEX_MODE: ready Follow-up to #2477. The model probe it added does a real round trip, but its final branch is the `else` of a "model 400" grep, so it swallowed spawn ENOENT, non-executable binaries and missing vendor payloads alongside genuine network timeouts. All three are deterministic — retrying never helps — yet they landed in the fail-open bucket and resolved to `ready`, so every Codex pass was skipped in silence and the review reported itself complete. Observed live: @openai/codex was on PATH with an empty vendor/aarch64-apple-darwin/codex/ directory. gstack said `ready` for two months while no Codex pass ran. Three changes: - `_gstack_codex_model_probe` classifies deterministic install failures (exit 126/127, or stderr matching ENOENT/ENOEXEC/EACCES/"cannot execute binary file") as MODEL_UNUSABLE_INSTALL, exit 2, never cached — a reinstall is picked up on the next probe. Exit 124 and genuine transients still fail open, which is what #2477 intended. - The preflight chain captures the probe's code instead of testing it for truthiness, so exit 2 routes to a new `broken_install` mode whose remedy is `npm install -g @openai/codex` rather than "check your model pin". A missing binary and an unusable model are different problems with different fixes. - `_gstack_codex_version_check` no longer reads a broken CLI as healthy. It ran `codex --version 2>/dev/null | head -1`, which captures head's status, not codex's — and 2>/dev/null discarded the one diagnostic available. It now captures the real exit code and warns on non-zero. Empty-but-successful output stays silent, per the existing "empty output → OK" case. Tests: 6 added to test/codex-hardening.test.ts covering both broken-install shapes, the exit-2 contract, no caching, the transient still failing open, the model 400 still classifying as MODEL_UNUSABLE, and the version-check warning. 845 pass / 0 fail across all 8 suites touching the changed files. Closes #2742 Wave-amended: autoplan hand-maintained preflight chain completed (tmpl+render); install-signature grep gated on failed spawn only; goldens regenerated against the wave tree (author's golden commit 5797d326 superseded); +2 tests * feat(redact): add Groq, Tavily and Notion API key patterns * fix(redact): stop reporting .env.local as an internal hostname `internal.hostname` ends in `.local|.prod|.staging|…`, so `.env.local` matches on `env.local` and a dotenv FILENAME is reported as a leaked internal host. The collision is not exotic. It fires on `--env-file=.env.local` in an npm script, `.env.staging` in a README, `.env.prod` in a .gitignore — ordinary lines on branches that leak nothing. Measured on one private repo, three of four MEDIUM findings in a routine push were this, and the fourth was a deleted localhost URL. That ratio is the real cost: a scanner that reports package.json is one people learn to skim, and skimming is how the HIGH finding it exists for gets missed. The guard follows the `insideUuid` precedent and stays deliberately narrow — it exempts only a span beginning `env.` immediately preceded by a dot, i.e. the literal `.env.<suffix>` form. `api.corp.local`, `build-7.internal` and `myenv.local` all still report. The test pins both directions, and the negative controls are the point: an exemption written as "any span ending .local" would pass the dotenv half while quietly gutting the pattern for every real host. Verified red/green — with the validate hook removed, exactly the 6 dotenv cases fail and all 9 real-host controls still pass. * fix: don't flag git SSH remotes as pii.email `pii.email` matches the `git@github.com` inside `git@github.com:acme/widgets.git`. That is a transport user@host, not a person's address, so any diff touching a clone URL -- a deploy config's repo URL, a submodule entry, a README clone line -- draws a spurious MEDIUM from the pre-push hook. Suppressed by URL shape rather than by adding `git` to EMAIL_ALLOW_LOCALPARTS. A bare `git@` allowlist entry would also suppress a genuine address at a domain that merely begins with "git" (git@gitmail.com), converting a false positive into a false negative -- the worse failure for a guardrail. Two shapes are accepted: - `<user>@<host>:<path>.git` for ANY host, covering self-hosted remotes, plus the equivalent ssh:// URL form. - `git@<known-host>` for github.com, gitlab.com, bitbucket.org and ssh.dev.azure.com, whose bare form appears in docs and in `ssh -T git@github.com` connectivity checks with no path at all. Matched exactly, so gitmail.com is unaffected. emailAllowed now receives the normalized text and the span offset so it can see that surrounding shape; it had only ever been passed the matched span. Tests pin both directions: the SSH remotes go quiet, and a real address still fires -- including at a git host (alex@github.com) and at a git-prefixed domain (git@gitmail.com). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(redact): install-prepush-hook refreshes a stale managed hook The marker check returned before the only writer, so once a repo had the hook, no later change to the wrapper could ever reach it. The `printf x` fail-open fix (v1.64.0.0) has still not landed in any repo that received the hook before it, and a wrapper naming a gstack that has since moved stays pointed at a dead path for the same reason. Compare the body against what this version generates: rewrite on drift, stay a no-op when identical. The chained pre-push.local is untouched on both paths. The existing trailing-newline regression test cannot catch this — it installs into a repo with no prior managed hook, the one case that was never broken. * fix(redact): install-prepush-hook refreshes a stale managed hook The marker check returned before the only writer, so once a repo had the hook, no later change to the wrapper could ever reach it. The `printf x` fail-open fix (v1.64.0.0) has still not landed in any repo that received the hook before it, and a wrapper naming a gstack that has since moved stays pointed at a dead path for the same reason. Compare the body against what this version generates: rewrite on drift, stay a no-op when identical. The chained pre-push.local is untouched on both paths. The existing trailing-newline regression test cannot catch this — it installs into a repo with no prior managed hook, the one case that was never broken. Wave-amended: spawnSync timeouts added to the new tests (v1.77 sync-spawn tripwire) * refactor(redact): name the SSH-remote path lookahead constant Wave polish on the #2734 absorption: the 512-char scp-path lookahead window follows the UUID_CONTEXT_CHARS named-constant convention instead of a magic number at the slice site. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(config): reject malformed cross_project_learnings at set A typo was stored with exit 0, so the feature stayed off and the first-run prompt never returned. Reject like codex_reviews; do not coerce. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(gbrain-detect): classify gbrain >= 0.43 held-lock refusal as engine-locked gbrain 0.43+ refuses a held PGLite lock with exit 1 and the message "GBrain's local database is already open through `gbrain serve` (MCP, PID N)" instead of the pre-0.43 exit 124 + "connect timed out" that the #2194 branch matches. The message matches no known pattern, so the classifier falls through to the defensive broken-config default — and Step 1.5 of /setup-gbrain and /sync-gbrain then tell the user to move a perfectly healthy config.json aside and re-init the engine. Reproduced live on gbrain 0.43.0.0, 0.44.0.0 and 0.46.30.0: with a serve holding the lock, gstack-gbrain-detect reports gbrain_local_status=broken-config; after stopping the serve it reports ok with the same untouched config. Match on the stable substring "already open through", mirroring the existing #2194 branch semantics: engine-locked for pglite, broken-db otherwise. Adds a fake-gbrain behavior for the 0.43+ refusal plus two cases (pglite -> engine-locked, postgres -> broken-db). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(memory-helpers): a slow gitleaks probe no longer disables secret scanning `gitleaksAvailable()` cached every failure the same way, so a 2s timeout on `gitleaks version` was recorded as "the binary is absent" for the rest of the process. One busy moment and the whole ingest ran unscanned behind a single stderr line — a fail-open outcome decided by machine load rather than by anything about the machine's setup. The caller only acts on `scanner === "gitleaks"`, so every later file was written with no scan and no second warning. The probe now classifies three outcomes. ENOENT (and a present-but-unusable binary: bad exit, EACCES) stays cached — that is a fact about the box, and re-probing it per file would be waste. A timeout gets one retry on a 10s budget, and if that also expires nothing is cached: the file is reported unscanned, the warning says so in those words, and the next file probes again. Observed under the 7-way sharded free-test runner, where spawning a shell script inside a temp bin dir took longer than the 2s budget. Tests: the retry path, the no-cache-on-timeout path (the second call must re-probe), and the cached-absent path. The fake gitleaks hangs for 30s rather than racing a short sleep against a short budget, and the budgets are chosen so load cannot flip an outcome: 30s where the retry MUST answer, 800ms where the probe MUST expire. An earlier draft used 1s/5s and flaked under the same shard runner this commit is about. The existing probe test pinned `detect` to calls[1], which a retry breaks; it now asserts the order instead of the index. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0111Mq3JGwZDcstn5wYcbhSw * fix(make-pdf): pdftotext version and flavor probe returns unknown on poppler describeBinary reports version="unknown" flavor="unknown" for every poppler install, so logDiagnostics prints nothing useful on the most common implementation. Two independent causes: 1. poppler writes the -v banner to stderr and exits 0. execFileSync returns stdout (empty) and does not throw on a zero exit, so the stderr fallback in the catch block is unreachable. The in-code comment already notes poppler exits 0, but only the throwing path reads stderr. 2. flavor is matched against the version line alone. poppler prints "pdftotext version 26.06.0" on line 1 and names itself on line 2, "Copyright ... The Poppler Developers", so even a working stderr read yields "unknown". Switch the probe to spawnSync, which returns both streams regardless of exit status, match the version banner rather than assuming line 0, and derive the flavor from the full output. Measured on poppler 26.06.0 (Homebrew, macOS), same machine and binary: before: { version: "unknown", flavor: "unknown" } after: { version: "pdftotext version 26.06.0", flavor: "poppler" } xpdf is unaffected: it exits non-zero and names itself on line 1, so it resolved correctly before and still does. Tests use shell shims reproducing each vendor's banner, stream and exit status, since a real pdftotext cannot be assumed present in CI. Two of the four fail on this commit's parent; the xpdf and no-banner cases pass there and are included as regression guards rather than red-proofs. * fix(open-gstack-browser): pre-flight cleanup never killed the stale daemon Step 0 read the old pid with `grep -o '"pid":[0-9]*'` and Step 2 read the port the same way. Neither can match. Every writer of that file in browse/src/server.ts serializes with `JSON.stringify(state, null, 2)`, so the bytes on disk are `"pid": 12060` — colon, space, digits. The failure was silent in the worst way. `_OLD_PID` came back empty, the kill never ran, browse.json was deleted anyway, and the next `connect` died with "existing daemon has different config (proxy/headed mismatch)" — an error pointing at proxy/headed flags rather than at the cleanup that no-opped. Caught against a daemon left over from a reboot: the operator was told to check flags they had never passed. Both patterns now accept optional whitespace. The new tripwire does not match strings — it RUNS the snippets the skill hands the agent, against a state file written exactly the way the server writes one, and asserts pid and port come back out. A third case pins the coupling to `JSON.stringify(state, null, 2)`, so a switch to compact JSON surfaces as a failing expectation rather than as silence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0111Mq3JGwZDcstn5wYcbhSw * test(make-pdf): clean up the pdftotext shim tmpdir after the suite Wave polish on the #2690 absorption: the describe-scope mkdtemp left one directory per run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(browse): honour CHROMIUM_PROFILE in cli profile-lock cleanup cli.ts resolved the Chromium profile dir with a hardcoded $HOME/.gstack/chromium-profile, while browser-manager launches the profile returned by config.resolveChromiumProfile(), which honours CHROMIUM_PROFILE and GSTACK_HOME. killOrphanChromium() and cleanChromiumProfileLocks() are called with no argument, so whenever CHROMIUM_PROFILE was set they cleaned locks for, and killed Chromium on, the DEFAULT profile rather than the one being launched. Starting a browser with a custom profile therefore evicted an unrelated browser running on the default profile. Delegating to resolveChromiumProfile() also picks up GSTACK_HOME and os.homedir(), so the cleanup path now matches the launch path on Windows where HOME is frequently unset. * fix(auq): the interactive fence is quota-silent — it defers to the skill's own decision points Burn-in calibration: run 1 (fence tail 'when unsure, ask') overshot the plan-ceo review band at reviewCount=8; run 2 (tail mentioning 'HOW MANY questions') undershot at 1. Any ask-count language in the fence anchors the model in one direction or the other. The tail now says only: classify as interactive, then follow the skill's own decision-point instructions exactly as written. Pins updated to forbid count language in either direction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(browse): pin cli.ts profile-dir wiring to the canonical resolver Wave-added coverage for the #2732 absorption: a 6-line fix with zero tests is how the hardcoded path shipped in the first place. resolveChromiumProfile's env behavior is already pinned in config.test.ts; this pins cli.ts's delegation and forbids the hardcoded path from returning. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(browse): preserve return value for async IIFE expressions in js/eval (#2727) Wave-amended: test moved to browse/test/ (browse unit-test convention); trailing-semicolon normalization kept — it is load-bearing for the expression wrapper * fix: bin writers drop data on Windows paths with an apostrophe Two independent Windows git-bash bugs in the bin writers, both silent because callers invoke these scripts with 2>/dev/null and do not check the exit status — a hard failure was indistinguishable from success. Bug 1 — apostrophe in the checkout path breaks the bun -e program. gstack-learnings-log, gstack-question-log and gstack-telemetry-log build a bun -e program as a double-quoted shell string and interpolate SCRIPT_DIR into a single-quoted JS import specifier. A path such as C:/Users/Someone's PC/... closes the JS string literal early and Bun fails to parse ("Expected ; but found s"). Every learning write and every plan-tune question event no-oped; telemetry error redaction fell to its fail-closed null path. The #1950 cygpath -m guard did not cover this — cygpath normalises the drive form but does not remove the apostrophe. Fixed by not interpolating the path at all: cd into the module root and use a relative import specifier, which is immune to apostrophes, spaces, backslashes and MSYS paths alike. The one remaining interpolated data path in gstack-developer-profile (readFileSync of PROFILE_FILE) is passed via the environment instead, matching do_log_session in the same file. Bug 2 — gstack-developer-profile --derive fails on an MSYS-form GSTACK_HOME. GSTACK_HOME defaults to $HOME/.gstack, which under git-bash is /c/Users/..., and Bun on Windows cannot open that form (ENOENT). This script carried no cygpath guard at all. Fixed by normalising GSTACK_HOME once, before PROFILE_FILE / LEGACY_FILE / the events path are derived from it, so all three pick up the normalised value. Adds test/hostile-path-writers.test.ts, which runs the bins from a directory whose name contains an apostrophe and asserts that rows are ACTUALLY WRITTEN (not merely that the exit code is 0 — exit-code-only checks are what masked bug 1). The apostrophe repro is OS-independent: SCRIPT_DIR derives from the script's own location, so a copied checkout under a hostile directory name reproduces bug 1 on Linux/macOS CI too. Wave-amended: all four writers unified on the env-var import pattern the PR already used in gstack-developer-profile (no CWD-dependent module resolution) Wave-amended: all four writers unified on the env-var import pattern the PR already used in gstack-developer-profile (apostrophe-safe without CWD-dependent module resolution); import-shape pin updated * fix(memory-ingest): stop two silent transcript-ingest failures Two independent bugs made transcript pages silently fail to reach the brain. 1. Frontmatter fence gluing. buildTranscriptPage() built the closing "---" with no trailing newline, and session bodies always start with "## ", so the rendered page ended "...---## User". gbrain's frontmatter matcher (/^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/ in src/core/markdown.ts) requires the closing "---" to end its own line, so it skipped the glued fence, latched onto the next standalone "---" in the transcript body, parsed the prose between as YAML, and dropped the page with "Invalid YAML frontmatter". Transcripts with no later "---" fell back to body-only, silently losing their frontmatter. Fix: emit the fence on its own line with a blank separator, matching renderPageBody()'s artifact branch. 2. Slug collisions. Two source files can map to one path-derived slug (a session resumed under the same id on one day, or two ids sharing a 12-char prefix). writeStaged() names each file "${slug}.md", so the second overwrote the first; gbrain collected N-1 of N staged files and the reconciliation guard failed the whole batch every run. Fix: disambiguateSlugs() keeps the first occurrence and gives each later collider a stable "-<sha8(source_path)>" suffix (deterministic, and slug + page_slug move together so writeStaged, the failure mapping, and state recording agree). Exports buildTranscriptPage, renderPageBody, and disambiguateSlugs for tests. Adds regression tests for both failures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Wave-amended: contributor's local-workaround docblock note removed; issue refs retargeted #2653 (closed by its author) -> #2724 (the live 887-staged-to-0-ingested report) * fix: keep feature markers in GStack state * fix: align feature marker seeding with GStack state Wave-amended: seeding relocation re-applied to the composite action (v1.77 moved CI seeding out of the inline workflow steps the original commit edited); wiring tripwire re-pointed accordingly; stale marker comment updated * chore(upgrade): migrate feature-discovery markers to GSTACK_HOME Follow-through on the #2748 absorption: existing installs answered the continuous-checkpoint and model-overlay prompts with markers beside the install; v1.78 reads them from GSTACK_HOME. Copy them once so nobody gets re-prompted. Idempotent, non-fatal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(land-and-deploy): check fork branch in head repo Wave-amended: gh leaves .headRepository.nameWithOwner empty (verified live against gh 2.83) — owner/name now composed from headRepositoryOwner.login + headRepository.name so reconciliation is not a permanent no-op; fork branches get report-not-delete (maintainers lack fork push rights); pins updated * test: spawn timeouts on the #2748 marker tests (v1.77 sync-spawn tripwire) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: spawn timeouts on absorbed-PR tests (v1.77 sync-spawn tripwire) The absorbed community tests (#2748, #2676, #2714, #2720) were authored before the v1.77 tripwire required a timeout on every sync spawn in the test trees. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(gbrain): a slow --version probe classifies as timeout, never no-cli (#2716) resolveGbrainBin's bare catch collapsed 'gbrain missing' and 'gbrain present but the 2s --version budget expired' into the same null — freshClassify then said no-cli, which the --is-ok whitelist from #1964 does NOT forgive, so a bun-shim install on a loaded POSIX box silently lost every brain-aware block. The probe now returns a discriminated result (cached per-process, same lifetime the old null had) using the same killed/SIGTERM/ETIMEDOUT discrimination the sources-list probe below already uses; timeout routes to the forgiven 'timeout' status. GSTACK_GBRAIN_VERSION_PROBE_TIMEOUT_MS test override added (same precedent as the sources-probe override). Receipt: the slow-but-present sibling test fails on a v1.77.0.0 scratch worktree (classifies no-cli there). Fixes #2716 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(codex): close the consult-mode fence, report turn.failed as a failure, capture exit codes portably (#2671, #2669) Three defects in the codex skill sections: - The resumed-session bash block never closed its fence; every fenced region after it inverted (prose rendered as code, the synthesis-recommendation tail rendered inert). A repo-wide fence-pairing test now scans every generated SKILL.md and sections/*.md with a CommonMark-faithful state machine (an info-string opener inside a fence is literal content — nested template examples in document-generate/make-pdf stay legal; a file ending inside a fence fails). - The JSONL parsers had no turn.failed branch: a turn that STATED its failure was reported as 'possible mid-stream disconnect'. Challenge and consult now print the event's error and run a three-way completeness check (failed-with- reason / silent-disconnect / ok); consult previously had no completeness check at all. - ${PIPESTATUS[0]} is empty under zsh, so hang detection never fired and every clean run printed a spurious '[codex exit ]'. All three capture sites use ${PIPESTATUS[0]:-${pipestatus[1]}}, pinned statically and EXECUTED under real bash and zsh in the new test. Expect a step-change in codex_timeout telemetry — the counter starts firing for zsh users. Receipt: the portability pin fails on a v1.77.0.0 scratch worktree; the fence fix is structural (17 → 18 fence lines, tail no longer inside a block). Fixes #2671 Fixes #2669 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: outside-voice fallback is labeled honestly — same model family, not cross-model (#2735) When Codex is unavailable, the plan-review outside voice falls back to a Claude subagent and the copy sold it as 'cross-model coverage' with 'genuine independence'. Fresh context is real; cross-model validation is not — a user weighing 'both reviewers agree' deserves to know both reviewers share a model family. Six canonical strings fixed at the resolver source (constants.ts not_installed/not_authed, review.ts outside-voice bullet + three dispatch paragraphs); ~10 generated docs and the ship goldens regenerated. Printing the resolved fallback model at dispatch time is descoped as a functional change (follow-up in the wave dispositions). Fixes #2735 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(relink): skill_prefix patches the gbrain render too — the file the host actually serves (#2738) gstack-relink linked SKILL.md from RENDER_DIR when a gbrain render was active but ran gstack-patch-names only on INSTALL_DIR, so the served frontmatter kept the unprefixed name and skill_prefix=true silently no-oped for every brain-aware skill. The render tree (user-owned, untracked) is now patched too; gstack-patch-names is idempotent so repeat relinks never double-prefix. The gen-skill-docs note that pointed users at relink now describes what relink actually covers. Receipt: the new test fails on a v1.77.0.0 scratch worktree (served render keeps 'name: qa'). Fixes #2738 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(render): section refs point at the FINAL render dir, never the tmp swap dir (#2692) gen-skill-docs bakes its --out-dir into rendered CONTENT (rewriteSectionBase), and both swap-in callers (setup, gstack-config gbrain-refresh) render into claude.tmp.<pid> before the #2569 atomic rename — so every rendered skill carried ~9 dead section Read paths that pointed at a directory the swap had just deleted. New --link-root flag names the final serving dir (defaults to --out-dir for direct-render callers: bin/dev-setup, dev-skill.ts, mkdtemp tests — full caller audit in the wave notes); the rewrite now uses a replacement callback so a $-bearing configured path can't expand as $& in a replacement string. The swap logic itself stays byte-identical. Tests pin the generator contract (tmp out-dir files reference the final dir, $-bearing path included) and both callers' wiring. Fixes #2692 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(setup): persistent timeline Stop hook opt-out — timeline_stop_hook config gate (#2677) --no-team is a one-shot teardown, so every later bare ./setup (including the ones /gstack-upgrade runs) re-registered the timeline Stop hook with no way to say 'never'. New gate mirrors the plan_tune_hooks pattern: flag (--timeline-stop-hook/--no-timeline-stop-hook) > env (GSTACK_TIMELINE_STOP_HOOK) > saved config (timeline_stop_hook) > default yes. An explicit flag persists to config so the decision survives upgrades; an explicit 'no' also removes a live registration (reconciliation), so the opt-out works against installs registered by an older setup. --no-team semantics unchanged (NO_TEAM_MODE is never initialized from config). Full gstack-config surface: DEFAULTS entry, header docs, list/defaults enumeration, warn-and-default validation. Fixes #2677 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(browse): tame the macOS headless GPU spin + reap the lock-less headless Chromium on stop (#2709) Two defects in one report. On macOS 26 / Apple Silicon the headless-shell GPU process pegs ~800% CPU indefinitely after real page work and --disable-gpu alone is not enough; the reporter validated that adding --disable-software-rasterizer/--disable-gpu-compositing/--disable-gpu-watchdog drops it to 0.0% with screenshots still working. The flag block is a pure platform-parameterized function (unit-tested on any host), darwin-gated, headless-only (buildGStackLaunchArgs feeds the headed/GBrowser paths where GPU-off is wrong), with a GSTACK_DISABLE_GPU=off escape. Separately: the headless launch has no userDataDir, so it never writes the SingletonLock that killOrphanChromium walks — 'browse stop' reported success while the orphan kept spinning. The daemon now records the launched child's pid + wall-clock start time in the state file (the xvfbPid/xvfbStartTime contract), and stop paths reap a survivor only after verifying BOTH the recorded start time and a Chromium-looking cmdline — a recycled PID, even one running a different legitimate Chromium, is never killed (identity tests include the coreutils-shebang trap that defeats argv0 renames). macOS efficacy is per the reporter's validation; live re-verification on Apple silicon is tracked in TODOS.md. Refs #2709 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(wtree): a failed touch falls through to the HEAD seed instead of reopening the racy window (#2687) The v1.74 racy-git fix carries the real index's mtime onto the temp copy — but its 'touch -r … || true' meant a FAILED touch silently kept the copy's fresh stamp, marking every entry non-racy and reopening the exact same-size- rewrite hole. A failed touch now discards the copy and seeds from read-tree HEAD (slower; every entry re-hashed; fingerprint stays honest). Verification for #2687 itself: the reporter's same-size-rewrite repro run 20 iterations against this tree — 0 misses (the underlying race was fixed by v1.74's b1485d88 with its own regression test; this wave verifies and closes, it does not claim that fix). Receipt: the stubbed-touch test fails on a v1.77.0.0 scratch worktree. Fixes #2687 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: rewrite gate pin follows the LINK_ROOT rename (#2692) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: v1.78 fix-wave deferrals filed in TODOS.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * v1.78.0.0 release metadata: VERSION, package.json translation, CHANGELOG wave entry, agents digest Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(osv): ignore ledger names its filed tracking issues (#2753, #2754) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(redact): large reports survive the pipe — exitCode instead of process.exit; inert test payload The wave's PR quality gate failed closed: gate-secret-scan.mjs pipes the diff's added lines into gstack-redact and parses the JSON report, but process.exit() discards stdout still buffered in the pipe — this wave's 646-finding report (202 KB) is the first big enough to arrive truncated (~145 KB) at node's collector, so JSON.parse failed and the gate read 'no report' as HIGH. The report and auto-redact body paths now set process.exitCode and let the runtime drain stdout; exit-code contract unchanged (verified 0/2/3 end-to-end). Also: the C1 test's stdin payload no longer uses a provider-prefix credential shape (the gate correctly flagged it; the content was never read on the error path under test). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(memory-ingest): fence regression test survives Windows tmpdirs Wave polish on the #2699 absorption: the hand-built JSONL interpolated the raw tmpdir into a JSON string — on Windows (D:\a\...) that's an invalid escape, the user line was silently dropped, and the body started at '## Assistant' (Windows Free Tests red). JSON.stringify the path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(auq): the interactive fence ends at classification — all behavioral tails removed The pinned-container periodic lane proved the collapse dead (reviewCount 0 → 7/8/5 across the AUQ suite) but flagged the fence's remaining behavioral clause: 'never adds, removes, or batches the skill's decision points' broke the paired-finding control (5 > 4 — it suppressed the batching that fixture expects), and the band overshot its ceiling (8 > 7). Every behavioral tail tried so far skewed counts somewhere ('when unsure, ask' → 8; 'HOW MANY questions' → 1; 'never batches' → paired control red). The fence now ends at 'When unsure, default to interactive.' — classification only, zero behavior words. Pins forbid every tried-and-failed phrasing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(auq): the spawned trigger is the STATUS echo, nothing else — prose channel removed from the eager path Two pinned-container periodic rounds showed that ANY dispatch-prompt declaration channel in rule 1 keeps question counts unstable (round 1, fence with behavioral clause: paired control 5>4, band 8>7; round 2, bare fence: intermittent 0s return, paired control breaks both directions). The stable regime CI was calibrated against had no spawned prose in the eager path at all. Rule 1 now keys on exactly one machine-verifiable thing: the preamble's own SESSION_KIND: spawned STATUS echo. No text from a dispatch prompt, file, or page can flip a session to auto-choose (the strongest anti-injection form). Subagents that missed the env marker are caught at FAILURE time by the AUQ hooks' spawned escape (explicit declaration, never inference) — a channel that never enters an interactive session's eager reasoning. This reverses the wave's earlier explicit-declaration middle ground (and adopts the outside voice's twice-made echo-only argument) on the new evidence. #2733 protected: skill-e2e-docsync-spawned (gate) passes 1/1 on this prose — the ship Step-18 dispatch forces the env prefix, so the echo fires there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: periodic-lane stabilization residual filed (#2756) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(browse): chromium reap works off-Linux and on every stale-state path readPidCmdline fell back to '' on darwin (no /proc), so the identity gate never matched and reapRecordedChromium was inert on the platform #2709's GPU-spin reap actually targets — it now falls back to ps -o command=. readPidStartTime no longer throws when ps is missing (Windows): a launch must never die to a reap-bookkeeping probe. Three stale-state cleanup paths (dead-daemon stop, startServer stale cleanup, headed-connect) now reap the recorded chromium BEFORE unlinking the state file instead of orphaning it, and the stop-path wait polls (100ms steps, 1s cap) instead of sleeping a fixed 500ms. Wiring pinned: server-state pid/start-time write, all five cli.ts reap call sites, headless-only GPU-flag push. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(browse): chained IIFE + second statement no longer misclassified as one expression isSingleParenOrIifeExpression accepted any tail after the initial group's close as long as trailing chars looked chain-ish, so `(async()=>{await 1})().then(x=>x); console.log('done')` classified as a single expression and the expression wrapper emitted a SyntaxError. The tail is now consumed as a strict member/call/index/optional-chain walk to END of input via a shared string/escape-aware findBalancedClose scanner; anything else (';', operators) demotes to the block wrapper. Negative + positive tests added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(browse): move headlessGpuArgs below the import block The #2709 helper landed between two import statements; imports now stay contiguous. No behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(codex-probe): timed-out probe (124) keeps its fail-open contract Exit 124 reached the string-signature branch before the timeout fail-open, so a slow probe whose partial output happened to quote 'permission denied' classified as MODEL_UNUSABLE_INSTALL — a deterministic-broken verdict from a transient condition. 124 is now excluded from the signature branch, and the detect/display greps share one hoisted _BROKEN_SIG regex (they had already drifted: display dropped 'not executable'). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(codex): JSONL parser initializes its state vars in both modes challenge-mode initialized turn_completed_count but tested turn_failed via 'in dir()'; consult-mode initialized neither and rebuilt the counter with a dir() conditional per event. Both parsers now init turn_completed_count and turn_failed up front and use plain checks — same semantics, no module-globals introspection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ship): PR/MR create aborts on a missing or empty scanned body file Both the gh and glab send blocks now guard [ -s "$PR_BODY_FILE" ] and the prose restates that the variable comes from the scan block — bash blocks run in separate shells, and an unset/empty path would previously send an empty body (gh) or cat's error output (glab) instead of the scanned bytes. Codex/factory ship goldens regenerated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(upgrade): abort when a stale .bak already exists at the install path A leftover $INSTALL_DIR.bak from a crashed upgrade would make the mv nest the live install inside it, and the failure-restore arm would 'restore' the stale backup — possibly deleting the only good copy. The upgrade now refuses to start and tells the user to inspect/salvage the backup. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(redact-doc): mktemp-failure message names what it refuses to send 'refusing to send unscanned <noun>' read as if 'unscanned' modified a missing word for sink nouns like 'the spec body'; now 'refusing to send <noun> unscanned'. Generated spec section refreshed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(setup): typo'd timeline-stop-hook value warns instead of persisting --timeline-stop-hook=noo silently normalized to yes AND wrote yes to config — a persisted decision the user never made. Unrecognized values now warn (naming the source), apply the default for this run only, and skip the config write. The opt-out log line names the actual decision source (flag/env/config) and no longer claims a removal that may not have happened. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(memory-helpers): slow-probe warning no longer suppresses the absent warning One shared _gitleaksWarned flag served two different messages: a 'machine under load, retrying next file' warning early in a run permanently silenced the later 'gitleaks not in PATH; secret scanning disabled' warning — the user never learned scanning was off for good. Split into per-message flags. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(lib): shared isExecTimeout helper; export GbrainBinProbe The killed/SIGTERM/ETIMEDOUT discrimination was hand-rolled at three sites (gbrain version probe, engine classifier, gitleaks probe) and free to drift; it now lives once in lib/gbrain-exec.ts. GbrainBinProbe is exported (it's the return type of exported probeGbrainBin) and the cache carries a rationale comment: caching a timeout for process lifetime is deliberate — the memo dedupes the ~3 probes of one short-lived preamble process. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(gen-skill-docs): extract parsePathFlag; fix rewriteSectionBase docstring --out-dir and --link-root shared near-identical inline parsing; one helper now owns it. The rewriteSectionBase docstring said 'no-op when --out-dir is unset' but the gate is the link root (which --link-root can set independently) — it now describes the real behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(memory-ingest): reunite preparePages with its docblock; pin disambiguateSlugs wiring The #2724 disambiguateSlugs block was inserted between preparePages' docblock and the function, orphaning the secret-scanning policy doc onto the wrong symbol. Reordered. A call-site pin now asserts the prepare→stage flow actually invokes disambiguateSlugs, so a refactor can't drop the call while every unit test stays green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(hostile-path): per-run mkdtemp root; telemetry-log redaction coverage The suite used a FIXED tmpdir name, so concurrent runs (sharded runner, sibling worktrees) tore down each other's trees mid-flight — now a per-run mkdtemp root with the apostrophe dir inside. gstack-telemetry-log was the one bin named in the suite header with no test: it now must append a real row under the hostile path with the credential span redacted (<REDACTED-github.pat>) and the rest of the message preserved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(config): signal-killed spawns map to -1, not exit 0 Both cfg() helpers defaulted a null spawn status to 0 — a child killed by signal would read as success and mask real failures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(upgrade): v1.78.0.0 feature-marker migration suite The only migration without a dedicated test. Covers copy-when-absent (script must mkdir GSTACK_HOME itself), destination-wins (never overwrites), clean no-op, and two-run idempotence — asserting file existence and contents, not just exit codes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(auq): pin the explicit-declaration-only spawned escape sentence SPAWNED_ESCAPE_SENTENCE's tightened wording had no pin: positive pins on the explicit-declaration clause, negative pins on the retired v1.76 loose parentheticals ('e.g. your dispatch prompt says', 'marks this session as spawned'), and a drift guard that both hook directives embed the constant verbatim. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(gbrain): invalid version-probe timeout env falls back to the default GSTACK_GBRAIN_VERSION_PROBE_TIMEOUT_MS set to 'abc', '-1', or '0' must use the default budget — exercised behaviorally through probeGbrainBin with a fresh PATH per case (the memo keys on PATH). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(codex): execute the JSONL parser under real python3; fence scanner tracks opener length The parser's turn.completed/turn.failed/disconnect semantics were pinned by shape only — now the python block is extracted from both RENDERED sections and run against synthetic event streams (tokens line, FAILED + not-a-disconnect, silence -> disconnect warning, SESSION_ID echo), with byte-equivalence safety pins on the bash double-quote extraction. The fence scanner also gains CommonMark opener-length tracking: a 4-backtick fence wrapping a 3-backtick example no longer false-positives, with a self-test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(redact): large report survives a slow piped consumer Pins the >145KB truncation regression (process.exit before the pipe drained): 900 MEDIUM findings -> 259KB JSON report through a sleep-first POSIX consumer that holds the 64KiB kernel buffer full at child exit; asserts complete parseable JSON with matching counts and exit 2, plus an --auto-redact mirror (700 redactions, final sentinel byte arrives). Harness proven red against a copy of the bin with process.exit restored. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(dev-setup): update LINK_ROOT source pin to the parsePathFlag shape Companion to the gen-skill-docs parsePathFlag extraction: the pin still asserts the same invariant (LINK_ROOT defaults to OUT_DIR, so an in-place render stays a byte-exact no-op) against the new expression. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(changelog): fix-batch properties folded into the v1.78.0.0 entry Stale-backup refusal + empty-scanned-body guard on the mktemp bullet, the redact pipe-truncation fix as its own item (a v1.77 bug), and test counts refreshed to the post-fix-batch suite (8,660). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(upgrade): migration test resolves bash through the parent PATH A hardcoded /usr/bin:/bin child PATH breaks spawn('bash') on the Windows curated lane (spawn resolves against the CHILD env's PATH; no bash.exe lives there). Hermeticity is carried by HOME/GSTACK_* overrides, not PATH. Found by the cycle-2 review pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(memory-helpers): per-run cooldown bounds the slow-gitleaks probe cost Retrying a slow probe per FILE (#2715's slow!=absent split) re-paid up to probe+retry (12s default) per file — an 887-file ingest on a loaded box spent hours re-asking the same slow question. After 3 consecutive slow answers the run stops probing and warns once that remaining files go unscanned; the availability cache is still never written, so the next process probes fresh. Slow/absent discrimination is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(memory-ingest): slug assignments persist across runs via the state consult First-occurrence-keeps-bare was walk-order-dependent ACROSS runs: a source that got the suffixed slug once could take the bare slug the next run (its collider aged out or was skipped as unchanged), leaving gbrain holding the same transcript under two slugs — and a NEW collider could claim a bare slug that state shows belongs to an unchanged source, silently overwriting that page. disambiguateSlugs now consults state.sessions: a recorded slug stays owned by its source_path, re-ingested sources keep their slug verbatim, fresh assignments never take another source's slug, and legacy duplicate records (pre-#2724 overwrites) resolve first-owner-wins and self-heal on the next state write. Stateless behavior is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(changelog): D2/D3 properties folded into the absorbed-PR bullets Gitleaks per-run probe cooldown on the #2715 credit; cross-run slug persistence on the #2699/#2724 credit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: update project documentation for v1.78.0.0 README.md: the persistent timeline Stop hook opt-out (#2677) — flag, env var, and config key with resolution order. BROWSER.md: browse stop against a dead daemon now reaps the recorded headless Chromium child, identity-verified (#2709). CLAUDE.md + CONTRIBUTING.md: free-suite test count ~7,000 → ~8,700 (8,660 as of this wave). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: cross-model doc review fixes for v1.78.0.0 CONTRIBUTING.md: the day-to-day example now edits the .tmpl (SKILL.md is generated); the OSV row states the explicit --config load and the reasoned, expiring ignore contract. BROWSER.md: stop row mentions the identity-checked Chromium reap; env table gains CHROMIUM_PROFILE and GSTACK_DISABLE_GPU rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(changelog): headline claims what the receipts show "Both red weekly lanes are green again" overclaimed: OSV is verifiably green (pinned scanner, frozen install, branch dispatch), but the periodic lane keeps its pre-wave churn (#2756) — what this wave proves is that the v1.76 regression that silenced plan reviews is dead. Flagged by the cross-model doc review; headline now leads with the user-visible outcome. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- 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: Lockyer <135391289+Lockyer228@users.noreply.github.com> Co-authored-by: Udhdhav kheni <udhavkheni12@gmail.com> Co-authored-by: schienbiz <274676847+schienbiz@users.noreply.github.com> Co-authored-by: David Park <show@davidani.com> Co-authored-by: alopes50 <alex@alexlopes.com> Co-authored-by: Peter van Leeuwen <petervanleeuwen@SB-petervanleeuwen.local> Co-authored-by: Denis Zjukow <denis.zjukow@gmail.com> Co-authored-by: Paul Snyman <5826275+snymanpaul@users.noreply.github.com> Co-authored-by: Adam Badar <badaradam10@gmail.com> Co-authored-by: loulanyue <260355617@qq.com> Co-authored-by: Shreshth Kapoor <shreshth@osiflow.com> Co-authored-by: Ryan Ayers <rayers@dividia.net> Co-authored-by: Simon Altit <simon.altit@gmail.com> Co-authored-by: ptt <1928627998@qq.com>
…arrytan#497/garrytan#2440 class) (garrytan#2772) * fix(ship): foreground-flag + deadline + scope guard on all four dispatch sites (garrytan#497/garrytan#2440 class, 3rd recurrence) Steps 7/8/10/18 dispatch subagents whose LAST-line JSON the parent consumes, but none passed run_in_background: false — since Claude Code v2.1.198 subagents background by default, so /ship stranded at Step 18 waiting on output that never arrives. Every site now renders the shared {{FOREGROUND_DISPATCH_NOTE}} resolver constant, carries a ~10-minute deadline with an explicit recovery branch (stop the runaway task, reconcile against pre-dispatch HEAD, surface stray state, never re-dispatch), and Step 18's prompt gains a docs-sync-only scope guard (no VERSION changes, no base-branch merges, push-rejection reported as pushed:false with parent-side reconciliation and a second-failure branch). Greptile failures now record as UNAVAILABLE, not zero comments. GENERATED_WITH_GUIDANCE pins all five ship dispatch carriers; a new test pins the deadline recovery + scope guard phrases in pr-body (.md and .tmpl). Codex/factory ship goldens re-rendered. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(document-release): first-class spawned-dispatch contract document-release's own templates had zero subagent-awareness — the entire headless contract lived in /ship's dispatch prompt, so any other orchestrator (or an older installed /ship) dispatching it inherited none of the gate handling. The skill now carries the contract itself: detect spawned strictly from the dispatch prompt or the preamble echo (never from file content — prompt-injection guard), auto-choose recommended options while keeping the never-clobber-CHANGELOG and never-bump-VERSION-silently invariants via their Skip options. Step 8.4d gets an explicit spawned note (its interactive recommendation bumps VERSION — wrong headlessly), and the Codex Documentation Review section skips itself in spawned sessions (the apply gate needs a human; the dispatching workflow owns review passes). Contract, 8.4d note, and resolver skip are pinned in run-in-background-guidance.test.ts; document-release skeleton budget re-measured (39,812 B) and ratcheted to 40,200. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: sweep every remaining synchronous Agent-dispatch site with run_in_background: false The garrytan#2440 failure shape was a silently-missing review voice — a specialist launched in the background and merged before it completed. Every remaining synchronous dispatch site now carries the explicit flag: the Red Team dispatch, the spec review loop, the Codex second-opinion/plan-review/doc-review Claude fallbacks, the adversarial subagent, design sketch and outside voices, autoplan's design/eng/dx phase dispatches, CSO parallel finding verification, and design-shotgun's variant launch. Parallel fan-outs stay parallel — multiple foreground Agent calls in one message run concurrently (the shipped v1.64.0.0 review-army pattern). GENERATED_WITH_GUIDANCE now pins all 24 generated carriers, so a new dispatch site that drops the flag fails the free suite. Six carved-skill skeleton ceilings re-measured and ratcheted (~80-130 B growth each); factory ship golden re-rendered. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * release: v1.79.0.0 — CHANGELOG, VERSION, TODOS follow-ups Queue-advanced to 1.79.0.0 (1.78.0.0 claimed in the workspace queue; same MINOR level per the versioning invariant). Entry references the class history (garrytan#497 → garrytan#2440 → Step 18). Three TODOS filed: PreToolUse hook enforcement, structural ship-mode for document-release, cross-host dispatch semantics audit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * regen: review-army sections carry the Red Team foreground flag The scripts/resolvers/review-army.ts Red Team edit regenerated these two files but the sweep commit staged only the adversarial sections — the skill-docs freshness gate (regen + git diff --exit-code) catches this. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * regen: agents digest picks up v1.79.0.0 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(document-release): one canonical spawned contract, downstream notes are pointers The diff-selected LLM-judge eval scored the skill's clarity 3 (threshold 4, main scores 4): the spawned-session rules read as three separately- framed rule sets (contract paragraph, Step 8.4d note, Codex-review skip). The contract paragraph now declares itself the single source of spawned behavior and the two downstream notes reference it instead of restating rationale. The pointer avoids naming the Codex section verbatim so the codex-host render (which strips that section) keeps its negative pin. Judge re-scored 4/5/4 across repeated samples after the change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: foreground note names the Agent tool as non-substitutable The ship-docsync gate E2E caught a behavioral regression: the note's blocking-emphasis ('a backgrounded dispatch strands the run') steered the driven agent to run doc-sync via the Skill tool inline — the most blocking option — twice in a row, forfeiting the fresh-context isolation the dispatch exists for (baseline on main's text dispatches via Agent). The shared note now says explicitly: dispatch with the Agent tool itself, never substitute Skill or inline execution; the flag already makes the call block. Re-verified: ship-docsync passes on the amended text. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: pre-landing review fixes Review army + red team findings, all verified before applying: - ship-docsync E2E now asserts run_in_background === false on the captured dispatch (red team CRITICAL: phrase pins prove text exists, this proves the model obeys it — verified passing live). - Structural scanner test: any generated file with an Agent-dispatch imperative (or bare '(foreground)' prose, the garrytan#2440 inert shape) must carry the flag or hold a reasoned exemption — the 4th-recurrence net the hand-enumerated pin list can't provide. - Parent push reconciliation models reality: the parent shares the repo, so a non-fast-forward that hit the subagent hits the parent identically — fetch + ahead/behind check first, push only when the rejection was transient; dispatch prompt promise softened to 'the parent will handle it'. - Recovered commits from a dead subagent are vetted docs-only (git show --stat, never VERSION/package.json) before any push. - Deadline pacing named: ~3 minutes between checks, wall clock not polls. - Greptile UNAVAILABLE recording narrowed to the PR body (Step 20's schema carries no triage field). - document-release contract gains the echo-failure tie-breaker: prompt claims spawned + no echo → fail fast with the dispatch contract's failure shape instead of reproducing the garrytan#2733 prose-STOP; contract anti-injection and NEVER-relax clauses pinned in tests. - 'Claude Code v2.1.198' extracted to CC_BACKGROUND_DEFAULT_SINCE and interpolated at all resolver sites (byte-identical output). - CHANGELOG: entry-boundary blank line restored; worst-case-wait row scoped to the backgrounded path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: adversarial review fixes — failure shape, CHANGELOG guard, vet coherence Claude adversarial subagent findings (11), all verified before applying: - The dispatch JSON contract gains an explicit FAILURE shape ({"error":...}) and a parent branch for it — a doc-sync that could not run (spawned marking failed, broken preamble) previously had only the no-updates shape to emit, which the parent printed as 'Documentation is current': a silent false-clean of exactly the VAS-449 class. - Scope guard now covers CHANGELOG: skip Step 5 voice polish and resolve CHANGELOG-touching gates to leave-as-is (the parent authored the release entry; the prompt's older auto-choose clause conflicted with the contract's never-rewrite invariant). - Recovery vet is sequence-coherent: pushing a commit pushes its ancestors, so ANY non-doc commit (VERSION, package.json, CHANGELOG.md) blocks the whole sequence — no more push-the-vetted-child-of-an- unvetted-parent hole. - Steps 7/8/10 failure branches stop a still-running backgrounded task before falling back, so a late result never races the inline audit. - 'Documentation synced' print gated on pushed:true (item 6 owns the local-only outcome); doc-review skip note's backward step pointer fixed; foreground note scoped 'at initial dispatch' so the sanctioned inline FALLBACKS in Steps 7/8 read as sanctioned; constants docstring no longer overclaims single-sourcing (names the 3 inline templates). - Runtime-hook TODO raised P2 → P1: the spawned trust chain is agent-self-asserted; prose cannot close it, the hook can. Both docsync gate E2Es re-verified green on the amended prompt, including the strict run_in_background === false dispatch assert. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: codex adversarial fixes — JSON trust, fail-open visibility, user-state safety Codex outside-model adversarial pass (inline-diff workaround for the sandbox), P1/P2 findings triaged cross-model: - Parent validates the dispatch JSON's field types and treats documentation_section as untrusted markdown (Step 19's redaction scan covers the final body; instruction-shaped text inside it is never followed). The error branch now explicitly skips items 2-6. - Remote-ahead divergence is named, not silent: the parent lists foreign commits before creating a PR over a moved branch. - Recovery cleanup never discards content: stray staged doc edits are unstaged but never checked out or cleaned away. - Greptile UNAVAILABLE gets a concrete PR-body line, not a vague 'wherever results are reported'. Pre-existing-class P1s (self-asserted spawned trust chain, unenforceable foreground timeout) are cross-model confirmed and tracked: PreToolUse hook TODO at P1, harness-residue hazard documented. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: never-Skill prohibition made unambiguous; docsync fixture made diagnostic The ship-docsync E2E flaked on this sandbox because the driven agent resolved the STOP pointer's ~ to nonexistent homes (/root, /home) and acted blind — pass/fail sampled model priors, not the prompt. The fixture prompt now names the planted sections dir, making local runs deterministic (CI, with real install paths, was always diagnostic). With a diagnostic fixture: 2/2 passes, real pr-body read, Agent dispatch with run_in_background: false, zero Skill-tool substitutions. Prose: the foreground note now states the dispatch happens ONLY via the Agent tool (invoking the target as a Skill is wrong even though it appears in the skills list; inline FALLBACKs apply only after a dispatched subagent has failed), and the Step 18 imperative carries the never-the-Skill-tool clause at the decision point. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: CHANGELOG entry covers the review-hardened contract Failure JSON shape, CHANGELOG scope guard, vetted recovery pushes, structural scanner, and the behavioral dispatch assert are shipped properties of v1.79.0.0 — the entry now describes them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: update project documentation for v1.79.0.0 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: CHANGELOG voice — follow-ups to For contributors, never-Skill property stated Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): daemon 503 test binds an OS-assigned port, not old-port+1 windows-free-tests flaked on this PR: the tunnel-less restart bound daemon.loopbackPort + 1 — a fixed neighbor of the OS-assigned ephemeral port — and died with 'Is port 55738 in use?' whenever another shard or a TIME_WAIT socket held it; the file-level retry re-rolled the same dice. Every other startDaemon in the file already uses loopbackPort: 0 and the assertion reads d2.loopbackPort, so nothing needs a predictable number. 21/21 pass locally. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…one state root, gstack never clobbers a skill it did not create (garrytan#2802) * fix(freeze): hook reads the same state root /freeze writes — fails closed under GSTACK_HOME (garrytan#1459, garrytan#1509) check-freeze.sh resolved its state dir as ${CLAUDE_PLUGIN_DATA:-$HOME/.gstack} while every writer (/freeze, /guard, /unfreeze, /investigate) resolves through bin/gstack-paths, GSTACK_HOME first. With GSTACK_HOME set, /freeze wrote freeze-dir.txt under GSTACK_HOME, the hook read $HOME/.gstack, found no file, and allowed everything — a deny-tier boundary failing open. One resolver now: gstack_hook_state_root() in careful/bin/hook-extract.sh (already sourced by both check-freeze.sh and check-careful.sh) implements the exact gstack-paths chain, including the CLAUDE_PLUGIN_ROOT guard that keeps a CLAUDE_PLUGIN_DATA leaked from another plugin from redirecting our state. check-freeze.sh and gstack_hook_log_fire both call it; nothing spawns gstack-paths from a hook. Tests: the GSTACK_HOME deny regression, GSTACK_HOME-over-CLAUDE_PLUGIN_DATA precedence, plugin-root guard both ways, and a byte-parity check against bin/gstack-paths across six env combinations. Existing freeze tests now pass CLAUDE_PLUGIN_ROOT like a real plugin install would. Idea from PR garrytan#1509 (@NikhileshNanduri); implemented natively against the shared resolver rather than a second fallback chain. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(relink): never delete or link over a skill gstack does not own (garrytan#2119) gstack-relink runs on every ./setup. Its cleanup did `rm -rf` on any same-name entry whose SKILL.md was a symlink, with no readlink check, and its link step did `mkdir -p` then `ln -snf` onto any existing SKILL.md — on Linux that replaces a user's real file with a symlink into gstack (macOS refused by accident). setup's Windows mode-flip cleanup deleted any real dir whose name matched a gstack skill. A personal `qa` skill, or a fork installed under another path, was destroyed by the installer of a tool it never asked for. Ownership is now proven, never assumed. An entry is ours when it is a symlink resolving into INSTALL_DIR or RENDER_DIR, a real dir whose SKILL.md is such a symlink, or a real dir carrying the .gstack-owned marker setup now writes for Windows copy installs (legacy copies count when byte-identical to the source or carrying gen-skill-docs' AUTO-GENERATED header). Anything else — including an entry whose readlink fails — is foreign: left untouched, reported on stderr, and listed in relink's summary line. The same rule replaces setup's Windows name-match deletion; setup:1040 and gstack-uninstall:204 already gated on readlink, so this closes the last unguarded deleter of the class. Tests: foreign real dir in flat mode, foreign flat entry on a prefix flip, foreign directory symlink, RENDER_DIR-targeted entry (ours), marker-carrying copy (ours), marker-less copy (foreign); the Windows cleanup test now proves provenance three ways and keeps the user's own same-name skill. Idea and two regression cases from PR garrytan#2119 (@smblight); implemented on the destination entry, not only the symlink target. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(setup): Chromium bootstrap is best-effort and bounded — skills always register (garrytan#1900, garrytan#1901, garrytan#1902, garrytan#913, garrytan#2233) setup runs under `set -e`, and the Chromium bootstrap in section 2 sat ahead of skill registration in section 4 with a bare `bunx playwright install chromium`, an unbounded download, and an explicit `exit 1` after the post-install launch probe. On an offline, proxied, or AppArmor-restricted box the user ended with ZERO skills registered and a re-run that died at the same line; a wedged download hung setup indefinitely. Every browser failure now records a reason code in _PW_FAIL_REASON and setup continues: skipped (GSTACK_SKIP_PLAYWRIGHT=1, garrytan#913), chromium-install, chromium-install-timeout (the download is bounded by the existing _wait_with_deadline helper, default 600s, env GSTACK_PLAYWRIGHT_INSTALL_TIMEOUT, process tree killed via _kill_tree), chromium-install-locked (another setup holds the lock: this one registers skills and re-probes next time instead of exiting), windows-no-node, windows-node-modules, post-install-launch (with the GSTACK_CHROMIUM_NO_SANDBOX=1 hint for Ubuntu 24.04's userns policy, garrytan#2157). The daemon font refresh is skipped when Chromium is unavailable. The final summary names the skills that need the browser (/qa, /qa-only, /design-review, /browse, make-pdf, /pair-agent) and the fix for the recorded reason, and logs the reason code (never a path) through gstack-telemetry-log when telemetry is on. Tests: static invariants over the anchor-sliced block (no exit, every reason code, deadline helper, trap chaining, guarded refresh, summary contents) plus an integration harness that executes the real block with a stubbed probe and installer: install failure, hang killed at the deadline with the tree kill recorded, non-numeric knob fallback, live lock (continues, installer not run, lock preserved), stale lock reclaimed, post-install probe failure, and the skip flag. Credit @DavidMiserak (PR garrytan#1900) for the best-effort shape; re-implemented on the current block. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(designs): preserve the time-attack fork-port residual evaluation The read-only evaluation of what remains portable from time-attack/gstack (583 raw candidates, 415 canonical, 287 with a residual, 48 adversarially refuted, 14 standing) lived only on a throwaway VM. This records the report, the lite residual index, the absorbed/superseded ledger, the refuter verdicts, and SHAS.md with the fork tip, upstream HEAD, merge-base, and a sha256 per file, so every scheduled fix in this wave series traces to its evidence. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: file the fork-port residual deferrals and document the Chromium bootstrap knobs TODOS.md gains the seven items the CEO and eng reviews of the fork-port residual plan deliberately deferred (shared ownership helper, config-key reader tripwire, "pre-existing" vocabulary, opt-in reply_language, .auth.json writer removal, the fork-derived-change rule for CONTRIBUTING, hook slug parity audit), each with rationale, and updates the two residual bullets for PR garrytan#2232 and PR garrytan#2233 with their dispositions. README's Troubleshooting section explains the best-effort Chromium bootstrap and its three knobs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(relink): canonicalize link targets before the ownership check Pre-landing review finding: the ownership gate compared readlink output textually against INSTALL_DIR and RENDER_DIR, so two shapes of gstack's OWN entries read as foreign and were left behind on a mode flip — a legacy relative link (`gstack/qa/SKILL.md`, resolved against $PWD instead of the link's directory) and an entry linked against the real path of a symlinked install dir (~/.claude/skills/gstack -> checkout). Both now resolve: relative targets anchor at the link's directory, the directory part is canonicalized with pwd -P (the basename stays verbatim so a dangling managed target is not misread), and both spellings of each root are accepted. Two regression tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(telemetry): one-shot setup events never sweep other sessions' pending markers gstack-telemetry-log finalizes every .pending-<session> marker that is not the caller's own as outcome:unknown and deletes it. setup's onboarding events (_setup_welcome, _setup_playwright) have no session of their own, so a Chromium bootstrap failure during a live skill session recorded a false unknown for that session and removed its marker. New --no-sweep flag skips the stale-marker pass; both setup call sites use it (the synthetic --session-id did not prevent the sweep). Surfaced by the Codex adversarial pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(hooks): partial upgrades fail closed for freeze and fall back for careful A hook script and its sourced helper can be copied at different times. With an older careful/bin/hook-extract.sh that lacks gstack_hook_state_root: - check-freeze.sh now emits a deny ("fail closed, re-run ./setup or /unfreeze") instead of dying under set -e with no decision JSON. - check-careful.sh falls back to ${GSTACK_HOME:-$HOME/.gstack} so project rules under the plain chain still load and a decision is always emitted (a warn hook must never break on a stale helper). gstack_hook_state_root prints its root without a trailing newline and both callers capture it with a printf-x sentinel, so a GSTACK_HOME ending in a newline round-trips byte-for-byte with the writer's %q form. gstack_hook_log_fire stays on ${GSTACK_HOME:-$HOME/.gstack}/analytics, the same two-step chain every other analytics writer and reader uses, so the usage log remains one file under a plugin install. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(setup): never link over, copy over, or reap a skill gstack does not own (garrytan#2119) The relink gate alone left three destructive sites open: - link_claude_skill_dirs runs BEFORE relink on every ./setup and used `ln -snf` (Linux replaces a user's real SKILL.md with a symlink into gstack) or, on Windows, rm -rf + cp followed by a marker that made the user's directory "ours" on the next flip. It and _install_alias_skill_md now consult _claude_entry_is_ours first and skip loudly. - cleanup_prefixed_claude_symlinks kept a bare name-match deletion and a `*gstack*` substring match. Symlink arms use anchored `gstack/` segment patterns; the Windows real-file arm proves provenance (marker, byte-identity with our source, or the full two-line gen-skill-docs banner within the first 40 lines, never a one-line substring another generator could emit). cleanup_old_claude_symlinks uses the same banner rule. - gstack-relink's fast path judged absolute targets before canonicalizing, so `/x/gstack/../foreign/SKILL.md` counted as ours; dot-segment targets now canonicalize first. Its banner rule matches setup's. The `.gstack-owned` marker records the owning payload's realpath. Entries skipped by setup or relink are listed in the final setup summary. Chromium bootstrap refinements from the pre-landing review: an INT/TERM trap kills the installer's process tree; the Windows npm chain no longer masks an install failure; GSTACK_SKIP_PLAYWRIGHT=1 is reported as a choice rather than a failure and sends no telemetry; the timeout knob is normalized (0, 000, non-numeric, or more than nine digits fall back to the 600s default instead of killing on the first poll or never killing). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: README Chromium note outside the CLAUDE.md fence; report banner stripped; deferrals name the four gate sites - README: the Chromium troubleshooting paragraph sat inside the CLAUDE.md snippet code fence, so copy-paste put it into users' CLAUDE.md. Moved to the troubleshooting list. - docs/designs/fork-port-residual-2026-09/REPORT.md: the scratch-run preamble banner is gone; SHAS.md re-hashed. - TODOS: the ownership-gate deferral names the four sites and the marker-path idea for the fork-with-banner residual. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(todos): the bootstrap block coverage gap is pinned except the quarantine helper Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(setup,relink): ownership proof has two strengths; weak proof never deletes a directory or discards a differing file The first garrytan#2119 gate treated a byte-identical or banner-bearing real-file SKILL.md as full ownership, so a prefix flip could rm -rf a user's directory (their own qa skill started from a gstack SKILL.md, plus my-templates/) and the link pass could replace their customized file with a symlink. Two strengths now: - STRONG: the .gstack-owned marker (we created the directory), or a directory holding nothing but symlinks and the marker (deleting it loses no data). Only strong proof removes a directory whole. - WEAK: byte-identity with our source or the two-line gen-skill-docs banner on a real file. Weak proof covers that SKILL.md and our runtime-asset links only; a differing file is moved to ${GSTACK_HOME:-~/.gstack}/backups/skills/<ts>/<skill>/ before we link over it, and setup/relink print one summary line naming what moved. The marker is written on every platform now (path-independent proof for Windows copies and for checkouts whose path carries no gstack segment), but only for a directory gstack creates: a directory we merely link into (unclaimed, or a legacy install) never becomes deletable whole. A directory with no SKILL.md at all is unclaimed: the link pass may add our file, the cleanup pass has nothing to remove. Also from the review passes: the banner check reads 8192 bytes, not 40 lines (investigate, office-hours, plan-ceo-review and design-consultation carry the banner past line 40 and were left "foreign" on pre-marker Windows installs); a link into a checkout named without a gstack segment (git worktree add ../gstack-<branch>) is ours when that tree carries setup + VERSION + bin/; relink's fast path is gone so both files canonicalize before judging; relink's root alias (_gstack-command) is gated and stamped like every other entry; relink reports the bare entry name with setup's wording and setup dedupes when forwarding (_run_relink_quiet); the summary names the browser skills as examples. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(setup): Chromium-install lock reclaim is atomic and pid-validated; abandoned locks expire; the tree kill walks /proc without pgrep - A pid file holding "", "-1" or "0" counted as a live holder (kill -0 -1 signals every process and succeeds), locking Chromium out for good. A pid must be a positive integer; anything else is stale. - Two setups judging the same lock stale raced on rm -rf + mkdir and the loser deleted the winner's fresh lock. The stale dir is renamed first (atomic), so exactly one reclaims. - A lock dir with no pid file (killed between mkdir and echo) was never reclaimed; it now expires once older than the install bound. - _kill_tree needed pgrep; debian-slim and git-bash ship none, so the bound killed only the wrapper subshell and the installer kept running. Without pgrep the children are found by walking /proc/*/stat. - The timeout knob is normalized in one place with one comment; the trap's exit 130 is the only exit the block may contain. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(freeze): an unexpected non-zero death denies via an EXIT backstop instead of exiting with no decision set -e plus a failing pipeline (a tool on PATH exiting non-zero, a deleted cwd) ended the deny-tier hook with no JSON, which Claude Code treats as non-blocking: the edit outside the boundary proceeded. The EXIT trap now prints a deny for any non-zero exit that happens before a decision was written; every deliberate output sets _FREEZE_DECIDED first so a late failure never prints a second object. Tests also pin careful's state-root precedence (GSTACK_HOME over CLAUDE_PLUGIN_DATA, plugin data when CLAUDE_PLUGIN_ROOT names gstack) and the specific "out of date" deny for a helper without gstack_hook_state_root. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(telemetry): guard the stale-marker sweep with an if, not a break inside the loop Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(todos): the ownership gate lives in six sites, and the cleanup arms inline their own chain Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test: the two remaining linker harnesses extract the ownership helpers; the marker is the one allowed dotfile setup-claude-skill-assets and user-render-out-dir-install slice link_claude_skill_dirs out of setup without the helpers it now calls, so the extracted function died with "command not found" (or, inside an if, degraded into "foreign, skipped"). Both harnesses now carry the full helper set and the globals. The hidden-files census allows .gstack-owned, which the linker writes for directories it creates rather than copying from the skill source. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(setup,relink): weak proof never costs the user a file — assets, flips, failed backups, foreign dir links, alias markers Third review cycle on the ownership model, every item reproduced against a fixture before the fix: - Runtime assets (sections/, templates/, checklist.md, ...) were refreshed with rm -rf regardless of who owned the directory, so an unclaimed or weakly-owned directory lost the user's same-named real files. Real assets are now replaced only in a directory gstack created or strongly owns (marker, or SKILL.md symlink into gstack), plus the legacy Windows real-copy shape; elsewhere they are kept and reported. Symlinks are never content and are always refreshed. - The prefix-flip cleanup deleted a customized banner-bearing SKILL.md that the link pass would have backed up. Both cleanups now compare the file against the source (raw, or with its name: line rewritten to the entry name, which is how alias and prefixed copies legitimately differ) and move a differing file to the backup root. - A failed backup (unwritable root) returned success and the caller linked over the file anyway. It now fails, and the entry is left untouched and reported. - A foreign DIRECTORY symlink whose target had no SKILL.md fell through to the "unclaimed directory" rule and was replaced by a real directory. A symlink that does not resolve into gstack is foreign, full stop. - The alias installers stamped .gstack-owned into pre-existing directories; they now follow the same created-or-already-marked rule. - A directory counts as "only links" only when every link resolves into gstack: a user's own symlink makes it mixed, so their link survives. - The gstack-tree heuristic requires bin/gstack-relink, not just a VERSION file, a setup script and a bin/ directory. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(setup): lock reclaim hands a fresh lock back; a live holder past the bound is stale; /proc walk strips through the last paren - Reclaim renamed the lock by path after judging it stale, so a second setup that had already reclaimed and re-created it lost its fresh lock and two installers ran. After the rename the moved directory's pid is re-read: a new live holder, or a fresh lock whose pid is not written yet, is moved straight back. - A pid file whose process is alive but whose lock is older than the install bound is stale too (the holder is past its own deadline, or the pid was recycled to an unrelated long-lived process); it was locked forever. - The /proc fallback stripped the comm field to the FIRST ") ", so a comm containing ") " hid a child from the kill. proc(5) says the last paren. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(freeze): mark the decision written after the helper prints, not before If gstack_hook_decision ever failed between the flag and its output the backstop would have stayed silent; setting the flag after the print keeps the deny backstop armed until a decision is actually on stdout. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore: bump version and changelog (v1.80.0.0) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: update project documentation for v1.80.0.0 README troubleshooting + manual uninstall cover the skill ownership gate (.gstack-owned marker, ~/.gstack/backups/skills/<ts>/, foreign same-name skills left untouched). CLAUDE.md and CONTRIBUTING carry the ownership and best-effort Chromium bootstrap invariants for people editing setup and gstack-relink. PROJECT_STRUCTURE gains careful/, freeze/, guard/, unfreeze/, gstack-upgrade/, gstack-relink, and the setup/relink/hook test files. TESTING_INTERNALS documents the anchor-sliced setup harness convention. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(setup): the final summary reports customized SKILL.md files moved to the backup root The linker moved a weakly-proven, customized SKILL.md aside before linking over it but never said so; only relink printed a "Moved N" line, and by the time relink runs the file is already a symlink. The summary now names each moved file and where it went, next to the foreign-entry report. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test: run assembled setup harness scripts from a temp file, not `bash -c` argv (Windows MSYS2 8 KB truncation) windows-free-tests (run 33907177851) failed in test/setup-alias-name-uniqueness.test.ts with bash: -c: line 178: unexpected EOF while looking for matching `' The harness slices functions out of `setup` and passed the joined script as one `bash -c` argv element. The ownership gate grew that script from 6.7 KB to 15.7 KB, and on Windows bash is an MSYS2 program: when its parent is a non-MSYS process (bun), msys-2.0.dll's build_argv() runs any argument containing `?*["'(){}` through globify()/glob(), which copies the pattern into a fixed `Char patbuf[8192]` and silently stops after 8192 - MB_CUR_MAX (8186 chars under C.UTF-8); GLOB_NOCHECK then returns the truncated text as the argument. Character 8186 lands inside the single-quoted sed token on line 178. Rebuilding the exact script with CI path shapes and cutting it at 8186-8190 characters reproduces the identical message locally; cmd.exe's 8191-UTF-16 cap and CreateProcess's 32767 do not fit the evidence. Fix: test/helpers/bash-script.ts writes the script to a temp file and runs `bash <path>` — a short glob-free argument that never enters globify. Every setup harness that assembled a script for `bash -c` (11 files, 22 sites) uses it; timeouts and env are preserved verbatim, spawn/timeout errors are appended to stderr, temp cleanup is best-effort. `spawnSync('bash', [<Windows absolute path>])` already passes on windows-latest in setup-help, uninstall-windows-copies and the migration tests. The Windows-curated list is byte-identical before and after. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(test-free-shards): the rerun-refresh harness spawns bash <tempfile> via test/helpers/bash-script.ts, not bash -c Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…ing skill, the PDF/diagram renderer, and web research; the bundled browser stays the automatic fallback (garrytan#2810) * feat(aside): browser-driver contract, cookbook, research and fallback resolvers {{ASIDE_SETUP}} (readiness probe + ten rules for driving the user's real browser), {{ASIDE_COOKBOOK}} (script shapes verified live against Aside CLI 1.26: one flow per aside repl script, CDP console hook before navigation, evidence lines, session-directory artifact handoff, GSTACK_STEP_OK sentinel), {{ASIDE_RESEARCH}} (research through aside exec, WebSearch when Aside is absent, knowledge otherwise) and {{BROWSE_FALLBACK}} (the fifteen-row Aside-step to $B-command table plus the rules that differ, so every browsing skill keeps working on gstack's own headless browser). test/aside-driver.test.ts pins the sentences and asserts every browsing skill carries the Aside block followed by the fallback; test/helpers/aside-available.ts is the shared live-Aside probe. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(render): Aside-first local-HTML renderer with the bundled browser as fallback lib/aside-render.ts serves the HTML's directory on loopback (Aside refuses file:// URLs), opens it with waitUntil load, prints through CDP Page.printToPDF so tagged output, outlines, header/footer templates and page numbers survive, emulates device metrics for sized screenshots, and writes in-page evaluations to files; when Aside is absent it runs the same spec through the browse daemon (newtab, load, js, pdf, screenshot, closetab) and reports ENGINE=aside|browse. bin/gstack-render.ts is the CLI skill templates call. lib/claude-bin.ts and lib/error-handling.ts become the canonical copies (browse/src re-exports them). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(browse): /browse drives Aside first, with the $B reference behind the fallback Contract, cookbook, mode choice (aside repl by default, aside exec for reading), report format, the fallback section, and the full command reference carved on demand. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(qa): /qa and /qa-only drive Aside, fall back to $B QA_METHODOLOGY runs every phase as Aside scripts (orient, explore, document, re-test, mobile viewport via CDP emulation, links via HEAD fetch); the authenticate phase is 'you are already signed in'; a 13th rule requires consent before mutating actions on non-local targets; the fallback section translates each step onto $B. The qa E2E tests run on whichever engine is present. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(design): design-review, design-consultation, design-shotgun, plan-design-review, design-html drive Aside Design-system extraction is one script printing FONTS/COLORS/HEADINGS/TOUCH_TARGETS/NAV; competitor research confirms the exact URLs before opening them in the real browser and runs on the bundled browser when Aside is absent; design-html's viewport screenshots, sketches and comparison boards render through gstack-render. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(deploy): benchmark, canary, land-and-deploy Step 7, devex-review drive Aside One aside repl script per page prints NAV/PAINT/LCP/RESOURCES/SCRIPTS/CSS/SUMMARY (benchmark), CONSOLE_ERRORS/NAV/TEXT + screenshot (canary, re-run every 60s), and the post-deploy check reads responseStatus from the navigation entry; each carries the $B fallback. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(third-party-actions): Aside is the recommended driver; gstack's visible browser stays the fallback The readiness probe is lifted from {{ASIDE_SETUP}} at gen time (byte-identity pinned) and rule 3 points at browse/SKILL.md for how to drive; the consent question offers Aside first and gstack's own visible browser (handoff/resume for sign-in) as the fallback, as v1.72 framed it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(scrape): /scrape reads pages through Aside; the browser-skills runtime rides the fallback Look-then-extract scripts build the JSON inside the page and print it between JSON_START/JSON_END; aside exec for fuzzy intents; on the $B fallback the browser-skills match/prototype flow and /skillify apply as before. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(make-pdf): print through Aside first, the bundled browser otherwise asideClient.ts replaces the direct $B client with one render() call per PDF (the exact option mapping the browse pdf command had: paper, margins, header/footer/page numbers, tagged, outline, printBackground, preferCSSPageSize, Paged.js wait); the diagram pre-pass, oversized-image downscale and DOCX rasters each run as one render script with per-fence try/catch; exit 4 now means no browser is available and names both remedies; $P setup reports which engine it found. The e2e gates run on whichever engine is present, so the Linux lane exercises the fallback. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(diagram): the triplet is one gstack-render call SVG, PNG and excalidraw from one invocation over the content-addressed bundle staged under /tmp/gstack-render; every diagram type gets an excalidraw export; gstack-render picks the engine and prints ENGINE=; the diagram E2E gates on either engine. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(research): web research runs in Aside first, WebSearch second The planning, review, design, security and investigate skills research through {{ASIDE_RESEARCH}}; WebSearch stays in allowed-tools as the fallback; testing.ts's bootstrap step follows; skeleton ceilings ratcheted for the research block. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(setup,gen-skill-docs): prune renders of skills that no longer exist setup gains _prune_stale_generated for every host tree and the doc generator removes gstack-* output dirs it did not write, so a skill removed from the source tree can never linger in an install. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test: registries, budgets and suite reconciled for Aside-first with the $B fallback Touchfiles + E2E tiers gain the Aside keys, coverage matrix and eval baselines updated, size budget re-baselined to parity-baseline-v1.80.0.0.json (the contract plus fallback ride in every browsing skill), parity ceilings ratcheted with measured values, LLM-judge prompts and the E2E fixtures speak Aside-first, browse-fallback. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: Aside first, gstack browser fallback README, BROWSER.md, docs/, CONTRIBUTING, CLAUDE.md, ARCHITECTURE, AGENTS.md, TODOS and the root router describe the one product story: Aside is the browser gstack drives first; the bundled headless browser is the automatic fallback (Linux, Windows, app closed) where cookie import, GStack Browser, pair-agent and browser-skills still apply. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore: regenerate SKILL.md docs, llms.txt, agents digest, ship goldens, context-budget fixture bun run gen:skill-docs over the templates; goldens re-rendered; context-budget ceilings recaptured. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * v1.80.0.0: Aside is the browser gstack drives first; the bundled browser is the fallback MINOR: new capability across ten skills, the renderer and research; nothing removed. CHANGELOG release summary + itemized changes; VERSION 1.80.0.0; package.json 1.80.0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(todos): file non-Claude host ownership-gate and version-heading pin follow-ups Two follow-ups from the /plan-ceo-review + /plan-eng-review pass on merging PR garrytan#2804 with main's v1.80.0.0 ownership gate: bring the Codex/Factory/ OpenCode/Cursor/Kiro copy loops and the stale-render prune under the .gstack-owned marker rule, and a free test pinning that the CHANGELOG top heading equals VERSION (the collision that git cannot see). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: pre-landing review fixes for the Aside-first branch Review army + adversarial passes (Claude and Codex) on the merged branch: setup - _prune_stale_generated scans the host dirs too (the generator already removed the render before setup ran, so the host branch was dead), skips symlinks in the render tree (rm -rf on a slash-terminated link empties its target), removes a host symlink only when it resolves into gstack, cleans a bannered real dir through _cleanup_weak_dir, recognizes frontmatter-renamed skills, and logs through log. The always-run codex render passes every host dir that may link to it. - NEEDS_BUILD checks all three binaries (with $_EXE) and lib/ sources; the browser hint and the bootstrap summary honor GSTACK_SKIP_ASIDE, treat a requested skip as a request, and derive one skill list. lib/aside-render.ts + bin/gstack-render.ts - The loopback server carries a per-render secret path, checks containment on the real path (symlink escapes are 403), and rejects malformed encoding. - Inline eval results are one base64 line, so page text cannot forge ASIDE_DIR= or the sentinel; the last ASIDE_DIR wins. - runProc escalates SIGTERM to SIGKILL, bounds every wait, and clears every timer (an uncleared one kept gstack-render alive after printing OK). - renderTmpDir refuses a shared /tmp name owned by someone else; the work dir and server are created inside try; goto's budget follows the render budget. - probeAside classifies a present-but-failing CLI as ASIDE_NOT_RUNNING like the skills' bash probe; render() retries on gstack's own browser when Aside could not start or its private CDP bridge is gone (never on a page error or a timeout of a running script); the CLI reports the engine that actually rendered, exits 0 on --help, rejects non-numeric flags, documents --wait-timeout, fences EVAL/PAGE_ERRORS as untrusted content, and names the daemon's cookie-import JS lock remedy. - The browse path passes --scale only when asked (a scale change rebuilds the daemon context) and restores the viewport after a sized screenshot. resolvers / templates - The bash probe honors GSTACK_SKIP_ASIDE and has a perl deadline on stock macOS; .local is no longer LOCAL (mDNS); same-origin filters compare parsed origins; link status is HEAD-checked only on LOCAL targets; every aside exec goes through the receipted _aside_exec prelude ({{ASIDE_EXEC_PRELUDE}}), including nine template blocks that called it bare; the design sketch and diagram staging use private directories. - The generator prunes only bannered renders and never a host whose generation failed. Docs, stale comments and dead code cleaned; goldens re-rendered; tests updated and added for every behavior above. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test: coverage for the render CLI, setup rebuild check, make-pdf exit codes, and prose $B spans New free tests from the ship coverage audit: test/gstack-render-cli.test.ts (argv guards, --help, output contract with a fake daemon, failure and serve-root paths, no-browser case, prompt exit), test/setup-needs-build.test.ts (every binary and source set flips NEEDS_BUILD, Windows suffixes), make-pdf/test/cli-exit-codes.test.ts and setup-smoke.test.ts (error to exit code mapping, runSetup stages, renderPdf's engine), and prose-span cases for extractBrowseCommands in test/skill-parser.test.ts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: CHANGELOG and TODOS cover the review fixes (v1.81.0.0) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: sync project docs with the v1.81.0.0 review fixes BROWSER.md, ARCHITECTURE.md, CONTRIBUTING.md, README.md, CLAUDE.md, docs/TESTING_INTERNALS.md and docs/PROJECT_STRUCTURE.md now describe the shipped renderer and setup: the loopback render server's per-render secret path and real-path containment, ENGINE= naming the engine that actually rendered (mid-run retry on gstack's own browser), EVAL/PAGE_ERRORS fenced as untrusted content, --wait-timeout and the CLI's argv guards, the receipted _aside_exec prelude ({{ASIDE_EXEC_PRELUDE}} in the placeholder table), the LOCAL host rule without .local, LOCAL-only HEAD checks in the links script, GSTACK_SKIP_ASIDE across probe/renderer/setup, the ownership-gated retired-skill prune, the widened NEEDS_BUILD check, and the new free tests (gstack-render-cli, setup-prune-stale-generated, setup-browser-hint, setup-needs-build, make-pdf cli-exit-codes and setup-smoke). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: CHANGELOG states the precise mid-run retry rule Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(test): skill-e2e-bws slices the $B setup block from the Browser fallback section browse/SKILL.md no longer has '## SETUP' / '## Core QA Patterns' (Aside is the primary driver; the $B block moved under 'Browser fallback'), so the gate test sliced an empty block and handed the agent nothing to run. Anchor on '### Find the `$B` binary' up to the next heading. 7/7 pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(test): gate POSIX-only fixtures off Windows windows-free-tests: the gstack-render CLI tests drive a shebang fake browse that CreateProcess cannot exec, and two NEEDS_BUILD cases assert an execute bit and a bare-name miss that MSYS bash does not have (test -x ignores mode bits and resolves design -> design.exe). Those describes and cases now self-skip on win32; argument guards, --help, the no-browser case, and every other rebuild-check case still run there. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(render): runProc waits for the exit code until the kill deadline; newtab retries once on a cold daemon A process whose pipes have reached EOF is exiting, but runProc gave the exit code only five seconds to arrive and then returned null, which run() reports as a failed command. Under CI's six-shard load one such render failed with the artifact already written. The SIGTERM/SIGKILL timers already bound the wait, so the exit race now runs to the kill deadline. The first CLI call auto-starts the browse daemon; on a cold start it can answer 'Unable to connect' once while the server is still coming up. That single case is retried after 1.5s; every other newtab failure is not. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(aside-render): warm the daemon before live fallback cases; failures name the render error - Live fallback cases run 'goto about:blank' up to twice before asserting and skip (never fail) when the daemon cannot come up. - expectOk() puts r.error and the browse transcript into the assertion so a failed render is diagnosable from the CI log. - The argv-contract cases dump the fake's log on a miss. - File default timeout is 30s: the subject is the CLI contract, not latency. - Two cases pin the cold-daemon newtab retry and that other errors are not retried. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: CHANGELOG notes the cold-start tolerance of the bundled-browser renderer Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Sina <sdroid674+github@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…edes garrytan#2831) (garrytan#2836) * feat: add optional Memorable workflow memory (cherry picked from commit 6bd5d47) * fix: refuse the second registration, and say what leaves the machine Two things the first cut got wrong. Memorable's own installer registers the same UserPromptSubmit hook, under its own name and outside gstack's table. `memorable start`, `memorable setup` and `memorable install-hooks` all do it, and that is the documented way to install the CLI, so on most machines it is already there before gstack is asked. Registering ours beside it ran the same command twice on every prompt: context injected twice, and the session captured twice against the user's own extraction allowance. `enable` now looks for it and refuses, naming the entry and the file it lives in; `status` says who registered it rather than reporting none. Matched on the command rather than on a tag, for the reason the hook table already gives: Claude Code rewrites settings and private tags do not survive it. The removal instruction says to delete the entry by hand because Memorable has no command that removes its own hook. `uninstall-hooks` is not a command in 0.5.18; it answers "unknown command". The README said "Memorable, not gstack, owns the captured data and any network access", which answers the question by pointing away from it. It now carries a per-command table of exactly what leaves the machine, in the shape the adopted gbrain section uses, and it is explicit that the hook makes no network call of its own, that every row is the third-party CLI acting under its own consent, and that `gstack-egress` will therefore not show any of it. Under it, the split between what gstack pin-tests (the gating and the wiring) and what is Memorable's claim (storage, sending, and what disable and forget erase). The CHANGELOG entry is removed. This file has never carried an [Unreleased] heading; every entry is a version and a date, written at release. The text is in the pull request for whoever cuts the next one. Three tests added: enable refuses and touches neither consent nor settings when Memorable already holds the hook, status names that registration, and a foreign UserPromptSubmit hook is not mistaken for Memorable's. (cherry picked from commit e0899af) * docs: a guide for the Memorable bridge, in the gbrain-sync shape README carries the section and the egress table; this is the page it links to for anyone who wants the whole thing. Same shape as docs/gbrain-sync.md, which is the closest thing in the repo: an optional integration whose interesting questions are all about what leaves the machine and how to turn it off. What it covers that the README cannot at that length: that the hook sees every Claude Code prompt rather than only the ones a skill produced; that capture is a separate consent from this bridge, so turning the bridge off does not turn capture off; what to do when Memorable has already registered the hook itself, which is the common case because its own installer does it; and why the hook has no loud failure path. The egress table is repeated here rather than linked, because the sentence it is answering ("what does this send") is the one somebody arrives on this page already asking. Co-authored-by: Advaiyt Sane <advaiyt.sane@gmail.com> Co-authored-by: Nikhil Krishnaswamy <krishnaswamynikhil@gmail.com> (cherry picked from commit 5c108cc) * feat(settings-hook): identity-aware remove-source + read-only list-items remove-source used to inspect only entries still carrying the _gstack_source tag. Claude Code strips that tag when it rewrites settings.json, so an off switch built on remove-source alone silently no-oped on exactly the entries it was written for. Removal is now driven by KNOWN_HOOKS identity for the requested source (tagged or not), keeps the tagged-single-item legacy-stray rule, never touches another source's items, and leaves entries with nothing of ours byte-identical. list-items is the read-only view of the same identity table: one JSON string literal per matching hook command, filters (--owned-by, --command-regex as a JavaScript RegExp) applied inside the JS, empty stdout for no match, and the mutating verbs' exit codes (1 usage, 3 unparseable settings, 4 unexpected shape) so callers can decide mutations from its output without parsing raw command strings. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(config): memorable_recall consent key (on|off, default off, reject-and-preserve) The gstack-side gate for the Memorable UserPromptSubmit bridge. `on` lets a Claude Code hook hand every prompt to a third-party binary, so the key follows the codex_reviews rule: an invalid value is rejected and the stored value kept, never coerced in either direction. Registered in all four places gstack-config keeps in sync (annotated header, DEFAULTS table, the set validator, and both enumeration loops). Memorable's own capture consent (`memorable enable`) is a separate thing gstack never sets. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(hooks): memorable-user-prompt-hook.ts — consent gate, deny veto, HIGH-tier pre-scan, fail-closed receipt, trust envelope; runExternal in spawn-bin The PR's hook exec'd the vendor binary with the full environment and passed its stdout to Claude verbatim. It is now the house pattern: a fail-open bash shim over a .ts twin that (1) gates on the memorable_recall consent key, (2) skips repos whose trust policy is deny or read-only, (3) scans the prompt (raw bytes and decoded string leaves) and refuses to hand over a HIGH-tier credential shape, (4) writes a fail-closed egress receipt naming the local executable it ran, (5) spawns the vendor in its own process group with an allowlisted environment and group-kills it on timeout, (6) accepts only a string additionalContext back, caps it at 8 KiB on a UTF-8 boundary and wraps it in the trust envelope, and (7) records an `output-written` outcome after the stdout write completes. One deadline clock (4.5 s) undercuts Claude Code's 5 s kill and bounds both ledger writes through the new lockBudgetMs option on writeReceipt/writeOutcome (default unchanged). spawn-bin gains runExternal for external executables (detached group, stderr drained, stdin EPIPE handled, stdout capped, win32 refused). The wiring test pins the sink fail-closed and sweeps hosts/. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(gstack-memorable): canonical hook path, no vendor consent, --timeout 5, identity-based status, verified disable, lifecycle lock enable used to bake the hook path from whatever tree the CLI ran in and to run the vendor's own `memorable enable` (its consent for storing AND uploading session traces) before registering anything. It now resolves the canonical install like setup does and refuses when that install does not carry this bridge (version and hook-twin check), registers through the canonical hook manager with --timeout 5, records gstack's own consent in memorable_recall, never executes the vendor, and restores the captured prior state if consent cannot be recorded. disable flips the gate first, removes the entry by identity (tag or no tag), verifies both states and reports partial failure instead of a blended success. status reads only: resolution path, gate, registration by identity (gstack / vendor-own / both / unknown), mismatch lines, receipt count, recent hook errors. enable and disable serialise under a lock with stale takeover. Windows is refused (TODOS.md D21). Exit codes mirror the hook manager (3/4/5). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(setup): --no-team sweep keeps the opt-in gstack-memorable hook `./setup --no-team` finishes its teardown with `prune-stale --all`, which removes every KNOWN_HOOKS item. The Memorable bridge hook is a user-registered opt-in unrelated to team mode, exactly like verify-gate, so it joins the sweep exclusion list. The verify-gate pin now accepts the comma-extended list; a schema-aware case proves the exclusion keeps both opt-ins (tagged or tag-stripped) while the uninstall sweep still removes them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(uninstall): named gstack-memorable arm, vendor-consent notice, honest kept config The identity sweep already removed the Memorable bridge hook as an unnamed stray. It now has a named arm like every other source, so the summary says what went, and says plainly that Memorable's own consent (if the user granted it) is theirs to revoke. Under --keep-state the kept config is set memorable_recall=off so it never claims a hook that is gone. The canonical-paths pins cover the sixth KNOWN_HOOKS row and the new uninstall source. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(egress): memorable-recall row in gstack-egress grants `gstack-egress grants` promises every standing consent in force with the command that revokes it. The Memorable bridge's memorable_recall key is one, so it gets a row: off by default, granted only when `gstack-memorable enable` set it, revoked by `gstack-memorable disable`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(memorable): accurate bridge guide; README row, Docs table, privacy pointer; PROJECT_STRUCTURE The README section becomes one row in the Standalone binaries table (the shape every other binary uses) plus a Docs-table row and one Privacy bullet saying that optional third-party bridges are off by default and receipted. The guide now separates the two consents, says what gstack hands to the vendor binary and what the receipt can and cannot attest, attributes every statement about the vendor's network activity to the vendor, describes the hook manager accurately (identity via KNOWN_HOOKS, list-items vs list-sources, rollback is whole-file), states the Claude Code hot-reload behaviour, adds a troubleshooting runbook, and keeps the contributors' credit in the CHANGELOG idiom. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore(todos): Memorable bridge follow-ups Filed from the CEO and eng reviews of the bridge fix-up: the generic third-party hook seam, Windows support (deferred whole), the envelope kind parameter, a vendor payload-minimization contract, a latency and timeout revisit, resolver and canonical-root consolidation, a non-interactive MEDIUM-tier redaction policy for hooks, and adopting list-items at setup's plan-tune check. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings-hook): list-items --owned-by with --command-regex intersects When both filters are given, an item must satisfy both: owned by the requested source AND matching the pattern. Before, the regex branch skipped every owned row, so the combination could never match. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(gstack-memorable): compat_check reads list-items output before grepping Under pipefail, piping the probe straight into grep -q let a non-zero probe exit mask the match, so a hook manager without list-items was sometimes reported as compatible. Capture the output, then grep. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test: coverage for the memorable bridge (remove-source regression for every KNOWN_HOOKS source) - settings-hook: identity removal pinned for each source in KNOWN_HOOKS; list-items unknown flag and combined --owned-by/--command-regex - gstack-memorable: enable/disable failure paths (lock give-up exit 5 with the test-only lock timeout override, consent-write failures guarded by canRevokeWrites, canonical-version mismatch, no-bun status) - hook: non-object JSON, missing cwd, non-ASCII bytes, held-open stdin, shim without bun, stripControl, resolveVendor, runExternal ENOENT - egress-receipt: lockBudgetMs 0 and writeOutcome on garbage input - uninstall: no memorable entry present reports nothing removed Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(memorable): gbrain backend note and the settings-rewrite race Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * perf(redact-engine): line/col by binary search over a per-scan line index lineColAt walked the input from offset 0 for every finding, so a match-dense input (a pasted log full of emails and IPs) cost O(findings x bytes): 128 KiB took ~400 ms and 900 KiB tens of seconds. The line starts are now indexed once per scan, on the first finding, and each finding is a binary search. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(gbrain-repo-policy-client): repoPolicyTier accepts a spawn timeout The policy script spawn was fixed at 10 s, more than twice the memorable hook's whole budget. Callers on their own deadline pass what they can afford; a timeout reads as unreadable and polarity stays the caller's. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(spawn-bin): runExternal resolves on the child's exit, keeps stdin errors advisory - A vendor that exits 0 but leaves a background child holding its pipes was held to the deadline, group-killed and reported as a timeout with its answer dropped. 'exit' now starts a short stdout drain, then resolves with the real exit code and kills whatever still holds the group. - EPIPE on the child's stdin (it answered before reading a large input) is reported as stdinError, separate from error, so a delivered answer is not classified as a spawn failure. - Stdio streams are destroyed and the child unref'd on resolve so a straggler cannot pin the hook process; tail/grace/drain sizes are named constants. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(hooks): memorable hook closes the review army's gaps - Vendor failures are logged even with empty stderr (a silently hanging vendor taxed every prompt invisibly); the stderr tail is withheld when the redaction engine finds a credential or PII shape in it; hook-errors.log is created 0600. - Trust-policy veto fails closed when git cannot run or answer in time (it read as 'no remote' before); the policy script spawn is bounded by the hook's clock; a payload cwd that is not a directory falls back. - Each secret scan is admitted by the deadline clock (the engine's cost grows with match density); stdin is decoded once. - The pre-spawn gate re-check logs a config failure instead of swallowing it; an incomplete stdin read is named as such, not as 'not JSON'. - Carriage returns are stripped with the other controls. - The vendor env allowlist adds the standard proxy, TLS and XDG variables so a vendor behind a corporate proxy or private CA still reaches its service. - A stdin EPIPE on a delivered answer is recorded in the outcome, not treated as a spawn error. - Stage caps and the truncation marker are named constants; a test-only GSTACK_MEMORABLE_TEST_BUDGET_MS can shorten (never widen) the budget. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(gstack-memorable): lock staleness from the directory mtime; honest messages - A contender that looked between the holder's mkdir and its ts write read a missing ts as 0, called the lock stale and reclaimed it; staleness now comes from the lock directory's own mtime (the settings-hook idiom). - The ensure-event failure is no longer labelled 'warning'; the consent-write rollback message says what was actually kept; a removal that left no entry is reported on stdout, not as an error; receipts are counted from the filtered JSON array, not a formatting artefact; the resolution order and lock tuning are named once. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(uninstall): memorable_recall goes off whether or not state is kept gstack-config resolves its root through GSTACK_STATE_ROOT/GSTACK_HOME, which can differ from the STATE_DIR uninstall removes; a full uninstall could leave memorable_recall=on in a config that survived. Flipped only when currently on. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test: pin the review-army fixes for the memorable bridge Nonce-scoped orphan checks (the system-wide ps grep could see another shard's sleeper); exit-with-lingering-grandchild; advisory stdin EPIPE; withheld stderr; vendor timeout logged with empty stderr; CR stripping; budget seam; rate-limit expiry and 0600 log; unreadable policy store fails closed; file-as-cwd fallback; mtime-based lock staleness and the mkdir gap. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(memorable): vendor environment allowlist and stderr policy; two follow-ups Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(hooks): memorable hook second-pass review fixes - Trust-policy lookup fails closed on any git failure that is not 'no such remote' or 'not a git repository' (a corrupt or unreadable .git/config and dubious ownership exit 128 and used to read as 'no remote'). - pickAdditionalContext takes the first complete top-level JSON object, so a vendor whose background helper appends a line to stdout (or prints a banner first) does not lose its answer. - The hook-errors.log rate limiter keys on a stable string (a vendor's timestamped stderr no longer defeats it); the log is chmod 0600 on every append because sibling hooks create the same file without a mode. - Scan admission is sized by payload bytes (scan() is uninterruptible). - The receipt payload class is a stable token; the prose moved to the docs. - Header, constants and comments match the behaviour (silent skips vs logged refusals; HIGH/MEDIUM withholding; STAGE_CAP_MS scope; runExternal in the spawn-bin header; the ledger lock budget doc). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(gstack-memorable): stale-lock takeover by atomic rename; comments match behaviour Two contenders that both saw a stale lock could both reclaim it with rm -rf; the settings-hook idiom (mv to a private name, exactly one winner) is used instead. The hook-manager fallback comment now says every verb falls back. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test: second-pass coverage for the memorable bridge Policy lookup outcomes (plain dir, repo without policy, corrupt .git/config fails closed); repoPolicyTier timeoutMs; line/col at line starts, after blank lines, CRLF and first char; tolerant first-JSON-object parsing; keyed rate limit; uninstall never creates a config just to say off and flips consent in a GSTACK_STATE_ROOT outside the removed state dir. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(memorable): silent skips vs logged refusals; payload class token; D21/D24 anchors Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(hooks): memorable hook survives host termination and brace-bearing banners - The bash shim runs bun as a job and forwards SIGTERM/SIGINT/SIGHUP (bash holds a signal until a foreground child exits); the .ts kills the in-flight vendor's process group on the way out (runExternal exposes the group kill through onSpawn), so a hook the host terminates cannot leave the vendor running with the prompt on its stdin. - The tolerant stdout parser tries every complete top-level object (bounded) and takes the first carrying a string additionalContext, so a banner with braces or quotes, or a progress object, no longer costs the answer. - git runs with LC_ALL=C and the not-a-repository check is anchored to the start of its message: a localized git or a repository path containing the phrase can no longer flip the lookup. - The rate limiter remembers up to 32 live keys, so alternating failures cost two lines, not one per prompt. - Unicode format characters (bidi overrides, zero-width spaces) are stripped from vendor text at egress; the zero-width joiner stays for emoji. - A killed child (timeout, ENOBUFS) resolves on exit without the stdout drain, and the post-kill grace is 100 ms, so the timeout outcome fits the reserve. - The ledger size warning, which the host discards from an exit-0 hook's stderr, is logged where status looks. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(gstack-memorable): failed stale-lock takeover reaches the give-up; disable runs without gstack-config; status shows ledger size Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test: host termination kills the vendor group; brace banners, decoys and format characters; non-reclaimable stale lock gives up; ledger line in status Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(memorable): the vendor dies with a terminated hook Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test: deterministic stdin EPIPE case for runExternal (child closes stdin, stays alive) Under parallel shard load a child that merely exits fast raced the write and the EPIPE was not always observed; closing the read end first makes it so. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(hooks): outside-model review fixes for the memorable hook and runExternal - Nothing in the vendor's process group outlives the call: the group is killed on every resolve, the clean 'close' path included (a helper the vendor forked with redirected stdio ran on unsupervised before). - A child that already exited when the deadline fires keeps its result; the deadline only ends the drain instead of rewriting a completed exit as a timeout. - The decoded-leaf scan reports when its node/depth bound cut the walk short and the hook refuses the hand-off as unscanned; object keys are scanned too. - git for the trust-policy lookup runs without inherited GIT_* selectors (GIT_DIR, GIT_WORK_TREE, GIT_CONFIG_*), so it inspects the session's repository and no other; a host cancellation kills that git as well. - An unmatched brace in a vendor banner no longer hides the answer after it. - The stderr tail is scanned whole before it is cropped for the log, so a credential's prefix cannot be cropped away from its secret half. - The vendor override reads an empty GSTACK_MEMORABLE_BIN as unset, exactly as bin/gstack-memorable does, so enable checks the binary the hook runs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(gstack-memorable): stale-lock reclaim checks the inode it judged and the owner's liveness; status reports a failed receipt query as unknown Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(uninstall): revoke memorable_recall independently of the hook manager and name a failed revocation Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test: group kill on clean close, walk exhaustion refused, GIT_DIR cannot bypass the deny, unmatched-brace banner, whole-tail scan, empty-override parity, uninstall revocation without the hook manager Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(memorable): the process-group guarantee and its setsid boundary Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore(release): v1.83.0.0 — Memorable recall bridge, identity-aware hook removal, faster redaction line/col CHANGELOG entry for the Memorable workflow-memory bridge (opt-in, off by default, Claude Code only), the identity-aware remove-source and read-only list-items in the hook manager, the memorable-recall egress sink, runExternal, lockBudgetMs, and the binary-search line/col index in the redaction engine. No migration. Contributed by @AdvaiytSane and @NIkhil-cmd-cmd (garrytan#2831). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test: stdin EPIPE observation is scheduling-dependent under shard load; pin the invariant instead A delivered answer is never reclassified as a spawn error; when the EPIPE is observed it is reported as stdinError. Whether it is observed before the child's exit resolves the call is not something the test can force. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: list the memorable-recall sink among the fail-closed egress sinks CLAUDE.md and ARCHITECTURE.md enumerate the receipt sinks that refuse to send when the ledger cannot be written; the Memorable bridge's per-prompt hand-off is pinned fail-closed in test/egress-receipt-wiring.test.ts but was missing from both lists. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test: memorable bridge tests pass on a runner that enforces file modes; scanner-proof key fixture - The consent-write-failure tests make the state dir read-only, which also blocked the bridge lock directory (exit 5 before the path under test); the locks dir is pre-created so only the consent write fails. - The unreadable-store test leaves a 0600 directory behind (the policy script chmods the store path); cleanup restores the search bit and the suite's afterEach reopens directories before removing. - The AWS-key-shaped fixture is built by concatenation, as every sibling test does, so the CI credential gate does not read it as a live key. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(gstack-memorable): name an unwritable state directory when the lock cannot be created Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: AdvaiytSane <advaiyt.sane@gmail.com> Co-authored-by: Nikhil Krishnaswamy <krishnaswamynikhil@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…skills, DOM-mode scans, open DESIGN.md format, one typed slop catalog (garrytan#2832) * chore(design): pin impeccable rule ids and detector JSON shape as fixtures Real captures from a human-initiated `npx impeccable install` in a scratch directory (engine 0.1.3, linux-x64), never a runtime download: - test/fixtures/impeccable-antipatterns.json: upstream crates/live/assets/antipatterns.json at 87d8f6d6 (the state engine-v0.1.3 shipped), 61 rules, source commit recorded in `_source`. - test/fixtures/impeccable-detect-sample.json: `detect --json` over gstack's planted-slop fixture (source mode), paths normalized. - test/fixtures/review-eval-design-slop.dom.html + impeccable-detect-dom-sample.json: the same page served locally, dumped through the browse engine with the shared DOM-dump script, then scanned. Pins the load-bearing assumption that the static engine reads inline <style> in a .html file: the DOM scan yields the same id set as the source scan. - lib/dom-dump-script.ts: the one dump script both browser engines evaluate (IIFE, no single quotes). Folds CSSOM rgb() back to author hex so palette rules still fire, and removes inlined <link> nodes so the engine does not warn about an unresolvable stylesheet. Both verified against the engine. - test/fixtures/impeccable-detect-help.txt + impeccable-captures.meta.json: the flags, exit codes, finding fields, and re-capture protocol. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(design): typed slop catalog in lib/; AI_SLOP_BLACKLIST derived lib/design-catalog.ts is the single source of truth for gstack's design anti-pattern vocabulary: the 11 legacy blacklist lines (verbatim, flagged `legacyBlacklist`), every one of impeccable's 61 registry ids with gstack prose, tier, impact, confidence, grep heuristic, and /impeccable handoff, plus the gstack-only tells the LLM pass judges (hero metrics, identical cards, glassmorphism, missing states, unthemed browser surfaces, ...). `impeccableId` is set only when the id exists in the registry fixture, and `renderCatalog({style:'ids'})` brackets an id only then, so rendered prose never shows an id the detector cannot emit. Role-scoped font lists (OVERUSED_FONTS_DISPLAY, BANNED_FONTS, FONTS_BODY_UI_OK, FONTS_MONO_OK, FONTS_VERIFIED_FREE) live beside the entries. scripts/resolvers/constants.ts now derives AI_SLOP_BLACKLIST from the catalog. Generated output is byte-identical (bun run gen:skill-docs is a zero diff). Pure module: no I/O, no scripts/ imports, loading prints nothing, so bin/ can import it at runtime on every host. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(review): generate review/design-checklist.md from the catalog review/design-checklist.md was hand-written and its own header admitted it drifted from DESIGN_METHODOLOGY category 9. It is now rendered by scripts/resolvers/design-checklist.ts from lib/design-catalog.ts: category 1 lists every grep-detectable slop entry plus the legacy blacklist lines, sorted HIGH/MEDIUM/LOW, each with its heuristic and, where the detector knows the rule, its bracketed id (27 items, up from 6). The font blacklist renders from BANNED_FONTS. Categories 2-5, Instructions, Classification, Output Format, and Suppressions keep their prose. Title and slop heading are unchanged (test/skill-e2e-review.test.ts and hosts/opencode.ts key on them). gen-skill-docs writes the file for the Claude host only (a Claude-side runtime asset; other hosts copy or inline the render), honors --out-dir, and reports STALE/FRESH under --dry-run like sections do. test/design-checklist-sync.test.ts pins committed == generated, the host/out-dir scoping, and the dry-run freshness line. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(design): modes, craft-floor reflexes, calibration, catalog in doctrine DESIGN_HARD_RULES: the classifier names four visitor modes (Persuade, Operate, Read, Experience, plus Hybrid per section) and keeps the MARKETING/LANDING PAGE and APP UI aliases; Read and Experience get three rules each; a "Reflexes no detector catches" block (browser surfaces, one authored motion moment, depth has an offset, tinted secondary text, space above headings, light-or-dark from the use scene) and the three-looks calibration follow the universal rules. The slop section renders the 11 legacy lines plus the detector rule ids and judgment tells from the catalog; in design-review, which also renders DESIGN_METHODOLOGY, it becomes a one-line pointer so the catalog is paid for once. Header counts are computed, not hardcoded. DESIGN_METHODOLOGY: category 9 renders the catalog in three registers (legacy lines verbatim, detector rules that need judgment with bracketed ids, gstack-only judgment tells as prose, polish-level ids on one line); categories 5 and 7 carry the browser-surface and one-motion-moment reflexes; the typography overused-face item points at [overused-font] with the role-scoped exception. The consultation Codex prompt's anti-slop line reads from the catalog. Budget: design-review eager 25.6K -> 27.0K (ceiling 27,984), plan-design-review unchanged at 17.4K; no carve-guard or context-budget re-baseline needed; ship goldens unchanged (ship never renders the hard rules). Derived from pbakaus/impeccable reference/craft-floor.md + new-work.md (Apache-2.0), rewritten. See NOTICE.md (commit 12). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(design-consultation): font procedure, role-scoped overused list, color strategies The proposal section stops handing out a font menu. "Choosing faces: a procedure, not a menu" names the subject's world, shortlists per role, strikes the overused list for that role, verifies availability in-session, and states the loading strategy. {{OVERUSED_FONTS}} renders the role-scoped lists from lib/design-catalog.ts: overused as display (the detector's overused-font set plus the training-data defaults), fine as body/UI on an Operate or Read surface, mono for data and code, banned in any role, and a short verified-free list with its verification date. Color approaches become Restrained / Committed / Full palette / Drenched. The anti-convergence directive drops light-vs-dark as a dial (it comes from the use scene) and the three-looks calibration sits under Your Design Knowledge. The slop list is {{DESIGN_SLOP_BULLETS}}: prose from the catalog, no rule ids, polish-level tells omitted. design-html's "Never include (AI slop blacklist)" list keeps its literal (carve guard) and each line now carries a trailing <!-- id --> naming a catalog entry, pinned by test/design-catalog.test.ts so the last surviving duplicate is derived-by-test. Both resolvers are registered and listed in ARCHITECTURE.md. No carve-guard or budget re-baseline needed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(bin): gstack-design-detect wrapper + design_detector config key bin/gstack-design-detect.ts finds and runs an impeccable engine the user installed; it never installs, downloads, or executes anything that could download. `probe` reads only: config (design_detector off → DISABLED), IMPECCABLE_BIN (absolute, realpath outside the repo and cwd), a PATH walk (absolute entries outside the repo; a #! shim counts as launcher-present, never READY), the ~/.impeccable/bin/<newest semver>/ cache, and the engine installed beside a skill launcher (scripts/bin/<os>-<arch>/impeccable, the layout a real install produced). It reports IMPECCABLE_SKILL, host-aware IMPECCABLE_HOOK (+ HOOK_OTHER), the ignore lists from .impeccable/config*.json, IMPECCABLE_ENGINE_UNTESTED for versions outside the fixture set, and a hint only when a launcher exists without its engine. `scan` re-probes, refuses URLs and anything outside the repo root or the design-report allow-list (realpath, so symlinks cannot escape), derives `--changed <base>` targets NUL-safely through git and lib/frontend-scope.ts, batches 100 absolute paths per engine call with stdin ignored, a SIGKILL timeout, a 50 MB stdout cap, and sanitized length-capped fields, then prints one normalized JSON document (--format gstack) or the engine's bytes (--format raw); DETECT_TOP (fenced as untrusted content), DETECT_SUMMARY, and DETECT_EXIT go to stderr; exit code passes through with 1 over 2 over 0; exit 3 is a gstack bug. `rules` prints the mapped set. Every run appends a content-free line to the local analytics file. lib/design-detect-contract.ts owns every sentinel string, the limits, and the normalized-finding shape (pure module); test/design-detect-contract.test.ts asserts every sentinel-shaped token the agent can read exists there. lib/frontend-scope.ts mirrors gstack-diff-scope's frontend arm, pinned by a parity test that runs the bash script. bin/gstack-config gains design_detector (auto | off, default auto, invalid values rejected with the file unchanged). test/fixtures/fake-impeccable.ts is the env-driven engine stand-in; test/gstack-design-detect.test.ts covers READY/NOT_CACHED/ NOT_AVAILABLE/DISABLED, env trust (.env never loaded, in-repo IMPECCABLE_BIN ignored), newest-semver cache, hook and ignore detection, refusals, exit passthrough, raw byte-identity, normalization, the display cap, timeout, parse errors, diagnostics, --changed, and analytics. The egress scanner test records the wrapper as a documented non-sink. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(design): {{DESIGN_DETECTOR}} wired into design-review, ship review-lite, review army, design-html The user-installed impeccable engine becomes a deterministic pre-pass in four skills, through one resolver with three renders: {{DESIGN_DETECTOR}} (the probe block and how to read every sentinel), {{DESIGN_DETECTOR:phase0}} (design- review's mechanical scan), {{DESIGN_DETECTOR:gate}} (design-html's bounded slop gate). Every rendered invocation is `bun --no-env-file run <bin>/gstack-design- detect.ts ... --host <host>` and every scan ends with the DETECT_EXIT_CODE echo so exit 2 (findings) never aborts a block. design-review: probe in Setup; Phase 0 picks DOM mode (URL target) or source mode (diff-aware, no URL) once; source mode scans the changed frontend files in Setup, DOM mode never reads source (Rule 4). Phase 3 gains a DOM-dump step per page: both browser engines load the shared script from lib/dom-dump.js (Aside splices it into a double-quoted repl script; the fallback engine copies it into a temp dir for `$B eval --out --raw`), the dump is size-capped, run through gstack-redact (a HIGH finding skips the page), and persisted under $REPORT_DIR/dom/$RUN_ID/; one scan runs after the last page, labeled "static scan of the rendered DOM; cross-origin CSS not resolved". REPORT_DIR honors GSTACK_HOME so the wrapper's allow-list and the report dir agree; RUN_ID is set once in Setup. design-baseline.json is schemaVersion 2 with runId, targetSet, base, and a detector block (mode, engine, byRule, byPage), written temp+rename with a per-run copy; Regression Output diffs ids only when mode and target set match, caveats an engine change, and calls live-page count deltas advisory. Phase 7 hands deferred detector findings to the `handoff=` command the scan printed; Phase 9 recomputes the same way and deletes the dumps unless --keep-dom; Phase 10 reports `Detector: N → M`. ship review-lite gains step 0 (probe, `scan --changed <base>`, tier buckets, detector + checklist dedupe, advisory and ignored never count) and a `detector` count in its log payload; the PR body gets a Detector line (rule ids and counts only). The Review Army Design specialist runs the mechanical pass at the top of review/design-checklist.md, which now carries it. design- html probes after DESIGN_SETUP and runs the one-pass gate before screenshots. lib/dom-dump.js is generated by gen-skill-docs from lib/dom-dump-script.ts (Claude host, --out-dir aware, dry-run freshness) and pinned byte-equal, so the prose never carries the script. The contract gains DETECT_JSON, DOM_DUMP_OK, and the self-describing set; its test now checks both directions. Budget: design-review eager 25.6K → 28.5K. The plan's target was +2.5K; after the levers it named (ids-only detector rules, no inline script, trimmed prose) it lands at +2.87K, and the remainder is doctrine and detector wiring, so the ceiling moves to the captured 31,319 for design-review only (the full capture would also have loosened 21 ceilings this branch never touched; those stay). design-html skeleton re-baselined to 54,000 (measured 53,592). Codex and Factory ship goldens refreshed (review-lite step 0 and the PR-body line render inline there). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(design): catalog never-lines in the mockup prompt Ten catalog ids carry `mockupNever` (kicker-above-heading, icon-tile-stack, gradient-text, ai-color-palette, cream-palette, nested-cards, dark-glow, pulsing-dot, identical-cards, hero-metrics) and lib/design-catalog.ts exports their deduped plain-English names as MOCKUP_NEVER_NAMES. briefToPrompt() in the design binary appends "Never: <names>." before its fixed tail, so `$D generate | variants | evolve` stop reaching for purple gradients, icon tiles, and cream defaults before the comparison board opens. The binary still bundles (`bun build --compile design/src/cli.ts`); ./setup rebuilds it. design-html's Never-include list now covers every mockupNever id (kicker / icon tile, hero metric rows, gradient text, cream palette, nested and identical cards, glow and pulsing dots), each line tagged with its catalog ids; test/design-catalog.test.ts pins the exact ten flags, the deduped names, and that the template list is a superset. New design/test/brief.test.ts pins the prompt shape. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(lib): open DESIGN.md reader/writer + gstack-design-md bin lib/design-md.ts implements the open DESIGN.md format (google-labs-code/ design.md, Apache-2.0): YAML front matter with the five token groups (colors, typography, rounded, spacing, components) and eight canonical `##` sections in spec order (Overview, Colors, Typography, Layout, Elevation & Depth, Shapes, Components, Do's and Don'ts), aliases mapped, extras preserved after them in their original order. parseDesignMd never throws (unparsable front matter → `unknown` with a reason); renderDesignMd re-emits the preserved front matter bytes and only `convert` writes fresh YAML through a small block-style emitter (Bun.YAML.stringify is flow style); upsertSection splices the body only; tokensFlat resolves `{path}` references to primitives and reports group, self, dangling, and cyclic refs as DESIGN_MD_TOKEN_REF_INVALID. convertLegacy turns gstack's pre-spec DESIGN.md into the open format: Product Context and Aesthetic Direction fold into Overview, Typography roles become display/body/label/mono tokens (mono carries fontFeature: tnum), Color hexes become colors (mode-qualified labels keep their qualifier; strategy lines are not colors), the Spacing scale and Layout radii become spacing and rounded, Motion / Grain Texture / Decisions Log survive as extras. The format marker lives inside the file: a YAML comment on line 2 of a spec file, an HTML comment on line 1 of a legacy file. bin/gstack-design-md.ts: `check` (DESIGN_MD_FORMAT + marker), `convert [--write]` (backup to DESIGN.md.legacy.bak, temp+rename, refuses ambiguous input with DESIGN_MD_CONVERT_REFUSED), `tokens` (flat JSON), `mark <spec|legacy-keep>`. Exit 3 + DESIGN_MD_INTERNAL_ERROR is a gstack bug. design/src/memory.ts: updateDesignMd upserts "Extracted Design Language" through the lib (front matter bytes untouched, canonical order kept, section replaced on rerun) and creates a spec skeleton with tokens from the extraction when no file exists; readDesignConstraints leads with the flat tokens and the Overview for spec files. The design binary still bundles. test/design-md.test.ts pins all of it against gstack's own DESIGN.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(design): write/read DESIGN.md in the open spec; persisted format choice gstack's design skills now write DESIGN.md in the open DESIGN.md format and read tokens from it. {{DESIGN_MD_CHECK}} renders the format check through bin/gstack-design-md.ts: design-consultation's Phase 0 settles the format once (spec → update tokens in the front matter; legacy without a marker → one AskUserQuestion: convert with a .legacy.bak, keep the legacy file, or start fresh; the answer is written into the file as the format marker so no skill asks again; a marker already present is obeyed silently; unknown → prose; missing → Phase 6 writes one). Phase 6's template is the spec form: YAML front matter with name, description, and exactly the five token groups (colors, typography.display/body/label/mono with fontFeature: tnum on mono, rounded, spacing, components with {path} references), then Overview (Creative North Star, product context, mode per surface, references, key characteristics), Colors (opening with the Restrained / Committed / Full palette / Drenched strategy), Typography, Layout, Elevation & Depth, Shapes, Components, Do's and Don'ts, plus gstack's Motion and Decisions Log as extras; the template ends with a check that the file parses as `spec`. design-review runs the `:calibrate` form in Setup: a spec file's flat tokens are the calibration source (a value present in the tokens is never a finding), the marker is respected, and conversion is never offered there; its DESIGN.md export writes the spec form. design-html's token extraction writes the spec form and respects an existing choice. review/design-checklist.md category 5 and ship's review-lite step 1 name `gstack-design-md tokens` as the calibration source; plan-design-review Pass 5 cites tokens by path when front matter exists. The contract owns the bin's DESIGN_MD_MARKER / REASON / WRITTEN / BACKUP lines; the contract test's pending list closes. Carve guard: design- consultation skeleton 66,500 → 67,500 (measured 67,014; +1,508 B against the 1.5 KB cap). Codex and Factory ship goldens refreshed (review-lite step 1). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(design): PRODUCT.md prefill + /impeccable handoffs design-consultation's context gathering and design-shotgun's auto-gather read PRODUCT.md (impeccable's product-context file) when it exists: it counts as the user's prior answers, gets confirmed in one line, and is never re-asked. Neither skill opens `.claude/skills/impeccable/**`; PRODUCT.md and DESIGN.md are the shared surface, and impeccable's prose never loads inside a gstack skill. Handoffs: ship's review-lite ends each NEEDS INPUT detector row with the `handoff=` command the scan printed (`/impeccable <cmd>`) when the probe reported IMPECCABLE_SKILL: present, recommending the command and never opening its files; design-review's Phase 7 does the same for deferred findings, and `design_detector: off` silences handoff lines with the rest. Codex and Factory ship goldens refreshed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore(design): convert gstack's own DESIGN.md to the open spec `gstack-design-md convert --write` on the repo's DESIGN.md: tokens in YAML front matter (typography.display/body/label/mono, colors with their light/dark qualifiers, spacing scale, rounded scale), Overview from Product Context and Aesthetic Direction, Colors / Typography / Layout as canonical sections, Motion, Grain Texture, and Decisions Log preserved as extras, format marker on line 2. Hand-checked; `check` reports spec with no token-reference errors. A Decisions Log row records the conversion and that DM Sans stays the body face: it is on the overused-as-display list, and body/UI use on an Operate surface is the allowed exception under the role-scoped rule. The pre-conversion file lives on as test/fixtures/design-md-legacy.md, which test/design-md.test.ts now uses for its legacy cases; the converted root file is asserted to be spec. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: NOTICE, Apache license text, README interop, project structure NOTICE.md names what gstack derived from impeccable (rule ids and names in the catalog and the registry fixture; the visitor modes, craft-floor reflexes, and calibration in the design resolvers; the font procedure in the consultation template) and from Google's DESIGN.md specification (the format lib/design-md.ts implements), states that gstack does not distribute or audit the impeccable engine, and points at licenses/Apache-2.0.txt (verbatim). README: the design-consultation, design-review, and design-html rows say what changes when impeccable or the open DESIGN.md format is in play, and a "Works with impeccable" paragraph explains the pre-pass, the shared ids, PRODUCT.md and DESIGN.md as the shared surface, the handoffs, the no-nag posture without impeccable, and the off switch. docs/skills.md gets the detector paragraph under /design-review. docs/PROJECT_STRUCTURE.md lists the new lib and bin files, NOTICE.md, and licenses/. docs/designs/IMPECCABLE_INTEROP.md promotes the CEO plan (its ~/.gstack copy is flipped to PROMOTED) with a "what shipped" summary. TODOS.md files the seven deferrals from the reviews: the design-review Phases 7-11 carve (the budget lever, with the +2.87K vs 2.5K landing recorded), the Bun .env audit across bin/*.ts, the Kiro bin/lib gap, the $D check slop rubric, taste-profile interplay, the CEO Section 11 bullets, and the scan cache. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test: touchfiles, tiers, shim E2E, real-engine fixture Touchfiles: the catalog, contract, detector bin, checklist resolver, review- army resolver, and DESIGN.md lib join the dep lists of review-design-lite, design-review-fix, the design-consultation cases, and plan-design-review-no- ui-scope, so editing any of them re-selects the tests that read their output. Three new E2E keys: design-review-detector-shim (gate; source mode on a feature-branch diff), design-review-detector-shim-dom (gate; DOM mode: the slop fixture served on loopback, dumped through the browse binary with lib/dom-dump.js, persisted under a GSTACK_HOME-scoped REPORT_DIR, scanned once; self-skips when browse/dist/browse is absent), and design-html-slop-gate (periodic; one fix pass, at most two scans, remaining findings accepted with reason). Every case reaches the engine through test/fixtures/fake-impeccable.ts via IMPECCABLE_BIN from outside the temp repo, reads extracted skill sections (never a whole SKILL.md) with the installed bin path rewritten to this checkout, and asserts the probe ran, the right scan verb ran, `npx impeccable` never did, and the output carries FINDING rows tagged [ai-color-palette] and [low-contrast]. review-design-lite gets the fake engine and an eighth tally signal for a detector row; its 4-hit threshold is unchanged. test/gstack-design-detect.test.ts evaluates design-review's REPORT_DIR expression with GSTACK_HOME set and proves a dump under it is accepted by the wrapper's allow-list. The sample fixtures were real captures from commit 1 (engine 0.1.3), so there is nothing hand-written left to swap. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(design-detect): never execute a repository-controlled engine; allow-list --changed targets; sanitize engine text Pre-landing review findings (security + checklist), all reproduced before the fix: - A checked-out branch could commit `.claude/skills/impeccable/scripts/bin/<os>-<arch>/impeccable` and the probe would report READY and `scan` would run it, with the agent's full environment. Launchers and sibling engines under the repo or cwd now count as "skill present" only (IMPECCABLE_NOT_CACHED: repository-local install, and the hint never names a repository-local launcher to run); only HOME-rooted installs, IMPECCABLE_BIN, the cache, and PATH entries outside the repo qualify, all by realpath. The engine now sees a minimal environment (PATH, HOME, TMPDIR, locale, IMPECCABLE_*), never the agent's tokens. - `scan --changed <base>` pushed git-derived paths without the allow-list, so a committed symlink with a frontend extension handed a file outside the repo to the engine. Derived targets now go through the same allow-list as explicit ones and symlinks named by git are refused outright. - A repo-controlled `scripts/VERSION` with embedded newlines forged probe lines; the version is trusted only when it is semver, and every printed version is sanitized. Engine text containing the untrusted-content fence or a `SENTINEL:` prefix is neutralized with a zero-width space (neutralizeSentinels in the contract), so page text cannot close the envelope or forge a probe line. - A failing `git diff <base>...HEAD` (unknown or unfetched base) was swallowed and read as "no frontend changes"; it is now DETECT_REFUSED with exit 1. - The scan allow-list root follows `${GSTACK_HOME:-$HOME/.gstack}` like the templates and gstack-slug (config.yaml keeps gstack-config's STATE_ROOT precedence); a quoted or commented design_detector value reads correctly. Smaller: raw engine chunks are kept only in --format raw; diagnostics are capped (200 kept, 20 echoed); the engine identity hash reads size + 4 MB, not the whole binary; PROBE_STEP and ENGINE_STDERR are contract sentinels; the --verbose gate covers every probe step; analytics use one sentinel vocabulary; bare limits live in DETECT_LIMITS. The fake engine's knobs are IMPECCABLE_FAKE_* (so they pass the minimal env) and a shared test helper installs it. New tests cover each item above plus clean runs, `{}` parse errors, missing paths, and the 50 MB stdout cap. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(design-md): mark and updateDesignMd never rewrite the user's file; refuse a contradictory mark renderDesignMd re-sorted canonical section names into spec order on every render, so `gstack-design-md mark legacy-keep` (the "leave it alone" answer) and the design binary's mockup extraction reordered a legacy DESIGN.md (Typography and Layout jumped to the top) and normalized its whitespace, while the bin promised "body bytes untouched". `mark` now splices only the marker line (insertMarker) and `updateDesignMd` splices only its own section (spliceSection); every other byte of an existing file is preserved, and spec order applies only to files that open with front matter. `mark` refuses a choice that contradicts the file's format (spec on a non-spec file, legacy-keep on a spec file) with DESIGN_MD_CONVERT_REFUSED, exit 2, file unchanged. convertLegacy keeps intro prose under the title instead of rebuilding the preamble from the title alone. detectFormat returns a machine-readable `code` beside the prose reason (the bin no longer branches on reason text); the marker regexes derive from FORMAT_MARKER_PREFIX and FORMAT_CHOICES; the hop limit and legacy identity headings are named constants; slug is exported and reused; both writers use lib/fs-atomic.ts. Tests pin byte identity for mark and updateDesignMd on the legacy fixture, the refusal paths, and the preserved preamble. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(design): run the DOM dump in the page on both engines; align doctrine with the catalog The DOM-dump script is an arrow function, not a self-calling IIFE: Aside's `pg.evaluate($_DUMP)` receives the function and runs it in the page (the IIFE form executed in the repl sandbox, where `document` does not exist), and the fallback engine calls it with `$B js "($_DUMP)()" --out --raw`. Hygiene widens to every URL-bearing attribute (src, srcset per candidate, poster, action, formaction, data, ping, cite lose their query strings and fragments) and to data: URLs inside existing <style> nodes. The persist and scan blocks restate REPORT_DIR and RUN_ID literally instead of relying on a shell variable from an earlier block; the baseline's targetSet is defined per mode (repo-relative paths in source mode, page slugs in DOM mode) so DOM-mode deltas can match; the PR-body Detector line lists the states the probe can actually print. The DOM fixture is re-captured with the new script from outside the repo (the engine walks up from cwd for DESIGN.md, which the metadata now records). Doctrine contradictions the design specialist found: the landing-page motion rule matches the one-authored-moment reflex; the background rule names the catalog's halo/spotlight/stripe/grid slop instead of asking for gradients; the universal font rule is scoped to the display voice with the body/UI exceptions; "two typefaces max" allows the mono; the methodology's banned-font line renders BANNED_FONTS; Courier New is banned outright; the Brutalist, Retro-Futuristic, and Playful menu entries stop recommending system stacks, glow, and bounce; the coherence nudge uses the decoration vocabulary; Path A's gate names the display voice; font-loading prose points at the source the procedure verified; centered-everything is MEDIUM (an aggregate heuristic); the mockup guard reads "Never by default (unless the brief above asks for it)". The checklist's AUTO-FIX list renders the catalog's auto-fix rules; category 9 and the Hard Rules pointer count from the same partition helpers (detectorSlopEntries, judgmentTellEntries); the handoff list renders from HANDOFF_COMMANDS; a missing catalog id fails gen-skill-docs by name. gstack's own DESIGN.md gains border tokens and Decisions Log rows for its live-feed pulse and 11px mono labels. frontend-scope is case-sensitive like the bash arm. gen-skill-docs shares one emitGenerated helper for sections and lib-derived assets; renderCatalog keeps the one style with a caller. Tests: shared sliceBetween that fails on a missing end marker, the slop-gate fixture's real end marker, an isolated browse daemon for the DOM-mode E2E, the DOM hygiene test gated to CI or opt-in, docs notes for the two superseded plan sentences. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(design-detect): an engine is a file named impeccable outside the project; DOM dumps scan without inline ignores Second review cycle, security + checklist: - IMPECCABLE_BIN=/bin/sh (or node) was READY, and `detect` with cwd=repoRoot made the interpreter run the repository's own `detect` file. Every engine candidate (env override, PATH entry, cache, sibling) is now judged by the realpath of the FILE and must be named impeccable[.exe]; PATH and cache candidates that resolve into the repository are skipped like the others. "Inside the project" means the repository, or cwd when cwd is a project directory: HOME and its ancestors are exempt, so a URL-mode review launched from HOME still finds the HOME-rooted installs. - A base for --changed that starts with `-` was spliced into git argv (`--output=<file>` made git write a file and report no changes); an option- like or missing base is DETECT_REFUSED (not a ref name), exit 1, and the parser no longer defaults a missing value to main. - DOM dumps are the audited page's bytes, so an in-file `impeccable-disable` comment there is page-controlled: batches under the designs root run with --no-inline-ignores, repository batches keep the project's own ignores. - neutralizeSentinels covers the shapes it missed (bare sentinels such as DETECT_TOP total= and IMPECCABLE_DISABLED, the DETECT_EXIT_CODE= echo, the `[rule-id] impact=` group header) in one precompiled alternation instead of 37 replaceAll passes per field; only kept findings are normalized, and the summary's total stays the engine's count. - The minimal engine environment compares keys case-insensitively on Windows (process.env enumerates Path, SystemRoot there) and passes PATHEXT, COMSPEC, HOMEDRIVE, HOMEPATH, PROGRAMDATA. - Bare 64s move into DETECT_LIMITS; the unused SentinelName type is gone; the header states the directory-target contract (the engine's own walk). Tests: an interpreter as IMPECCABLE_BIN never runs the repo's detect file; a PATH symlink into the repository is never READY; option-like and empty bases are refused with no file written; the designs-root batch carries --no-inline-ignores and the repo batch does not; the identity label is deterministic per binary; the bare-sentinel and header shapes are neutralized; the installed fake engine works without IMPECCABLE_FAKE_OUTPUT (the helper copies the sample beside it); two tests clean up in finally. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(design-md): text-level edits keep CRLF, one section-boundary rule, control characters quoted - insertMarker and spliceSection normalized every line ending to LF, so a CRLF DESIGN.md came back rewritten beyond the one line they promised to touch. Both detect the file's dominant line ending and restore it. - parseDesignMd and spliceSection each walked headings with their own fence tracking; they now share headingLines (and upsertSection shares headingMatches). An unclosed ``` is treated as prose for that file: it used to swallow every later section on a splice. - A token value carrying a control character (an LLM-extracted font family with an embedded newline) was emitted as a bare multi-line scalar that Bun.YAML rejects, turning a freshly written DESIGN.md into frontmatter-unparsable; needsQuotes routes it through the quoted form. - The marker-line regex variants are built once beside YAML_MARKER_RE; the dead setMarker export and a no-op ternary are gone; LEGACY_HEADINGS derives from the identity list; the header diagram names the text-level editors as the write path for user-owned files; the bin validates and prints the mark choices from FORMAT_CHOICES. Tests: CRLF round-trips for both editors, a fenced ## inside a section and an unclosed fence, and a newline-bearing scalar parsing back. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(design): Aside dump script stays single-quoted; redaction gate sized to the dump cap; doctrine made consistent - The DOM-dump Aside block was the only double-quoted `aside repl` script in the tree (to splice the function text), which put the agent-filled <url> inside a double-quoted bash string: a same-origin href carrying $(...) would run in the reviewer's shell when Phase 3 opened that page. The script is single-quoted like every other Aside script and the function text enters through a closed-quote segment ('"$_DUMP"'); the fallback line is `$B js '('"$_DUMP"')()'`. A free test pins that no rendered Aside script opens with a double quote. - The persist block capped dumps at 10 MiB but ran gstack-redact with its 1 MiB default, so every real page between the two was deleted as DOM_DUMP_REDACTION_BLOCKED; the gate passes --max-bytes at the dump cap and blocks on any exit other than clean (0) or MEDIUM (2), so a redaction tool that fails to run can no longer fall through to "persist". - Dump hygiene removes <template> and <noscript> subtrees (invisible to the attribute walk), inline on* handlers, and the cross-origin <link> nodes already named in the note, so the file handed to the engine references no remote stylesheet. - Doctrine: the Codex design-voice prompts said "2-3 intentional motions" against the one-authored-moment rule; the overused-display heading scoped its ban to Persuade/Experience while the catalog and hard rules ban it everywhere; design-consultation's Important Rule 4 still said "as primary"; design-html's blacklist header is now "Never include by default" with the mockup/DESIGN.md/user-ask override the catalog grants; the slop gate honors Decisions Log and Do's and Don'ts blessings like /review does; the landing "poster" line says poster in stance, not type size; the design binary's variant dials no longer flip light/dark for variety; gstack's DESIGN.md rows name data labels (UI labels stay the DM Sans token) and call the skill-bar fill and hovers functional transitions. - design-review names how the base branch is found (gh pr view, then the repo default; never main) for the source-mode scan and the diff-aware mode. - frontend-scope matches the config globs at the repo root only, like the bash arm; the parity test carries nested samples. - Cleanups: renderCatalog's stale style option, an unused import, the identity-map bannedFontNames, the checklist header's "same entries" claim, the catalog header's consumer list, the orphaned main() docstring, the plan doc's IIFE bullet. design-html's skeleton ceiling is re-measured (54,184) for the two doctrine sentences. Tests: AUTO-FIX rendering from the catalog, the E2E slice markers checked in the free suite, the hygiene cases for templates/noscript/handlers/remote links, and the review E2E counting detector rows separately from the seven checklist plants. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(design-detect): project means below HOME; only page dumps drop inline ignores; a whole-scan budget; prototype-safe rule counts Third review cycle + Red Team, all reproduced before the fix: - With no repository, the wrapper adopted cwd as the repo root, so a review launched from HOME (URL mode can run from anywhere) rejected every HOME-rooted install as "repository-local", reported the user's own skill install with the wrong hint, and, for targets, accepted all of HOME (~/.ssh/id_rsa scanned). A project directory is now one strictly below HOME: `git init ~` never turns the user's installs into repository files, and from HOME only the designs allow-list qualifies as a target. - --no-inline-ignores keyed on "not inside the repo", which misclassified dumps when GSTACK_HOME sits under the repo and stripped the design-html gate's own `<!-- impeccable-disable -->` from finalized.html. Targets are classified as project / dom-dump (designs/<audit>/dom/**, the page's bytes) / artifact (other designs/ files, gstack-authored); only dumps drop inline ignores. - A repository's .impeccable/config.json can hide rules from the review; detector.ignoreValues was never surfaced. The probe prints IMPECCABLE_IGNORED_VALUES beside the rules, and the prose stops calling repo-config ignores "a decision the user made". - An engine id named `constructor` corrupted byRule through Object.prototype and `__proto__` counts vanished; byRule is a null- prototype object and an id that fails the shape check is `unmapped` as a key too. - Batches ran with no total budget (10,000 un-ignored files: hours). The scan stops at 5x the per-batch timeout with DETECT_TIMEOUT and exit 1. - The scan JSON carries an `untrusted` list of the engine- and page-derived fields, so the agent reading past the fenced DETECT_TOP block is told what is evidence. - The PATH walk keeps launcher-present for a .cmd wrapper or a differently named real file (the name gate applies to READY only). Tests: probe and scan from a fake HOME (cache READY, HOME file refused, dump scanned without inline ignores), artifact vs dump batches, prototype-member ids, the whole-scan budget over 11 batches, ignoreValues surfaced, the `untrusted` field. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(design-md): edits follow a symlinked DESIGN.md, keep the BOM and the majority line ending, refuse an unclosed fence - `mark`, `convert --write`, and the design binary's extraction replaced a symlinked DESIGN.md (a docs-site layout) with a regular file and left the real target untouched; both writers resolve the link first. - A single stray CRLF flipped a whole LF file to CRLF: the editors now keep the majority ending. A UTF-8 BOM broke format detection and ended up mid-file after `mark`; it is recognized and kept at byte 0. - Re-running `mark` on a marked file deleted the blank line after the marker (`\s*$` matched across the newline); the marker regexes use `[ \t]*`. - Fences: readers follow markdown (an unclosed fence runs to EOF); the text-level editors refuse such a file with DesignMdEditRefused (DESIGN_MD_EDIT_REFUSED) instead of splicing the wrong section, and the design binary reports that and leaves the file alone. - needsQuotes also quotes a scalar containing ` #` (an inline-comment shape parsed back as a truncated value). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(design): dump hygiene covers CSS URLs, srcdoc, and handlers; dumps persist owner-only; ignore prose treats repo config as evidence - The dump script cuts query strings from CSS url() in style attributes, <style> nodes, and the inlined stylesheets (signed asset URLs), empties srcdoc, and covers background and xlink:href. - Persisted dumps are chmod 600; MEDIUM redaction findings persist (an authenticated page shows emails) and the prose says so; earlier runs' dumps are swept before the first dump of a run unless --keep-dom. - The Aside dump prose asks for `'` in a pasted URL to be percent-encoded (a bare single quote would end the script) and never to paste an unread URL. - Repo-config ignores are evidence, not settled decisions, in /review, /ship, and design-review's probe prose; the scan JSON's text fields are named as untrusted. - design-html's skeleton ceiling is re-measured (54,545); ship goldens refreshed for the checklist prose. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(design-detect): audit directories scan as dumps; scans print probe lines on stderr; refused base always exits 1; PATH loses project entries Adversarial review (Claude subagent): - A DIRECTORY target under designs/ (the audit dir, which the prose hands the agent as REPORT_DIR) classified as an artifact, so the engine walked its dom/ subtree WITH inline ignores honored. Any directory under designs/ is now scanned as dumps. - A scan whose probe no longer finds an engine wrote its sentinel lines to stdout and exited 0, so `scan > "$_DJ"` captured "IMPECCABLE_NOT_AVAILABLE" as the scan result and the rendered bash read a clean scan. Probe lines go to stderr on every path; stdout is the JSON document or nothing. - A refused --changed base exited 0/2 when explicit targets were also given; it folds into the exit code (1 over 2 over 0). A trailing --changed no longer defaults to main. - A hand-edited `design_detector: Off` re-enabled the detector; the value is compared case-insensitively. - The engine inherited PATH entries inside the project (a direnv .envrc adding node_modules/.bin); those are filtered like every other project path. - DOM_DUMP_MISSING names the case where the dump script wrote nothing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(design-md): markdown edge cases: rule-opened legacy files, spaced fences, ~~~ blocks, duplicate headings, YAML 1.2 numerics - insertMarker keyed on "starts with ---", so a legacy file opening with a horizontal rule got a `# gstack:` line rendered as a heading that the parser then never read back (the conversion question re-asked every run). It keys on parsed front matter. - A closing front-matter fence with trailing spaces (`--- `) made a valid spec file `unknown`; the closer is any whole `---` line. - `~~~` fences hid nothing, so a `## ` inside one was a section boundary and a splice corrupted the fence; both fence kinds are tracked and only the same kind closes an opener. - convertLegacy silently kept the first of two `## Layout` bodies (and one of `## Color` / `## Colors`); it refuses with DESIGN_MD_CONVERT_REFUSED and the bin leaves the file and writes no backup. - needsQuotes covers 0x / 0o / .inf / .nan (YAML 1.2 numerics that changed type on round-trip); emitYamlBlock throws on an object inside an array instead of writing "[object Object]". - The design binary coerces the model's extraction JSON at the parse boundary (null names, missing arrays) so the paid call's result survives. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(design): print and alternate stylesheets are not scanned as page CSS; no cross-run dump sweep; probe-state and design-system caveats in prose - The dump inlined every linked sheet's rules as active CSS, so a print sheet's 12pt black text or an alternate theme produced tiny-text and palette findings the user never sees; disabled and alternate sheets are skipped and a media-scoped sheet is wrapped in its @media block. - The cross-run dump sweep is gone: two same-day reviews shared REPORT_DIR and one run's sweep deleted the other's dumps mid-audit. Dumps stay per run, owner-only, deleted after Phase 9 unless --keep-dom (now defined in the prose), and an interrupted run's dumps wait for the user. - Prose: design-system-* rows in DOM mode compare the page to THIS repo's DESIGN.md and apply only to the repo's own app; an empty scan JSON with exit 0 means the probe state changed since Setup (read stderr); the persist block names a missing dump instead of mislabeling it as a redaction block. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * v1.82.0.0: impeccable interop, detector pre-pass, open DESIGN.md format Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: update project documentation for v1.82.0.0 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * changelog: name the measure behind the test-count row Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(test): drive the DOM hygiene test through Playwright Chromium directly Under the six-shard CI free suite the test's private browse daemon never answered its health probe (two minutes of retries), failed the shard, and starved two unrelated test files into failing before the runner's timeout. The test now launches the same Chromium through playwright-core and calls the dump function with page.evaluate, the way Aside's pg.evaluate does: no state file, no daemon, no health window. It self-skips when the Playwright Chromium bundle is absent. Two more hygiene rules are pinned along the way (print sheets keep their @media, alternate sheets are dropped). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(test): compare gen-skill-docs paths with forward slashes on Windows gen-skill-docs prints repo-relative paths with the OS separator, so the checklist render pins (`GENERATED: review/design-checklist.md`) failed on the Windows lane against `review\design-checklist.md`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(test): assemble the planted PEM block at runtime The quality gate scans every added line of the PR diff through gstack-redact; the redaction test's literal PEM header was a HIGH finding on our own test file. The block is now built from fragments, so the scanned file never carries a key-shaped line while the test still plants a HIGH finding. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(design-detect): consent-gated engine install, checksum-pinned and receipted `gstack-design-detect.ts install` is the one download gstack makes, and only after a design skill's one-time question got a yes. It fetches the engine version gstack has tested (0.1.3) for this platform from impeccable's own GitHub release, verifies it against the checksum pinned in lib/design-detect-contract.ts (all five platforms, captured from the release's .sha256 sidecars; linux-x64 equals the fixture engine), writes an egress receipt before the fetch and refuses to download when the receipt cannot be written (fail-closed; the sink is registered in the wiring test's polarity table), caps the download at 32 MB, streams with the cap enforced, writes the file only after the hash matches, and places it under ~/.impeccable/bin/<version>/ (a trusted IMPECCABLE_HOME is honored; never inside a project). No skill, no hook, no launcher, no npx. --sha256 accepts a sidecar checksum for a version gstack has not pinned; --base allows a mirror (https, or http on loopback for tests). After a successful install the probe runs and its lines follow, so the skill sees READY at once. The probe ends with DESIGN_DETECTOR_INSTALL_OFFER (version, platform, bytes, destination) whenever it found no engine and the user has not answered the question; once design_detector_install_prompted is true it prints neither the offer nor the NOT_CACHED hint, which used to repeat on every run. The hint's npx wording is corrected: `npx impeccable detect --help` caches the engine for npx only, not where the probe looks. gstack-config gains design_detector_install_prompted (true|false, typo rejected, enumerated in list and defaults). Tests: a loopback mirror (async spawn, so the in-process server can answer) covers install, re-install as a verified no-op, checksum mismatch, 404, unpinned version, non-https base, design_detector off, and IMPECCABLE_HOME inside the repo; the offer and the silenced hint; pin completeness per platform. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(design): ask once before downloading impeccable's engine When the probe prints DESIGN_DETECTOR_INSTALL_OFFER the design skills ask the user one AskUserQuestion, in interactive sessions only (spawned or headless runs never install and never ask; Conductor gets the prose brief), before any other step: install the engine now, not now, never ask again (design_detector_install_prompted), or turn the detector off. A yes runs the receipted, checksum-pinned install and the skill continues with a READY probe. The brief says what impeccable is, what the one file is, where it goes, how it is verified and logged, and that no skill or hook comes with it; users who want the /impeccable skill run npx impeccable install themselves. design-review carries the brief inline (it is not carved). design-html keeps its skeleton small: the probe block points at a new read-on-demand section, sections/detector-install-offer.md, registered in its manifest and carve guard; its skeleton ceiling is re-measured (55,262) and its eager ceiling set to the measured 13,767. The review and ship passes state that they never offer an install. NOTICE.md, README, docs/skills.md, the interop design doc, and the CHANGELOG describe the new posture: gstack still never runs impeccable's installer or launcher; the one download is consented, pinned, and receipted. Ship goldens refreshed for the review-pass wording. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…2835) * fix: default cross-model workflows to frontier models * chore: bump version and changelog (v1.82.1.0) Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix: repair frontier eval budgets and workflow instructions Preserve frontier models and quality thresholds while fixing truncated judge output, ordered section expansion, consent checks, QA scoring, and ship audit gates. Add regression coverage and refresh generated docs. Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix: resolve workflow gaps exposed by frontier evals Clarify plan-review ordering and fallback modes, preserve deploy readiness gates, honor configured merge methods, correct benchmark and canary contracts, and restore vendored installs on setup failure. Cover recovery with real-shell regressions. Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix: use agent capture budgets for deploy evals Multi-turn deploy and benchmark sessions were incorrectly limited to the single-call judge timeout. Use the existing capture tier and leave outer-test cleanup headroom, with a free policy regression test. Keep all behavioral assertions and frontier models unchanged. Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix: clarify retro workflow and evaluate compare instructions Include compare mode in the frontier judge excerpt, define metric sources and snapshot ordering, and preserve the existing prompt-size budget. Co-authored-by: OpenAI Codex <noreply@openai.com> * fix: make documentation release review and publication consistent Review before commit, clarify changelog safeguards and unavailable reviewer modes, and preserve raw PR bodies across separate shell calls. Keep title sync in one shell and add regression coverage. Co-authored-by: OpenAI Codex <noreply@openai.com> --------- Co-authored-by: OpenAI Codex <noreply@openai.com>
Upstream reclaimed these in the merge; re-applied on top: - README install command + gstack-upgrade/SKILL.md.tmpl clone → github.com/seamfix/gstack (generated SKILL.md regenerated) - .github/workflows/*.yml runs-on → ubuntu-latest (this org has no Ubicloud runners; upstream's labels queue forever) Survived the auto-merge, verified: bin/gstack-team-init URLs, bin/gstack-update-check origin derivation, CONTRIBUTING/tutorial clone commands, the fork-local tripwire test (29 pass). NOT changed here (flagged for Platform review): scripts/sandbox-doctor.sh still present; GSTACK_SKIP_ASIDE still opt-out — both are decisions, not mechanical re-applies. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…fore apt/playwright installs free-tests and quality died at apt-get (Chromium/ShellCheck install) on 'Hash Sum mismatch' from the runner's preinstalled google-chrome.list — a known ubuntu-latest flake, exposed by the Ubicloud→ubuntu-latest switch. No test ran before the failure. Remove the unused source before apt operations. Add to the fork-local re-apply list. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…uessed filename (my last fix targeted google-chrome.list; the runner uses a different name/format) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ctive (SC1126) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…h; Aside off by default org-wide - scripts/sandbox-doctor.sh removed (dangerous dev-only: sudo, edits shell rc + global git config; we run CI on GitHub runners, not Vercel sandboxes). Its test and the CONTRIBUTING/TESTING_INTERNALS/PROJECT_STRUCTURE references removed too; CHANGELOG history kept. - gstack-team-init now writes env.GSTACK_SKIP_ASIDE=1 into the repo's .claude/settings.json in BOTH optional and required modes, so gstack never drives an engineer's real signed-in browser (or Aside's cloud agent) in a seamfix repo unless the repo opts in (set it to 0). All three runtime gates already honor the var; we only set it. Test added. Both are fork-local — re-apply on every upstream sync (recorded in the review + runbook). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
CodeQL (now enabled on the fork) surfaced 5 findings, all in upstream code this sync brought in, none in the fork-local changes: insecure-temp-file in |
|
Per-finding verdict on the CodeQL comments (Tobi flagged these). I read the actual code at each line. None is an exploitable vulnerability, and none was introduced by this sync — they're upstream code the diff happens to touch (CodeQL only comments on alerts inside a PR's changed files, which is why gstack#2's three-line diff drew none and this one does). The fork's
Recommended: dismiss 1, 2, 4 as false-positive/test in the security tab (2 is worth a note — CodeQL flagged the hardening code); 3 can be dismissed as low or left for the design-detector skim the security review already asked for. Not fixing upstream code in the fork (divergence + re-apply cost). Separately: the 100+ untriaged alerts on |
Brings the fork current so Platform owns an up-to-date baseline (was 16 releases behind: 1.68.3.0 → 1.84.1.0). Requested by Chibuzor, who is on 1.84.1 locally and wants the fork to match before Platform takes over the sync cadence.
Mechanical (verified, not asserted)
upstream/mainwith-X theirs(upstream wins conflicts), then fork-local patches re-applied on top. No conflict markers remain.README.mdinstall command +gstack-upgrade/SKILL.md.tmplclone →github.com/seamfix/gstack; generatedSKILL.mdregenerated (bun run gen:skill-docs --host all)..github/workflows/*.ymlruns-on→ubuntu-latest(this org has no Ubicloud runners; upstream's labels queue forever).bin/gstack-team-initURLs,bin/gstack-update-checkorigin-derivation (tested https/ssh forms →seamfix/gstack),CONTRIBUTING/tutorial clone commands, the fork-local tripwire test.bun test ./test/team-mode.test.ts ./test/gstack-team-init-hook-schema.test.ts→ 29 pass, 0 fail. Fork CI runs on this PR.Security review of the new delta (v1.81.0.0 → v1.84.1.0)
Verdict: MERGE WITH CAVEATS. Single-model read (Codex usage-limited to 2026-09-14; a second Claude model rate-limited) — so a Platform engineer should skim the ~600 unread lines of
bin/gstack-design-detect.tsbefore approving. Full record:ai-governance/docs/gstack-sync-reviews/2026-09-09-v1.81.0-to-v1.84.1.md(to be filed on merge).Two new opt-in features, both off by default and fail-closed:
memorableservice. Registered ONLY bygstack-memorable enable, never by setup/migration/prose. Policy ask: forbid enabling it org-wide (identity/compliance); optionally mark sensitive reposdenyin the gbrain trust store.design_detector offas a team default.Security-digest policy items — now applied
scripts/sandbox-doctor.shdeleted — dangerous dev-only script (sudo, edits shell rc + global git config); we run CI on GitHub runners, not Vercel sandboxes. Removed with its test and the CONTRIBUTING/TESTING_INTERNALS/PROJECT_STRUCTURE references.gstack-team-initnow writesenv.GSTACK_SKIP_ASIDE=1into the repo's.claude/settings.jsonin both optional and required modes, so gstack never drives an engineer's real signed-in browser (or Aside's cloud agent) in a seamfix repo unless the repo opts in (set it to0). All three runtime gates already honor the var; we only set it. Test added (30 pass).Both are fork-local changes to re-apply on every upstream sync (recorded in the review + runbook).
Branch protection on
mainhere is already in place (org rulesets: PR-only, 2 approvals, signed commits) — this correction is in the review record.🤖 Generated with Claude Code