* docs(designs): add fix wave 2026-10-15 plan (revision 3, approved) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(cso): invoke docker as argv0 docker so OrbStack's multicall binary dispatches executable() returns the canonical path, and OrbStack installs docker as a symlink to docker-tools, which dispatches on argv[0] and exits 127 with 'unsupported argv0 "docker-tools"'. Every Docker call failed, so doctor reported local-docker-isolation and every scanner missing and setup could not preload the qualified images. runProcess takes an argv0 option; docker.ts routes every Docker call through runDocker, which passes 'docker'. The native watchdog's execve calls pass "docker" as argv[0] too, so its container cleanup works on the same installs. The executed file is still the verified canonical path. Fixes #3187 * test(cso): cover Docker and watchdog invocation through a multicall binary A compiled fake that refuses any argv[0] but docker stands in for OrbStack's docker-tools. Both behavior tests fail on the previous code. Refs #3187 * fix(cso): bound the argv0 test's socket helper and probe images through OrbStack's multicall docker (#3187) Follow-up to #3188 by Bryan Stone (@aegixx): - the python3 spawnSync that binds the fake daemon socket gets a 30 s timeout; without it test/spawnsync-timeout-tripwire.test.ts fails. - reviewer test: dockerExactImagePresent, a production probe, admits an image when the endpoint's canonical executable is a multicall binary that refuses any argv0 other than "docker". It fails on47d639a(returns false) and passes with #3188. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(autoplan): own the conversation after a Claude Code 2.1.300 compaction Claude Code 2.1.300 keeps a compaction's last records in place and lists them in compactMetadata.preservedMessages. The segment's tail, which is also the boundary's logicalParentUuid, is written after the boundary under the compact summary, so the parent links form a cycle that the owned causal order never reaches. Every record after the compaction was dropped, and the publication guard denied each later phase entry with close_incomplete. Such a boundary now continues from the last preserved record that is a node of the file and not its own descendant. Boundaries without that metadata, with a foreign preserved uuid, or with nothing kept in place still supply no ownership. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(autoplan): report a compaction-explained unreachable component as unrecognized_shape (#3194) Follow-up to #3196 by vilano93. After the ownership walk, an owned node the walk never reached was silently dropped, and the phase guard answered the next phase entry with close_incomplete, which the user cannot satisfy. When the unreachable node's ancestry ends in a cycle through a compact boundary's logical parent, ownedCausalOrder now returns unrecognized_shape:compaction_unreachable, the guard's existing unverified disposition (allow with a visible warning and a guard-log row). Orphans whose ancestor is missing (not flushed yet) and other roots keep today's behaviour, so the fail-open surface does not widen beyond compaction. Each node is walked once. Gate-integrity edit: a close_incomplete denial becomes an unverified allow for that shape only. Negative controls kept: the existing guard, hook, guard-reasons and captured-fixture tests still deny (356 pass), and a new case with the boundary not yet flushed still denies close_incomplete. Intentional pin changes in #3196's own test file: the three "fails closed" cases (tail-only segment, missing preserved metadata, foreign preserved uuid) and the "without compactMetadata" case now expect unrecognized_shape:compaction_unreachable instead of close_incomplete or a silently cut transcript. Reviewer tests: three compactions in one session stay owned and pass the next phase entry; a 50k-record history with three compactions is decided in ~300 ms (budget 1000 ms), so descends needs no memo. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(lib): one echo sanitizer for external strings, with a Bash twin lib/sanitize-echo.ts and bin/gstack-sanitize-echo.sh flatten CR/LF/TAB to spaces, remove every other ASCII control byte (ESC included) and DEL, collapse runs of spaces, and cap the result at 200 bytes of valid UTF-8 with no leading or trailing space. Every external string gstack echoes into a message, a log line or a key=value state file goes through it (fix wave 2026-10-15, plan rev 3 DX F8). test/sanitize-echo.test.ts runs both twins on one table under bash, bash --posix and zsh, including a "\nstages=" injection, ESC sequences and UTF-8 characters cut by the cap. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(bun-version): judge the Bun floor under zsh and hold on an unreadable version (#3195) Reported by mochafreddo. _gstack_bun_below word-split $_gb_core, which zsh does not do: `[ -ne ]` errored and every error path read as "not below", so /gstack-upgrade Step 4 accepted Bun 1.3.2 in a zsh tool shell. - _gstack_bun_below copies the release's parsed fields into quoted positional parameters and compares with $((10#...)); no new _gb_* globals. It returns 2 when either side cannot be parsed. - gstack_bun_status prints malformed for an unparseable version or floor (never ok); gstack_bun_incoming_hold holds on it with bun-version-unreadable: ... (BUN_VERSION_UNREADABLE), the bun path and its output passed through gstack_sanitize_echo. setup's own malformed warning is unchanged. - /gstack-upgrade Step 4 runs the check through `bash -c` and stops on BUN_VERSION_UNREADABLE with a reinstall fix (never "bun upgrade"). - New result code BUN_VERSION_UNREADABLE with its troubleshooting entry; bin/gstack-doctor-components.sh regenerated from the table. test/bun-version-shells.test.ts: one version table (1.3.2, 1.3.3-canary.1, 1.3.3, 1.3.14, 1.4.2, 1.10.0, 01.04.02, 2.0.0, garbage) drives bash, bash --posix, zsh and the TypeScript twin's below(); `zsh -c '. bin/gstack-bun-version.sh; gstack_bun_status 1.3.2'` printed ok on47d639aand prints too-old now. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * style(cso): format #3188's runDocker call so format:cso:check passes (#3187) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(cso): keep scanner JSON reports parseable after redaction (#3185) Reported in #3185. The scanner path splices redactions into raw JSON, and redactFindingSpans replaced a bare 10+ digit number ("max_memory_bytes":1145427776, an epoch) with an unquoted marker, so JSON.parse failed and every non-trivial Semgrep run was INVALID_OUTPUT. - lib/redact-engine.ts: redactJsonFindingSpans, a string- and escape-aware lexical tokenizer over one or more top-level values (NDJSON included). A span inside a number replaces that whole token (-, fraction, exponent) with the quoted marker, number by number; a span inside a string keeps the in-string marker, widened to whole escapes; a span touching several tokens, whitespace, a literal or a structural character returns null (withhold). Non-JSON text gets the plain splice. redactFindingSpans is unchanged for every other caller. - The scan operation's declared format rides the real capture path: scanner-executor (scan stdout, or the report file read for outputPath plans) -> DockerGroup.execCapture -> runProcess -> spliceRedaction, which redacts the concatenated stdout once and maps each interleaved run onto it. Version probes and stderr stay plain; marker-only withholding and the cross-stream views check are kept, and the output is a fixpoint of that check. Behavior change: a redacted number in a scanner report is now the string "<REDACTED-...>" instead of an unquoted marker; non-JSON streams keep the unquoted form (test/cso-eval.test.ts unchanged). Intentional pin change: test/cso-scanner-release.test.ts pinned the report read as capture(['/bin/cat', plan.outputPath]); it now pins capture(['/bin/cat', plan.outputPath], plan.format), so the read also carries the declared JSON format (stricter, not weaker). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(browse): recover when the last tab is closed outside the CLI Closing the last tab outside browse (for example, closing the headed Chromium window) left the daemon wedged: the page 'close' handler removed the tab and set activeTabId to 0, and executeCommand called getActiveSession() first, so every later command (goto, status, even stop) failed with "No active page" until the daemon PID was killed by hand. closeTab() already opens a blank tab when the last tab closes; the external close path never did. Add BrowserManager.ensureActiveTab(), which reselects a remaining tab or opens one blank tab (concurrent callers share one recovery), and call it before resolving the active session in executeCommand. Fixes #3167 * fix(browse): status and stop answer after an external close; recovery says what it did (#3167) Follow-up to #3179 by aftabkh4n, which reopens a tab on demand. - BrowserManager.ensureCommandTab runs before every command: status, stop and restart never open a tab, so a daemon whose browser or context is gone can still report and be stopped; any other command gets an active tab. A failed recovery is BROWSE_TAB_UNRECOVERABLE carrying the restart command (cause passed through sanitizeEcho), and a command that ran on a newly opened tab prints a one-line warning first. - server.ts reads the session inside the read and write branches instead of before dispatch (2223 lines, cap 2224) and exposes browserManager and handleCommandInternal on __testInternals__ for the dispatch test. - meta-commands.ts: handleMetaCommand no longer reads the active session before its switch (only snapshot and resume used it), and status prints getCurrentUrl() so it works with zero tabs. This file is outside the lane's listed ownership; no other lane or #3190 touches browse/. - New result code BROWSE_TAB_UNRECOVERABLE and troubleshooting entries for it and for the old "No active page" wedge. Reviewer test browse/test/tab-external-close-dispatch.test.ts drives the real dispatch in a child process: status, text, goto and stop after an external close, and the closed-context case. On47d639a(with only the test seam added) all five fail with "No active page". Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(troubleshooting): /cso INVALID_OUTPUT on Semgrep reports and OrbStack's argv0 refusal (#3185, #3187) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(session-update): name the reinstall fix when an unreadable Bun holds the auto-update (#3195) Item 11 makes every caller of gstack_bun_incoming_hold treat a Bun version it cannot read as a hold (bun-version-unreadable, BUN_VERSION_UNREADABLE). The session-start banner only knew bun-too-old, so the new reason fell through to "git pull did not finish ... gstack is not updating" with a pull-and-setup fix that does not help. - bin/gstack-session-update: a bun-version-unreadable banner case with the reinstall fix and the troubleshooting anchor; the comment that said a malformed version passes is corrected. Intentional pin change: test/session-update-stages.test.ts "supported or unparseable Bun (bun-dev (local build)) proceeds: setup decides" is split. 1.3.14 still proceeds; bun-dev (local build) now holds with BUN_VERSION_UNREADABLE, leaves the checkout and setup untouched, and the banner names the reinstall fix. The fixture installs bin/gstack-sanitize-echo.sh beside the floor helper, as a real install of this release does. The new case fails on47d639a. Ownership exception granted by the coordinator for Lane A (bin/gstack-session-update is Lane B's file; PR 1b stacks on 1a). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(session-update): recover a conflicting autostash by its unmerged files The auto-updater fast-forwards with `merge --ff-only --autostash`. When the carried-over local edits conflict with the incoming revision, git 2.56 still exits 0 from that merge, pops the edits with conflict markers into the live checkout, and keeps the stash entry. The recovery only ran on a non-zero exit with "autostash" in stderr, so on git 2.56 it never ran: the updater logged UPDATED and ran setup over a half-merged tree, in the directory Claude Code runs hooks from. When it did run, it had two more problems: - `git checkout -- .` refuses unmerged paths ("error: path 'f' is unmerged", exit 1), so it left the markers in place anyway. - `git stash drop` dropped stash@{0}, whatever that was. refs/stash is shared with every worktree of the repository, and when refs/stash.lock is held git cannot store the autostash at all, so the top entry can be someone else's. Now the merge is judged by `git ls-files -u`, whatever its exit code. On unmerged files after the fast-forward, autostash_recover finds the autostash commit by the id git prints ("cannot store <id>" first, then "Created autostash: <id>", with the merge run under LC_ALL=C and core.abbrev=40). A moved refs/stash is only the fallback. When that id is not a stash entry, it is anchored under refs/gstack-autostash/<UTC>. The tree is reset to the fast-forwarded revision, which the hook check already passed, and the stash is kept. The log line is AUTOSTASH_CONFLICT_RECOVERED tree_reset=1 kept_stash=<id> kept_ref=<ref>. The update then continues, so the regenerable patches are re-rendered and setup runs as for any other move. If the commit cannot be found, nothing is reset: the markers stay for a person, the run logs PULL_FAILED, and setup does not run. Files that are already unmerged before the update, from someone's own merge, rebase or stash pop, now hold the update with PULL_FAILED "unmerged files ...". Without that check, the recovery would take them for its own and reset them away. core.abbrev=40, not "no": "no" is accepted from git 2.31.0 (its release notes), and `merge --autostash` exists from 2.27.0, so "no" would fail every update on git 2.27-2.30. Files: - bin/gstack-session-update: autostash_recover, the unmerged-files check before the merge, the C-locale merge with its stdout kept for the id, and removal of the old grep-and-drop block. - test/session-update-autostash.test.ts: 7 cases in a new describe block. A conflicting pop leaves no markers, keeps the edits and still runs setup. A user's own stash entry survives. A held refs/stash.lock gets the edits anchored. With a git whose merge prints nothing, a moved refs/stash still finds the edits; with the lock held as well, nothing is reset. Pre-existing unmerged files hold the checkout. A non-conflicting edit rides over the update (control). Tests, on upstream15ebd85cd: - 5 of the 7 new cases fail against the previous session-update; the unmerged-files guard and the control pass on both. - Two mutants are each caught by one new case: dropping the refs/stash fallback, and resetting when the edits cannot be found. - session-update-autostash, session-update-stages, session-update-hook-hold and team-mode pass 63/0 with `--timeout 60000`. At bun's 5 s default under heavy load (one reading: load average 169), the three shared files timed out 8 tests on this branch and 17 with the previous script. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(session-update): trust only git's autostash id; owe setup on a stop An independent review of the autostash recovery (Codex gpt-6-astra, high effort, verdict BLOCK high=1 med=2) found three defects on its rare paths. All three are fixed here, each with a case that failed first. - [high] The refs/stash fallback could erase local edits. When git's messages were unavailable, recovery took any change to refs/stash as its own autostash. refs/stash is shared with every worktree: if a sibling pushed a stash while refs/stash.lock kept git from storing this one, recovery took the sibling's entry and reset the checkout, erasing edits nothing held. Now only git's own printed id counts. A missing id resets nothing: the markers stay, the run logs PULL_FAILED. Recovery needs git's messages, so when the temp file for them cannot be created the update is held before the merge (HELD temp-file-unavailable), not merged blind. - [med] A stop after the checkout moved lost the setup it owed. The failure recorded only the pull reason. Once the person resolved the conflict, the next check saw an unchanged HEAD and no pending stage, logged UP_TO_DATE, and never set up the new revision. Now a failure after HEAD moved records `setup` as a pending stage, retried at once. The unmerged-files check holds it until the conflict is resolved, and the pull reason is the stage's reason, so the session-start line says what to resolve. - [med] An unreadable index read as "no conflicts". Both `git ls-files -u` checks discarded the exit status, so a failed read after a conflicting pop (which git exits 0 for) let setup run over the markers. Now a failed read stops the update with PULL_FAILED "cannot read the index", before the merge or after it. Files: - bin/gstack-session-update: autostash_recover drops the refs/stash fallback (and STASH_BEFORE). The temp-file hold. The ls-files exit status checked at both points. The pending setup stage on a failure after HEAD moved. - test/session-update-autostash.test.ts: a shim(fx, name, tool, body) helper; the fallback case now expects a refusal; the refusal case runs on through resolution to setup; three new cases (unreadable index before and after the merge, no temp file for git's messages). Tests, on upstream15ebd85cd: - The 5 changed or new cases failed on the previous commit, each on its defect: refs/stash trusted and recovered; UP_TO_DATE with no setup after resolution; UPDATED over an unreadable index, before and after the merge; a blind merge with no temp file. - Against upstream's own session-update, 8 of the block's 10 cases fail. - session-update-autostash, session-update-stages, session-update-hook-hold and team-mode pass 66/0 with `--timeout 90000`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(session-update): reap heartbeat and its sleeper on exit The lock heartbeat subshell left its sleep 300 child orphaned when the updater exited, so heartbeats accumulated across sessions. Track and reap the heartbeat job (with a job-table fallback for signals between fork and $!), give the subshell its own exit trap that kills and waits its sleeper, and map HUP/INT/TERM to exits so the EXIT trap always runs. Adds a fixture-driven regression test. * test(session-update): repin the lock trap and heartbeat to #3191's cleanup shape Intentional pin change: test/session-update-autostash.test.ts 'reclaim is TOCTOU-safe' and 'EXIT trap is ownership-checked' pinned the one-line EXIT trap and the 'while :; do sleep 300; ...' loop that #3191 (Jess Sullivan) replaces with cleanup_session_update and a 'sleep 300 & wait' heartbeat. The pins still assert the ownership-checked rm -rf, the EXIT trap, MYPID, and that the heartbeat is killed. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(session-update): move installs only through gstack-ff-update; extract hooks without tar (#3184) Item 2 (#3189, #3191 adopted in the previous commits) and item 3 of the fix-wave plan. bin/gstack-ff-update <dir> <rev> is now the only code that moves an install's tree. Inside an install-scoped lock (absolute git common dir, the updater's TTL and mv-aside reap, a heartbeat reaped on exit) it captures the working tree and the staged state through private indexes, carries edits that merge cleanly (as an autostash pop would), lets the regenerable SKILL.md/sections footprint take the target when it conflicts, and shelves any other conflicting edit, staged change or unmerged file into refs/gstack/rescue/<oid> (first parent the pre-update HEAD, second the staged state; compare-and-create; gstack rescue identity). The move is a two-way read-tree on the private index, so a late edit or an untracked file in the way refuses with autostash-unrecoverable and changes nothing. It sources nothing so the held-install recovery can fetch it from the new revision. gstack-session-update calls it instead of merge --autostash, holds a checkout with unmerged files (unmerged-files:), and names shelved edits at session start at most once a day with inspect, export-the-delta and delete commands. The incoming revision is extracted for the hook check through a private index and checkout-index with core.symlinks=false, never tar (MSYS tar cannot create connect-chrome before its target), and the held reason carries the failing command's flattened, control-free stderr. Held lines print before the stage line, one case per reason with its anchor, every path shell-quoted, no setup fix while unmerged files exist. docs/troubleshooting.md gains the held-install recovery forms (Git Bash, PowerShell 5.1, trial and go-back) and entries for each new line. design: kept_ref is refs/gstack/rescue/<oid>, refs/stash is never touched, the quiet-merge cases become a pre-existing-ref refusal and its recovery, pre-existing unmerged files are a HELD unmerged-files, and the git shim pins the real binary through git --exec-path. Intentional pin changes in existing tests: - test/session-update-stages.test.ts: printed paths are shell-quoted; the bun-too-old fix runs gstack-ff-update instead of git pull. - test/session-update-hook-hold.test.ts: the order pin looks for 'bash "$FF_UPDATE"' instead of 'merge --ff-only'; the hook-check-unavailable fix no longer recommends git pull --autostash; installedUpdater copies gstack-ff-update beside the updater. Reported by wengweiling (#3184). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(gstack-upgrade): Step 4 fast-forwards through gstack-ff-update (#3189) Item 2 of the fix-wave plan, kept in its own commit so PR 1b can be rebased onto PR 1a (which edits only Step 4's Bun floor-check line). Step 4's `git pull --ff-only --autostash` had the same hole the auto-updater had: a conflicting autostash exits 0 and leaves conflict markers in the install, and the skill printed FF_OK. It now runs bin/gstack-ff-update . origin/main after the fetch and the floor check: exit 0 runs setup (a rescued line is relayed with its inspect, export and delete commands), exit 4 (not a fast-forward) is the only way into the gated reset fallback, and any other refusal stops with nothing changed. The footprint discard moves to the reset fallback, ahead of its stash, where the warning text already promised it. Intentional pin changes: - test/upgrade-template-pins.test.ts: the ff-before-reset pin looks for 'bash bin/gstack-ff-update . origin/main'. - test/gstack-upgrade-fresh-shell.test.ts: the C9 fence marker is 'bash bin/gstack-ff-update'; the gstack-like fixture ships the bin. - test/upgrade-setup-recovery.test.ts: the git-path fixtures ship gstack-ff-update (a stub where git itself is a stub). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(setup.ps1): run under Windows PowerShell 5.1's legacy argument passing (#3130) Item 4 of the fix-wave plan. Windows PowerShell 5.1 strips the double quotes inside native arguments, so the bash -c script setup.ps1 handed Git Bash arrived broken ("unexpected EOF while looking for matching"). The script now holds no double quote: `set -f; IFS=; mapfile -d '' -t argv < $1; rm -f -- $1; exec ./setup ${argv[@]}` (empty IFS and no globbing keep $1 and each argv entry whole). That form drops an empty argument, so setup.ps1 refuses one before anything runs (SETUP_PS1_EMPTY_ARGUMENT, with its troubleshooting entry). test/setup-ps1.test.ts runs the adversarial arguments under legacy passing (pwsh with $PSNativeCommandArgumentPassing = 'Legacy', and powershell.exe itself on Windows) with a space in TMPDIR, plus the empty-argument refusal. Intentional pin change: the pinned bash -c line in "passes arguments NUL-separated through a private file to a constant bash script" now names the quote-free script and asserts it holds no double quote. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(setup): register hooks at the $HOME path when CLAUDE_CONFIG_DIR links back to it A per-session CLAUDE_CONFIG_DIR whose skills symlink to ~/.claude/skills passed the split-brain guard, so its temporary path was written into the global settings.json and the hooks broke once the session directory was removed. Fall back to $HOME/.claude/skills/gstack whenever both spellings resolve to the same tree. Fixes #3180 * fix(setup): one sourced helper picks the hook root for setup and gstack-memorable (#3180) bin/gstack-memorable carried a verbatim copy of setup's old canonical-root block, so `gstack-memorable enable` under a per-session CLAUDE_CONFIG_DIR still baked the session path into settings.json after #3181 fixed setup. The choice now lives in bin/gstack-claude-root.sh (gstack_canonical_claude_root), sourced by both. Intentional test changes: - test/setup-hook-canonical-paths.test.ts: #3181's source-slicing block test is rewritten to source the helper (with a spawnSync timeout, which the sliced version lacked and the tripwire flags); the "honors CLAUDE_CONFIG_DIR" pin moves from setup to the helper, and a new pin requires both callers to source it. Adds a no-$HOME-install case. - test/setup-hook-syntax-gate.test.ts: the hook-section slice starts at the new assignment and the harness sources the helper. - test/setup-claude-code-migration.test.ts, test/team-mode.test.ts: the partial-checkout fixtures copy the new helper setup sources. Reviewer test: gstack-memorable enable under a per-session config dir that links back to ~/.claude registers the $HOME path (fails on main). Builds on #3181 by NetrioGit. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(setup): remove 1.31-era unprefixed Cursor skill folders (#3133) A 1.31 setup run from a checkout under ~/.cursor/skills linked unprefixed <skill>/ folders there, each holding a SKILL.md symlink into the checkout. Current setup links gstack-<skill>/ folders, and the retired-skill prune only looks at gstack-* names, so the old folders stayed: Cursor loaded autoplan, review and the rest twice, or kept dangling links once the checkout moved. Run the Claude flat-name cleanup on ~/.cursor/skills as well. Cursor installs only gstack-* names, so no prefix gate is needed, and the helper removes only entries proven to link into gstack (#2119), so user-owned folders stay. It runs after linking, so a run that fails before the new links exist keeps the old folders. Reported by @andrewkjankowski in #3084. Fixes #3133 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(setup): name the skills dir in the flat-name cleanup line (#3133) `./setup --host cursor` now runs the flat-name cleanup on ~/.cursor/skills as well as ~/.claude/skills, so the "cleaned up old entries" line says which directory the folders were removed from. Reviewer test for #3152 (szsunyuan): a real dir whose SKILL.md links into gstack loses only that link and keeps the user's file; SKILL.md links into a sibling repo named gstack-fork, a relative link, and a dangling link outside any gstack checkout are all kept; the printed line names the removed folders and the Cursor skills dir. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(setup): install Chromium with `bun x`, and name exit 127 for what it is (#3131) A Bun installed with winget ships no `bunx`, so setup's `bunx playwright install chromium` exited 127 and was reported as "offline, proxy, or blocked download?". setup now runs `bun_cmd x playwright install chromium` through its existing bun resolver ($BUN_CMD), and bin/gstack-browser-ensure runs the bun it found on PATH with `x`. Exit 127 gets its own text naming the resolved bun, Bun from bun.sh, and the skip commands for Git Bash and PowerShell; other codes keep the network hint. New troubleshooting entry: #chromium-install-exit-127. Intentional test changes: - test/setup-playwright-best-effort.test.ts: the installer stub is now a `bun_cmd()` stub (was `bunx()`), its mark renamed bun-x-called and now records the args; the block under test calls bun_cmd. New winget-shaped cases (bun on PATH, no bunx anywhere) run setup's real bun_cmd. - test/gstack-browser-ensure.test.ts: the fixture is winget-shaped (the `bun` stub handles `x`, no `bunx` on PATH); the exit-7 pin now reads `<stub>/bun x playwright install chromium exited 7 (...)`. --help in gstack-browser-ensure prints the comment header only (3,24p). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * ci(windows): prove setup.ps1 on PowerShell 5.1 and the held-install recovery natively (#3130, #3184) windows-setup job: - setup.ps1 joins the path filter (with bin/gstack-ff-update, bin/gstack-claude-root.sh, bin/gstack-browser-ensure, docs/troubleshooting.md and test/setup-ps1.test.ts). - powershell.exe must be Windows PowerShell 5; test/setup-ps1.test.ts runs with a space in TEMP and must report exactly 4 pass, 0 fail, 1 skip (the Linux-only missing-prerequisite case). - A copy of the built checkout under a path with a space installs through `powershell.exe -File setup.ps1 --host codex`. held-install-journey job (new, parallel): - origin's main is this PR's tree on top of the v1.91.101.0 tree (47d639abf); fix-branch adds one file. - A prefix-mode v1.91.101.0 install at the runner's ~/.claude path in three states (clean, dirty-conflicting, already-unmerged), with auto_upgrade=true and the throttle cleared: the old updater must log `HELD hook-check-unavailable`; the PowerShell recovery form, read verbatim from docs/troubleshooting.md and run under powershell.exe, must reach main with every local edit (only those) in one rescue ref. The dirty state runs it with its .\setup.ps1; the others replace that step with a marker. - The trial and go-back forms move the install to fix-branch (detached) and back to main. - Only then a later release whose symlink sorts before its target is pushed, and the new updater must log UPDATED and reach it. - Both jobs assert the exact list of cases that ran. The journey steps ran locally on Linux with pwsh in Legacy argument mode as powershell.exe, a git exec-path whose ../../../bin/bash.exe is bash, and a failing tar standing in for MSYS tar (all 9 cases passed); the windows-latest run itself happens on the PR. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(ff-update): a killed step-4 run fails the status check instead of the type check Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * ci(windows): the held-install job runs without the bun cache step The cache key line is a secret-scanner false positive on added lines (env.kv); the fixture's own setup installs its packages either way. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(fixtures): register #3191's heartbeat fixture as free test/free-fixtures.test.ts failed on the frozen-branch Ubicloud run: test/fixtures/session-update-heartbeat.sh (adopted with #3191) was in no paid touchfile and not in FREE_FIXTURES. Its only consumer is test/session-update-heartbeat.test.ts. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(session-update): leave the bun-version-unreadable banner case to item 11 (PR 1a) PR 1a owns the bun-version-unreadable hold (BUN_VERSION_UNREADABLE), its banner case and its troubleshooting entry; this branch is rebased onto 1a. Drops the case, the docs entry and the banner test this branch had added. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(setup.ps1): resolve Git Bash in the shared order, with GSTACK_BASH as an alias and a BASH_UNAVAILABLE refusal Follows lib/script-invocation.ts (Lane C, PR 2): GSTACK_GIT_BASH is the override and GSTACK_BASH its deprecated alias at the same precedence; when both are set and differ GSTACK_GIT_BASH wins and one warning prints. An override that does not exist is refused, never skipped. Without one: the install git --exec-path belongs to (behind a scoop shim, or a portable Git), the folders around git.exe, %LOCALAPPDATA%\Programs\Git, %ProgramFiles%\Git, and last a bash on PATH whose uname -s reports MSYS; WSL's bash (System32, WindowsApps) is rejected. A miss lists every candidate with its rejection, then the PowerShell user-variable and ~/.claude/settings.json env recipes (BASH_UNAVAILABLE, #bash-unavailable). test/setup-ps1.test.ts gains a 14-row pwsh-on-Linux table with every location injected (override, alias, both set, missing override and alias, scoop shim, per-user, portable and machine-wide Git, MSYS PATH bash, WSL and non-MSYS rejections, missing); 11 rows fail on main. The rows mirror Lane C's table in test/script-invocation.test.ts; they are not wired to it, since that branch is not available here. The Windows job's expected skip count for this file moves from 1 to 15 (the table is Linux-only). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(setup.ps1): give the empty-argument refusal its result code (SETUP_PS1_EMPTY_ARGUMENT) Adds the row to lib/result-codes.ts (anchor #setup-ps1-empty-argument, which setup.ps1 already prints). bin/gstack-doctor-components.sh is regenerated from the table (generated; resolved by regeneration at merge). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * ci(windows): silence SC2016 on the PowerShell version probe (the $ is PowerShell's, not bash's) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * ci(windows-setup): give tar a POSIX path for the spaced checkout MSYS tar reads `D:\a\_temp/...` as host:path (rsh syntax) and fails with 'Cannot open: No such file or directory' before setup.ps1 runs. The copy target now goes through cygpath -u; setup.ps1 still gets the Windows form. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * ci(windows-setup): dereference symlinks when copying the spaced checkout MSYS tar cannot extract connect-chrome, a symlink that precedes its target (the #3184 failure mode), so the copy died before setup.ps1 ran. tar -h stores the target instead. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(windows): one Git Bash resolver for gstack's shebang bins (#3197) Windows has no shebangs, and gstack had three conflicting bash lookups: the hooks fell back to a bare PATH `bash` (often WSL's launcher), the gbrain sync checked three fixed paths, and setup.ps1 walked up from git.exe. lib/script-invocation.ts is now the only TypeScript resolver: GSTACK_GIT_BASH (GSTACK_BASH as a deprecated alias, same precedence, warning when they differ, a missing value refused), bash next to `git --exec-path` (scoop, per-user, portable Git), the known install paths, then a PATH bash only when `uname -s` reports MSYS/MINGW. WSL launchers are rejected unprobed. A miss is BASH_UNAVAILABLE, listing every candidate with its rejection reason. Bun-shebang bins run under Bun (process.execPath, or the PATH bun inside a compiled binary). Hooks' runBin resolves through a state-root cache keyed by git.exe path and mtime, written by temp file plus rename, so the hot path spawns nothing extra; the override is never cached. wtreeFingerprint({cwd, timeoutMs}) returns {tree} | {error, kind} with GIT_OPTIONAL_LOCKS=0 and 40-64 hex ids; the consumers move next. Intentional pin change: test/gbrain-spawn-windows-shell.test.ts expects forward-slashed bash paths (WIN_BASH and the GSTACK_BASH override), the form the shared resolver returns because Bun's spawnSync on Windows returns ENOENT for a backslash exe path containing spaces. The copied-tree adapters in test/timeline-stop-hook-slug.test.ts and test/plan-tune-cathedral.test.ts list the two modules spawn-bin.ts now imports; the new fixture is registered in test/helpers/free-fixtures-data.ts. bin/gstack-doctor-components.sh is regenerated for the new result-code row. Reported by @exGeni. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(windows): fingerprint and gstack bins through the shared invocation (#3197) bin/gstack-wtree was spawned by path in lib/regen.ts, lib/gate-diff/git.ts, lib/restamp.ts and bin/gstack-evidence; gstack-slug in restamp, gate-diff and evidence; gstack-redact in the pregate secrets check; and a bare `bash -c` in regen, the lane selection manifest and gstack-evidence run. On Windows each one failed to launch. All now go through lib/script-invocation.ts, and each fingerprint consumer keeps its fail direction while saying why: - gstack-regen check: incomplete, `cannot fingerprint the working tree (gstack-wtree exited 1: <helper's own repair>)` instead of `(gstack-wtree failed)`; write keeps the registry label as its run id. - gstack-gate-diff: `+nowtree` candidate, with the cause. - gstack-restamp: RESTAMP_CONFLICT carrying the launch or exit detail (Windows printed `()` before). - gstack-evidence: records without wtree (grades STALE) and warns once; SHA-256 repositories now get a 64-hex fingerprint. lib/regen.ts keeps a string-returning wtreeFingerprint(repoRoot) wrapper for lib/pregate/context.ts and lib/pregate/index.ts (files #3190 changes); item 10b migrates them. test/script-invocation-tripwire.test.ts fails on a new by-path or bare-bash spawn under lib/, hosts/ or a Bun bin; five sites outside this change are listed with their owners and may only shrink. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(pregate): keep framework route paths from a selection manifest (#3176) laneFileSet parsed manifest output with `[\w.-]+` path segments, so `app/(shop)/[id]/page.test.ts` came back as `page.test.ts`. The selected test then matched no platform lane, its remote receipt obligation vanished and it fell back to a local run. manifestTestPaths keeps route segments whole (`(group)`, `[id]`, `[...slug]`, `[[lang]]`, `@slot`, `$param`, `+page`), reads every path-shaped token on a line (JSON arrays, quoted, `path:line`, parenthesized), and decides what is a test file with the import graph's own isTestFile, so a test-file rule change there (bats, #3177) reaches manifests without a second regex. A manifest that cannot start is "unknown" (widens), like a non-zero exit. This is the manifest-parser half of #3176 by @vocodestudios; the import-graph half (tsconfig aliases, Next.js route mapping) ships in a separate PR. test/pregate-manifest-paths.test.ts drives the CLI with a bracketed route test in the fixture's Windows-only lane and asserts its requires-remote obligation; it failed on main (requires_remote was []). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * ci(windows): windows-pregate-e2e drives the #3197 paths natively A windows-latest job (Bun 1.4.2) builds a fixture repository and runs gstack-restamp --dry-run, gstack-regen check and write, gstack-evidence run and gstack-gate-diff from their TypeScript entry points, asserting a real tree fingerprint each time; times a question-log hook cold and warm and checks it cached its Git Bash resolution; and checks that a missing GSTACK_GIT_BASH refuses with BASH_UNAVAILABLE and lists the candidate. The journey runs twice: under the runner's Git for Windows and under a pinned PortableGit 2.56.0.2 (SHA-256 verified), found only through `git --exec-path`. gstack-pregate run joins it with item 10b. The workflow is registered in scripts/test-pr-profile.ts FREE_ONLY_PR_FILES (no paid case reads it), recorded as a non-behavioral HARNESS_VERSION decision in scripts/harness-version.json. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(setup.ps1): resolve Git Bash by the shared fixture table (#3197) test/setup-ps1.test.ts now drives setup.ps1 through every row of test/fixtures/windows-bash-resolution.json, the table that pins lib/script-invocation.ts, instead of a hand-mirrored copy. Two rows failed and are fixed here: - an override and alias that differ only in slash direction no longer print the differ warning; - the root git --exec-path names is also searched for usr\bin\bash.exe (Git for Windows on arm64). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(windows): isolate the no-bash case and launch pregate as the product does (#3197, #3176) windows-free-tests run 38813751242 failed two tests this layer added. Both were fixture defects on a real Windows host, not product defects: - gbrain-spawn-windows-shell "returns null when Windows has no bash" stubbed only `exists`. The resolver's PATH step asks the real Bun.which and found the runner's MSYS bash, which is correct. The case now also stubs `which` and `gitExecPath`, so it really means "no bash anywhere". - pregate-manifest-paths spawned the extensionless bin/gstack-pregate directly. CreateProcess cannot exec a shebang, so nothing ran and stdout was "". It now launches through lib/script-invocation's runScriptSync, gstack's own Windows invocation, so the case runs the manifest lane end to end on Windows instead of being excluded. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * ci(windows): the missing-bash step survives the refusal it expects (#3197) GitHub runs `shell: bash` as `bash -eo pipefail`, and the step's own `set -uo pipefail` does not turn -e off. So when gstack-restamp refused, as the step expects, `OUT=$(...)` aborted the script before `CODE=$?` and before any output was printed: exit 1 after 0.5 s with nothing in the log (PR #3209, job 116729693492). The capture is now `OUT=$(...) && CODE=0 || CODE=$?`. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(pregate): the manifest-lane case expects the host's own remote lanes (#3176) Once 8b2f297cc made the case run on Windows, windows-free-tests run 38824886195 (shard 4) showed the expectation itself was Linux-only. pregate owes a lane remotely when its runner platform is not the host's (lib/pregate/workflows.ts). On a Windows host the fixture's Windows lane runs locally, and the Ubuntu `free-tests.yml/unit` lane is the remote obligation; that is the record Windows produced. The case now derives the owed lanes from the host the same way. On Windows it also requires `ran: node --test 'app/(shop)/[id]/page.test.js'`: there the route reaches the local run only through the manifest's whole path. With a cut manifest path (page.test.js) the local set is empty, checked through buildContext({ platform: 'win32' }) + selectLanes. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * ci(windows-setup): expect 31 skipped setup.ps1 cases now that the shared table drives it Layer 2 drives setup.ps1 through the 16 shared rows of test/fixtures/windows-bash-resolution.json; like the 14 existing resolution rows they run under pwsh on Linux and skip on Windows. The case list is still asserted exactly (4 pass, 0 fail, 31 skip). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(ship-measure): give the CLI tests a private HOME (#3162) Every bun run test left ~/.gstack/projects/ship-measure-cli-*/evals and sweep-cli-fake-*/evals in the developer's real home, and the free-suite runner flagged them ("real home changed ... Give the writer a private HOME/GSTACK_HOME"). ship-measure's diagnostic-dir check calls getProjectEvalDir(), which builds the eval dir from os.homedir(), and these two CLI tests passed the real HOME while overriding only the state root. Both now set HOME and USERPROFILE to the test's own state dir. The sweep test also runs on the Windows lane, where os.homedir() reads USERPROFILE. test/ship-measure.test.ts:79 already gives its config test a private HOME the same way. getProjectEvalDir() itself is unchanged: whether eval history should follow GSTACK_HOME is left open in #3162. Fixes #3162 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(ship-measure): prove the sweep CLI never writes under the parent HOME (#3162) Reviewer test for #3163 (adopted from y$un_, forrest.sun527): points the parent HOME at a sentinel with a stub global gstack-slug and asserts no .gstack dir appears there. Fails on47d639a(the sentinel gets .gstack/projects/leaked-slug/evals); passes with #3163. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(redact): isolate fixture git from inherited hooks Fixture git was spawned with the parent environment, so a developer's core.hooksPath ran on the local push and rejected it (#2967). The helper blanks the global and system files and drops GIT_CONFIG_COUNT and GIT_CONFIG_PARAMETERS. A hooksPath set in the fixture repo still runs. * test(redact): blank GIT_CONFIG_GLOBAL with /dev/null on every platform (#2967) Follow-up to #3156 (adopted from Lockyer228): Git for Windows rejects GIT_CONFIG_GLOBAL=NUL (the repo convention is EMPTY_GLOBAL_GIT_CONFIG = '/dev/null' in lib/cso/process.ts), so both fixture helpers use /dev/null everywhere. Reviewer test: a hooksPath in the default global files under HOME/.gitconfig and XDG_CONFIG_HOME/git/config cannot reject the fixture push; it fails with the inherited environment (the47d639abehavior) and passes with the isolated env. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: gstack-config compares whole root paths and prints a runnable recovery command - report_root_disagreement compared word-split values, so two variables naming the same directory with a space were reported as disagreeing. Compare each complete value instead. - The privacy-key recovery hint single-quoted the root and left the script path bare; an apostrophe or space made it unrunnable. Quote with printf %q. - On Bash 3.2 (macOS /bin/bash) a failed `.` under set -e exits before the `||` fallback, so a missing gstack-state-root.sh exited 1 silently and test/state-root-resolver-failure.test.ts failed there. Check readability first. - The merge fixture now lives under a path with a space and an apostrophe, executes the printed command, and asserts same-directory variables agree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(config): pin the %q-quoted recovery command and run it from a spaced install path Follow-up to #3186 (adopted from davidfragnoli-lang). Intentional pin change: the fix line's GSTACK_STATE_ROOT='<legacy>' pin becomes the exact bash printf %q form of the legacy root (the PR loosened it to a prefix check; this restores an exact pin) and the original test name is kept. Reviewer test: copy bin/ to a path containing a space and an apostrophe; the printed fix: command runs cleanly and flips the merged value. Fails on47d639a(unquoted $_SELF), passes with #3186. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(gstack-gbrain-sync): fail when a star gitignore already ignores the pin ensureGbrainSourceGitignored appends .gbrain-source even when git check-ignore already excludes that path. The extra line dirties a tracked ignore-everything gitignore. * fix(gstack-gbrain-sync): skip the pin line when git already ignores it ensureGbrainSourceGitignored asked only for a literal .gbrain-source line. An ignore-everything star already excludes the pin, and the extra line left the tracked gitignore dirty. * fix(gbrain-sync): only a tracked .gitignore can stand in for the pin line Follow-up to #3178 (adopted from KuroChen). The skip now requires `git -c core.excludesFile= check-ignore -v` to name a tracked .gitignore (not global excludes, not .git/info/exclude, not a negation), so a pin ignored only on this machine still gets the shared line teammates need (#1384). Tests: the two git init spawns get timeouts (they failed the spawnSync tripwire), fixtures isolate HOME/XDG_CONFIG_HOME, and the PR's star-gitignore fixture stages its .gitignore (intentional fixture change: the contract is a tracked file). Reviewer tests: global excludes only, info/exclude only, an untracked star .gitignore, and a re-include each still append; all three fail on the PR head. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(skill-end): log the completion event before the final drain (#3198) gstack-skill-end drained and pushed before it appended its own completed timeline event, and gstack-timeline-log backgrounded its enqueue, so a full-tier end boundary returned with its latest event unpushed. The completion log now runs before the drain and the logger waits for its enqueue (one config read and a spool write; it exits early when sync is off or the session is unattended). Every consent, secret-scan and queue-lock gate is unchanged; the stale "local-only, never sent anywhere" comment is replaced. Patch and regression test from Kkkkkuro (#3198): 2 pass / 5 fail on47d639a, 8/8 with the fix. Reviewer test: with artifacts_sync_mode off the completion is written locally with no queue entry and no push. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(pregate): map Next.js route paths and tsconfig path aliases to their tests QUOTED excluded ( ) [ ] +, so a test naming app/(app)/page.tsx in a string never matched. resolveRef never read tsconfig compilerOptions.paths, so an import through @/ resolved to nothing. Both were PREGATE_NO_LANE false negatives in a Next.js App Router repo whose tests did name the files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(pregate): TypeScript alias precedence, relative extends and a no-lane explanation Follow-up to #3176 (adopted from vocodestudios). Path aliases now follow TypeScript's choice: an exact pattern, else the wildcard with the longest prefix, targets tried in order. When the chosen pattern resolves nothing, every plausible reading (other matching patterns, baseUrl, the repo root) gains the edge, so uncertainty selects more tests, never fewer. Relative extends chains are followed up to MAX_TSCONFIG_EXTENDS (4) hops, with baseUrl and paths resolved against the config that declares them. Trailing commas are dropped by the JSONC tokenizer (never inside a string) instead of a whole-text regex. A touched file with no lane now gets explain lines naming the import specifiers in test files that resolved to no repo file, any extends hop that was not followed (with the limit), and the hint to declare the file under dependencies or report the resolver gap. The lines ride testsFor's provenance, which lanes already pushes to --explain. Reviewer tests: overlapping aliases in both key orders, exact vs wildcard, target fallback, the union case, a two-hop extends chain, a string containing ',]', and the depth-limit explanation. Five fail on the PR head; all 12 pass. The manifest-parser half of #3176's follow-up (lane-selection.ts) ships in PR 2. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(browse): reap PTY children on agent shutdown * fix(browse): reap a terminal-agent PTY's whole process group at shutdown (#3147) Follow-up to #3148 (adopted from Y4NN777). The PTY child leads its own session and process group; its identity (pgid, sid, leader start time) is recorded at spawn and stays checkable after the leader exits. Disposal schedules a group reap after PTY_SHUTDOWN_GRACE_MS that, unlike the direct-child SIGKILL fallback, is not cancelled when the leader exits, and shutdown reaps every pending group before exiting, so descendants that ignore SIGHUP no longer outlive the agent. Only proven members are signalled: same pgid and sid, started no earlier than the leader, and nothing at all when the leader's PID is held by a process with another start time. Linux only (/proc); other platforms keep the direct-child behavior. During the shutdown grace the server answers 503 and no new PTY is spawned. Test changes to the PR's own test (intentional): the agent is spawned with process.execPath and the PTY child traps INT TERM HUP, so the case proves the SIGKILL fallback instead of passing on the kernel hangup. Reviewer tests: a HUP-immune grandchild is dead within the grace plus 1 s; an early-exiting leader's surviving descendant is reaped; a session that completed before shutdown still has its group reaped; unit cases for a foreign group and a reused leader PID. All four integration cases fail on 47d639a; three fail on the PR head. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * build(deps): bump the github-actions group with 5 updates Bumps the github-actions group with 5 updates: | Package | From | To | | --- | --- | --- | | [actions/upload-artifact](https://github.com/actions/upload-artifact) | `7.0.1` | `7.0.2` | | [actions/download-artifact](https://github.com/actions/download-artifact) | `8.0.1` | `8.0.2` | | [actions/setup-node](https://github.com/actions/setup-node) | `7.0.0` | `7.1.0` | | [ossf/scorecard-action](https://github.com/ossf/scorecard-action) | `2.4.4` | `2.5.0` | | [github/codeql-action/upload-sarif](https://github.com/github/codeql-action) | `4.38.2` | `4.38.3` | Updates `actions/upload-artifact` from 7.0.1 to 7.0.2 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/043fb46d1a93c77aae656e7c1c64a875d1fc6a0a...cf430e030ddbb5b0abf93d22962f4752f3646cd9) Updates `actions/download-artifact` from 8.0.1 to 8.0.2 - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c...9000827ccba6bdab643e8b6fd33ac0654aef8333) Updates `actions/setup-node` from 7.0.0 to 7.1.0 - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/820762786026740c76f36085b0efc47a31fe5020...949feb2413d6458794dcd2491c4babbbce0c15c1) Updates `ossf/scorecard-action` from 2.4.4 to 2.5.0 - [Release notes](https://github.com/ossf/scorecard-action/releases) - [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md) - [Commits](https://github.com/ossf/scorecard-action/compare/2d1146689b8cda280b9bc96326124645441f03bc...fa7117db83c52d787ae5a89f350999701505d78e) Updates `github/codeql-action/upload-sarif` from 4.38.2 to 4.38.3 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2...24c54180a607b1449ed407dd24f251e4e9147c8d) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: 7.0.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: actions/download-artifact dependency-version: 8.0.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: actions/setup-node dependency-version: 7.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: ossf/scorecard-action dependency-version: 2.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: github/codeql-action/upload-sarif dependency-version: 4.38.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(model-policy): track the upload-artifact v7.0.2 pin from #3175 Intentional pin change: test/model-policy-workflow.test.ts asserted actions/upload-artifact@043fb46d…; dependabot #3175 moves the workflow to cf430e03… (v7.0.2, verified against the upstream tag). The pin tracks the workflow. #3175's free-tests.yml hunk is left out of this PR (free-tests.yml is a #3190 file; it is re-bumped in PR 5). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(model-policy): its upload-artifact pin matches the eval workflows' pin Reviewer test for dependabot #3175: the model-policy workflow's single upload-artifact pin equals the pin evals.yml, evals-periodic.yml and windows-free-tests.yml use, so a group bump that misses one workflow (or the hard-coded pin above) is caught here. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * build(deps): hold Playwright minor/major bumps behind the windowsHide patch (#2923) Dependabot #2923 bumped playwright to ^1.64.0 and dropped the patchedDependencies entry keyed to playwright-core@1.62.1, which keeps windowsHide on Windows (#2160, #1989). The bun ecosystem now ignores only semver-minor and semver-major updates for playwright and playwright-core; patch updates still flow. The 1.64 migration (re-key the patch, the Dia/cookie qualification pins, the arm64 artifact name) is filed with owner Garry, revisit 2026-11-15. Test fails on47d639a(no ignore rule). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(readme): drop the stale skill lists users pasted into CLAUDE.md (#3169) README's install prompt and troubleshooting block each carried a skill list for CLAUDE.md; both were stale (18 shipped skills missing) and they disagreed. Claude Code lists installed skills itself, so both now keep only the /browse rule, and the troubleshooting entry says to delete an old pasted list. New free test: any comma-separated skill list in README.md or CLAUDE.md must name exactly the shipped skills (with a negative control for the detector). Fails on 47d639a; passes now. Reported by the #3169 issue author. The matching doctor warning for pasted lists ships in PR 5. Intentional pin change: test/test-value-bar.test.ts no longer expects the README skill list to name /test-audit twice; it now expects that list to be gone (0 occurrences), the registration this commit removes on purpose. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore(osv): image-size has a fix; html-to-docx's ^1.0.0 pin blocks it (#2753) The image-size ledger said no fixed release exists. OSV lists 2.0.3 as fixed for GHSA-5p2g-fcmc-qvqq and GHSA-w3rx-r6r6-pgpr (checked against api.osv.dev), but html-to-docx@1.8.0 pins image-size ^1.0.0 and there is no fixed 1.x, so the fix is a cross-major override. Both entries now say so and name the upgrade trigger; the 2026-11-30 expiry is kept. New test checks the wording and that bun.lock still shows the stated pin (it fails on47d639a). Correction first raised on #2753 by sonukapoor. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(ios-qa): forward GSTACK_IOS_LAUNCH_ENV to devicectl launches (#1796) Reimplements only the launch-environment half of #1796 (credit Bmathews721; the session-cache half is superseded by #3132's design and had state-root and symlink-write problems). devicectl.ts parses GSTACK_IOS_LAUNCH_ENV as a JSON object of string to string; launchEnvArgs() returns the --environment-variables argv (empty when unset or {}) and launchApp passes it before the bundle id. Invalid JSON, a non-object, a non-string value, a bad name or a key in the debug bridge's reserved GSTACK_ namespace refuses before any launch with the new result code IOS_LAUNCH_ENV_INVALID (the detail names keys and types, never values). launchEnvArgs is exported for the forced-relaunch path in tunnel-bootstrap.ts. Adds the result-code row (bin/gstack-doctor-components.sh regenerated by gen:skill-docs), a troubleshooting entry under its exact text, the pregate-no-lane entry's explain note, and the how-to section with an example. New test fails on47d639a(module load: no parseLaunchEnv). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(deps): declare sharp and clear two transitive advisories sharp is imported in four files, including browse/src/screenshot-size-guard.ts, and test/dependency-security.test.ts asserts a >=0.35.4 floor on it and loads it. It was declared in no dependency field, reaching the tree only through @huggingface/transformers with the overrides pin forcing the version. Declare it at ^0.35.4, matching the floor that test already asserts. Upgrade @huggingface/transformers to ^4.3.0, which clears sprintf-js@1.1.3 and aligns the sharp override with what the parent declares: 4.2.0 declared sharp ^0.34.5 while the override pins 0.35.5, which is outside that range, and 4.3.0 declares ^0.35.4. Upgrade @anthropic-ai/claude-agent-sdk to 0.2.141, which clears @anthropic-ai/sdk@0.81.0. The existing overrides entry for sharp is left in place. * test(deps): sharp is declared and its override satisfies every asking range Reviewer test for #3145 (adopted from sonukapoor): sharp must be a declared dependency, and the pinned overrides.sharp must satisfy both that declaration and every lockfile package that depends on sharp (@huggingface/transformers included). Fails on47d639a(sharp not declared; transformers 4.2.0 asked for ^0.34.5 against a 0.35.5 pin). Classifier smoke on the changed tree (transformers 4.3.0, onnxruntime-node 1.30.0, sharp 0.35.5, claude-agent-sdk 0.2.141): the real sidecar entry bundled for Node with a private state root, and the classifier in-process under Bun, each downloaded the TestSavantAI model fresh, reported testsavant: ok, and returned scores identical to 47d639a's tree on five inputs, including a >512-token input that exercises the truncation override. dependency-security, deps-smoke, overlay-sdk-cancel-eof, security-classifier and screenshot-size-guard pass; typecheck:test reports no new diagnostics. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(gbrain-sync): the no-bash skip names GSTACK_GIT_BASH and how to set it (#3197) The brain-sync stage's Windows skip line told users to set GSTACK_BASH. After the fix wave the canonical override is GSTACK_GIT_BASH, with GSTACK_BASH as a deprecated alias (plan correction 9). The summary is now the exported NO_BASH_SUMMARY: it names GSTACK_GIT_BASH first and GSTACK_BASH as the old name, and gives the two recipes that reach Claude Code (a PowerShell user environment variable plus a Claude Code restart, or the "env" block of ~/.claude/settings.json). Minimal on purpose: once PR 2 lands, this switches to lib/script-invocation.ts noBashMessage(). Until then lib/gbrain-exec.ts still reads only GSTACK_BASH, so this line is accurate only with PR 2 merged first. New test test/gbrain-sync-no-bash-message.test.ts fails on the previous commit (NO_BASH_SUMMARY not exported) and passes now. Handoff from Lane C (PR 2). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(ios-qa): the forced relaunch carries GSTACK_IOS_LAUNCH_ENV too (#1796) bootstrapTunnel's relaunchApp (an app whose boot token an earlier daemon consumed is relaunched once with --terminate-existing) built its own devicectl argv and dropped GSTACK_IOS_LAUNCH_ENV, so an env-gated debug bridge vanished after the one relaunch the howto says carries it. It now uses launchEnvArgs(); an invalid value refuses the relaunch with launch_failed and the IOS_LAUNCH_ENV_INVALID detail before anything is terminated. Two new tests in ios-qa/daemon/test/devicectl-launch-env.test.ts drive bootstrapTunnel through a consumed-token device: both fail on the previous commit (relaunch argv lacks --environment-variables; the invalid value still terminates and relaunches) and pass now. ios-qa daemon: 167 pass. tunnel-bootstrap.ts was assigned to Lane D by the coordinator for this fix. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * ci(version-gate): check the version queue only on PRs into main Stacked layer PRs carry no VERSION stamp by design (the release layer stamps once), so the queue check failed on any layer touching package.json. The queue and its collision rule only describe main; a PR into main is still checked exactly as before. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(browse): scope the SIGTERM PTY-reap case to POSIX signals (#3147) windows-free-tests run 38813751242 (shard 6) failed "SIGTERM reaps every active PTY child before terminal-agent exits" with "timed out waiting for pty-child.pid", serially and on retry. Diagnosis: invalid test expectation on Windows, not an agent defect the test can observe. The contract under test is the agent's graceful shutdown, its process.on('SIGTERM'/'SIGINT') cleanup in terminal-agent.ts. On Windows, Bun's subprocess.kill('SIGTERM') is libuv's TerminateProcess, so that handler never runs there; and the PTY fixture is an extension-less `#!/bin/bash` script, which CreateProcess cannot launch, so the child never starts (the timeout is at the pid file, after the port, token, grant and WebSocket all succeeded). The file's other integration cases were already Linux-scoped; this one now skips only on win32 and still runs on Linux and macOS. The pure ownership unit tests in the file keep running on Windows. Open, not covered by any test: whether a ConPTY child outlives a hard-terminated agent on Windows. Testing it needs a Windows-launchable terminal fixture, which BROWSE_TERMINAL_BINARY (one executable, fixed argv) cannot express without a product change. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(plan-review): bound the native outside voice without TaskOutput (#3182) Claude Code 2.1.277 removed TaskOutput, so the plan-review native fallback's preflight always failed and users without the Codex CLI never got a second voice. The fallback now uses primitives the host still enforces: a background `sleep 300` Bash timer (its exit re-invokes the parent; the 2.1.300 sleep block applies only to foreground commands), a background Plan dispatch whose completion arrives as a <task-notification>, and TaskStop for whichever task loses. Only a matching completed notification that beats the timer counts; interim ("is waiting on its own background work"), turn-limit, foreground and late results get no credit. The "[Subagent hand-back]" header (2.1.277) is treated as framing. The unavailable line now names its cause and fix, ends with OUTSIDE_VOICE_UNAVAILABLE (new result code + troubleshooting entry), and the review-log row carries unavailable_reason. The bounded-wait prose was tightened so plan-ceo-review and plan-eng-review stay under their existing size ceilings (no ratchet raise for this item). Intentional pin change: test/run-in-background-guidance.test.ts hasBoundedOutsideVoiceWait and the dispatch-contract cases now pin the timer/notification/TaskStop contract instead of TaskOutput's envelope. bin/gstack-doctor-components.sh is regenerated from lib/result-codes.ts. Reported by HuellintonMota in #3182. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(bin): gstack-ceo-plan-find locates the CEO plan for a branch (#280) /plan-ceo-review saves its scope decisions under the state root's ceo-plans/, but no plan review can find them: only /design-html reads that directory, newest file first. This helper is the CEO-plan sibling of gstack-design-doc-find, so /plan-eng-review can call one command instead of a glob in prose. It prints the newest top-level plan whose "Branch:" header names the branch, compared the way gstack-slug writes BRANCH (feat/x matches feat-x), else the newest plan, else nothing. Subdirectories are not read. A usage error exits 2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(plan-eng-review): read the CEO plan in the Design Doc Check (#280) /plan-eng-review never looked in ceo-plans/, so an eng review after /plan-ceo-review ran without the scope that review accepted, deferred or skipped. capy-ai closed #414 asking for this focused fix, and TODOS.md scopes it as a gstack-ceo-plan-find sibling of gstack-design-doc-find. The Design Doc Check now prints "CEO plan found: <path>" or "No CEO plan found" after the design doc line, and the prose says what to read the plan for. The data-not-instructions rule names the CEO plan too, since another agent may have written it. Only plan-eng-review changes: the shared {{DESIGN_DOC_DISCOVERY}} block and the branch-scoped review log are untouched. Reported by @chancheuklap in #280. main left plan-eng-review 4 bytes under its parity cap, so the +325 bytes raise maxSizeRatio to the measured 1.2196, noted the way earlier growth was. Fixes #280 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(plan-eng-review): disclose a CEO plan borrowed from another branch (#280) Review changes on top of #3159 (szsunyuan / y$un_): - gstack-ceo-plan-find keeps stdout a bare path or empty; when it falls back to another branch's plan it prints one stderr line, "CEO plan found (from branch <b>, not <current>): <path>", with control characters stripped from the plan header ("unknown" when it has none). - The Design Doc Check tells an interactive reviewer to ask before using such a plan, and a spawned session to use it and quote the disclosure. - The Design Doc Check prose is tightened so plan-eng-review fits the owner-approved 1.2196 ceiling (measured 151,950 / 124,597 = 1.2195). Reviewer tests: test/ceo-plan-find-cross-branch.test.ts (5 of 7 fail on the Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> #3159 head). The design-doc finder's matching disclosure stays deferred. * fix(test): credit abutting section chunks whose edge bytes repeat (#3182) detectSectionReads trims the longest suffix/prefix overlap between consecutive Bash outputs. Two chunks that abut exactly (tail -c +N after a chunk ending at N-1) can share an edge byte by chance; the trim then drops a real byte and a fully printed section reads as incomplete. #3182's review-sections edit moved plan-ceo-review's 66,999-byte boundary onto "cur|rent", which turned the existing byte-range test red. The detector now also checks the plain concatenation; a gap still refuses. Detector/fixture defect, not a product change. New regression case in test/section-capture-native-tools.test.ts fails without the fix. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(changelog): fix wave 2026-10-15 release notes Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * v1.91.104.0 fix: the 2026-10-15 issue wave Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(qa): probe each documented input rule, not only its boundary values The paid gate's qa-functional-cli-report run (38829994639) probed zero and an unsafe integer but never a non-digit amount, so it missed the planted parseInt defect ("Amounts contain only ASCII digits") and reported no defect. The functional contract map's invalid-input row now asks for one probe per documented input rule (allowed characters, format, sign, range, arity). test/qa-functional-prompt.test.ts replays a captured CI Read of system-functional.md; its completed-method variant now feeds the current section bytes, so the validator check keeps its omitted/pending negative controls without pinning the old wording. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * v1.91.106.0: restamp the fix wave above v1.91.105.0 Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(release): keep the wave's dependency bumps through the main merge Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> Co-authored-by: Bryan Stone <aegixx@gmail.com> Co-authored-by: vilano93 <244097767+vilano93@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: aftabkh4n <akobkz@gmail.com> Co-authored-by: Benjamin D. Smith <benjamin.smith@binarysword.com> Co-authored-by: Jess Sullivan <jess@sulliwood.org> Co-authored-by: NetrioGit <5wngh@naver.com> Co-authored-by: y$un_ <forrest.sun527@gmail.com> Co-authored-by: Lockyer <135391289+Lockyer228@users.noreply.github.com> Co-authored-by: davidfragnoli-lang <249433777+davidfragnoli-lang@users.noreply.github.com> Co-authored-by: KuroChen <kurochen_macmini@KuroChendeMac-mini.local> Co-authored-by: AV <vo.quanganh@gmail.com> Co-authored-by: Y4NN777 <171065166+Y4NN777@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sonu Kapoor <sonukapoor@gmail.com>
142 KiB
Troubleshooting gstack messages
Find the message you saw, then follow its fix. Every check gstack runs ends in one of three states:
- ran: the check executed. It may still have found problems.
- not run: gstack chose not to run it (you turned it off, or nothing applied).
- unavailable: gstack tried and could not run it. This is missing coverage, never a pass.
A message that is not ran has this shape:
<check>: not run (<reason>). Fix: <command>
<check> unavailable: <reason> (<detail>). No review ran; this is missing coverage, not a pass. Fix: <command>
<check>: ran, verdict unverified (<reason>). Fix: read the output above
Search this page for the words after unavailable: or not run (. Each
section has a stable link anchor; the reason codes and anchors come from
lib/gate-outcomes.ts, and a free test fails if a code has no section here.
Check readiness before a skill runs
Run ~/.claude/skills/gstack/bin/gstack-doctor (other hosts: ./setup --status
in your gstack checkout prints the doctor's absolute path). It prints one row
each for the install, state root, Bun, hooks, Codex, the cached Codex model
check, artifacts sync, the browse bundle, the other compiled binaries, the
/cso native helper, Claude Code, the largest session journal and recent
/autoplan guard codes. Each row is ok, warn,
not configured or fail with its fix; only fail exits non-zero. It makes
no paid call: the Codex rows report the cached model check and its age, and
--live runs that check once. Paste its output into bug reports.
Model policy
The model-policy guide explains the two tiers, all six settings, precedence and the read-only inspection command. A resolved selection is not proof that the requested provider or model can run.
Model policy configuration is invalid or unreadable
Meaning. The new policy path could not read a coherent configuration or a configured value is invalid. An unreadable file is not treated as an absent setting: doing that could silently select the premium default.
Fix. Repair the file or key named by the error. plan_review_tier accepts
frontier, smart or host; implementation_tier accepts frontier or
smart. Use gstack-config unset <key> to restore a default, not an empty
value. A rejected set leaves the old value intact. Correct ownership/read
permissions rather than deleting an unrelated state directory.
For a malformed line such as plan_review_tier = smart, edit the reported file
and correct or remove that line first: unset only removes canonical key:
records and cannot repair a misspelled delimiter or quoted key.
Expected result. gstack-models list or the corresponding resolve
command explains the winning source. No outside model call ran on this error.
A custom provider needs an explicit model or host mode
Meaning. Native settings or environment flags route the reviewer to a custom endpoint or partner platform. A public API catalog identifier is not necessarily a valid deployment identifier there. Malformed native provider configuration also prevents gstack from assuming the public endpoint.
Fix. Supply a compatible model for that request, set the provider's gstack
environment override or tier-model setting, or choose
gstack-config set plan_review_tier host. Keep credentials in the provider's
normal authentication mechanism, never in a model setting. If the error names
a malformed native configuration, fix that source first.
Expected result. Inspection reports an explicit selection or honest host-managed delegation. It does not certify account entitlement or send a public default to an inferred custom endpoint.
The selected review model could not be used
Meaning. The requested model was rejected or the provider could not complete the review. A model may be nonexistent, unavailable to the account, retired, or behind a misconfigured endpoint; preserve the provider's evidence rather than treating all of these as the same diagnosis. No successful outside review is implied, and gstack does not substitute a different model.
Fix. Change the source named in the error. For example, an environment override beats a tier setting, so editing the tier cannot replace it. Unset or correct that override, or explicitly name a supported model for the request. Follow the existing auth, quota, sandbox or timeout repair when that is the reported failure. Missing usage identity remains unknown, even if inspection showed a requested ID.
Expected result. The next invocation announces its model and source, checks that same selection and reports the actual provider result. A cached probe or a native fallback is not a completed outside review.
Model-catalog freshness is unknown, stale or needs an update
Meaning. Official evidence changed, a required source could not be
checked, or the retained evidence does not match this catalog and parser.
unknown/source-unavailable is not current. A later source failure must not
erase an earlier retirement finding or close its maintenance issue.
Fix. Read the failed source and evidence details in the freshness workflow report and tracking issue. Repair the source parser or publication access when needed, then run the workflow manually against the default branch. If a model was superseded or deprecated, follow the maintainer checklist to qualify a replacement or record a supported mitigation. Do not clear the issue just because a new release has not been adopted yet.
Expected result. Complete source checks produce evidence bound to the current catalog, while unresolved lifecycle findings remain visible. Personal pins and unrelated PRs are unchanged. Check the workflow's last run date too: a scheduled job that never runs cannot report its own absence.
Repairing recoveryRequired state
This flag deliberately keeps the issue open until a maintainer repairs the record. Another successful fetch alone cannot certify corrupted history.
- Save the current issue body, including its retained recovery archive, and
download the latest trustworthy workflow report. Use its
report.stateas the starting state; do not invent run IDs, catalog hashes or check dates. - Reconcile its
lifecyclearray with the issue's independent lifecycle block and any retained archive. Preserve known retirements, deadlines and human dispositions. A model currently in the catalog must not keep an oldresolvedByCatalogflag from a previous replacement. - In the issue editor, restore valid JSON between the
state:beginandstate:endmarker lines. Put that same lifecycle array betweenlifecycle:beginandlifecycle:end. Restore the standalone ownership and region markers, and leave human text outside the owned region intact. - Set
recoveryRequiredtofalseonly after that reconciliation. If prior successful evidence cannot be verified, setlastSuccesstonullrather than making up a fresh success. If lifecycle history cannot be recovered, leave the flag set and the issue open for investigation. - Run Model-policy freshness against the default branch. Only complete, current evidence with no unresolved findings can close the issue. A parsing or source error leaves it open; do not clear history to manufacture a pass.
Outside reviews (Codex and Claude Code)
Outside reviews send your diff, plan or question to a second AI provider. Their
verdict comes from lib/outside-review-result.ts, which prints
VERDICT: clean|findings|unverified|unavailable. A findings verdict with a
P0 or P1 finding blocks exactly like a native P0/P1. unverified and
unavailable are missing coverage: /ship and /review continue, show the gap in
the readiness dashboard and the PR body, and never count it as a pass.
How the verdict is read from a review
- Severity tags.
[P0]-[P3], Codex's nativeP1:labels, and a P0-P3 label at the start of a line after an optional heading, bullet, number or bold:**P1 — x**,**P1** — x,1. P2 — x,P2 —,### P1, or a table cell| P1 |. P0 and P1 block; P2 and P3 are advisory. A P0-P3 token in that label position that none of these forms reads (## P1/P2 findings) makes the reviewunverified, neverclean. Code fences and inline code are quotes, not labels. - Severity words in label position.
Severity: High,Priority: low, a line that starts withHigh:,High —,[High]or**High**(after an optional heading, bullet or number), a bold**High**anywhere, or a table cell| high |. Critical and High block like P0 and P1; Medium and Low are advisory like P2 and P3. Words inside prose do not count: "high-level", "low-risk", "a medium-term follow-up" and "no critical findings" are not findings. - Every review gate. These rules apply to every gate that summarises a
review, including the
executiongate behind/codexconsult and challenge. A reply whose lastVerdict:line saysnot readybut carries no readable severity printsFINDINGS: unspecifiedand exits 3 like a blocking finding. - No findings. A review with no tag and no label is
cleanonly when it says so explicitly (NO_FINDINGS, "no issues", "did not find any bugs"). Otherwise it isunverified(see below), neverclean. A/codexconsult answer with no severity labels at all is not a review summary and staysclean. - Design proposals (the design-direction voices in /design-consultation
and /office-hours) are read with the
proposalgate: a completed proposal needs only itsRecommendation: ... because ...line.
Outside-review prompts ask the reviewer to label each finding Critical, High,
Medium or Low, so most reviews land on findings or clean.
gstack: cannot load gstack-codex-probe / gstack: cannot locate <helper> (shell: ...) / CODEX_MODE: helper_unavailable
Meaning. Skills run gstack-codex-probe as a command, one subcommand per
check, so the shell your agent uses does not matter. cannot load gstack-codex-probe and CODEX_MODE: helper_unavailable mean the probe file is
missing or not executable in your install.
cannot locate <helper> (shell: ...) comes from a helper that is still loaded
into your shell with source (gstack-egress-lib.sh, and the Codex probe when
a skill rendered before the upgrade sources it). Such a helper finds its own
directory from bash (BASH_SOURCE) or zsh (%x). In any other shell (dash,
sh), or when the shell cannot say which file it is reading, it stops instead of
guessing a path. The message names the shell it saw.
Fix. For cannot load, re-run ./setup from your gstack checkout (or
/gstack-upgrade). For cannot locate, run the skill from bash or zsh (the
macOS and Linux defaults), or tell the helper where gstack is installed:
export GSTACK_ROOT=~/.claude/skills/gstack # your install dir; it holds bin/
If you see gstack: sourcing gstack-codex-probe is deprecated ..., a skill
rendered before the upgrade is still sourcing the probe. It keeps working
until a release on or after 2026-10-21; run /gstack-upgrade to re-render
your skills now.
Expected result. ~/.claude/skills/gstack/bin/gstack-codex-probe select-model exec
prints CODEX_SEL: <model> (and CODEX_MODEL: <model> (exec; source: ...) on
stderr) from any shell, and preflights print a CODEX_MODE other than
helper_unavailable. gstack-codex-probe help lists every subcommand.
Codex outside review unavailable: Codex's sandbox could not start here (...)
Meaning. Codex runs every command inside a Linux sandbox (bubblewrap). In
many containers and devcontainers the kernel does not allow unprivileged user
namespaces, so the sandbox cannot start and every command Codex tries fails.
gstack reports this instead of trusting a review that read nothing.
CODEX_MODE: sandbox_unavailable is the same condition found by the free
preflight before any paid call.
What is kept. Nothing was sent for review, or the review was discarded. Your code and files are unchanged.
Fix. Enable unprivileged user namespaces for the container (for Docker, a seccomp profile that allows them), or, inside a container you trust, run Codex without its sandbox for this shell:
export GSTACK_CODEX_NO_SANDBOX=1
Expected result. With namespaces enabled, the next review prints
OUTSIDE_STATUS: completed. With GSTACK_CODEX_NO_SANDBOX=1, every review
prints WARNING: GSTACK_CODEX_NO_SANDBOX=1: ... because Codex can then read
and write anything your user can. Only the exact value 1 works, and only from
your shell environment.
... outside review unavailable: the reviewer could not run commands or read the diff (...)
Meaning. The reviewer tried to run commands but none succeeded, or its answer says it could not run commands or read the diff. A "no issues found" written after that is not a review.
What is kept. Nothing; the answer is shown above but not counted.
Fix. Read the reviewer's stderr above, repair what it names, and re-run.
Expected result. OUTSIDE_STATUS: completed with a VERDICT: line.
... outside review unavailable: the reviewer process failed (exit N: ...)
Meaning. The provider CLI exited non-zero. The first stderr line is in the parentheses.
What is kept. Partial output is shown above; it is not counted.
Fix. Repair the cause in the provider diagnosis (log in again, fix the model, check the network), then re-run.
Expected result. OUTSIDE_STATUS: completed.
... outside review unavailable: the reviewer hit its time limit and was stopped (exit 124)
Meaning. The provider did not finish within its deadline and was stopped.
What is kept. Partial output is shown above; it is not counted.
Fix. Re-run with a smaller scope, or check the provider's status and your network.
Expected result. OUTSIDE_STATUS: completed.
... outside review unavailable: the reviewer returned no response
Meaning. The provider exited successfully but returned nothing.
Fix. Read the stderr above, then re-run.
Expected result. OUTSIDE_STATUS: completed.
... outside review unavailable: the reviewer declined to review
Meaning. The provider answered with a refusal instead of a review.
Fix. Re-run; if it declines again, rely on the native review.
Expected result. OUTSIDE_STATUS: completed.
... outside review unavailable: the response lacks the markers this gate requires (...)
Meaning. The answer did not contain what the gate checks: a
Recommendation: ... because ... line, a SCORE:/AMBIGUITIES: pair, or
severity tags.
Fix. Re-run the review.
Expected result. OUTSIDE_STATUS: completed.
... outside review: ran, verdict unverified (...) / OUTSIDE_STATUS: unverified / GATE: UNVERIFIED
Meaning. The review completed but had no severity tag or label and gave no explicit no-findings conclusion, so no pass or fail can be read from it (how the verdict is read).
What is kept. The full answer is shown above.
Fix. Read the output above and decide. It is not a pass, and /ship and /review list it as missing coverage.
... outside review: ran, verdict unverified (the review labels a finding with a P0-P3 severity in a form the parser cannot read (line N: "...")) / REASON: unparsed_severity
Meaning. The reply starts a line with a P0-P3 token, the place a finding's
label goes, but in a form gstack does not read as one severity, for example
## P1/P2 findings or - P2/P3 nits. gstack will not guess which severity
applies, so the verdict is unverified instead of clean. The quoted line in
parentheses (also printed as UNPARSED:) is the first such label. When the
same reply also has a readable P0 or P1 finding, that finding blocks
(VERDICT: findings, exit 3) and the unparsed line is still printed.
What is kept. The full reply is shown above.
Fix. Read the quoted line and the reply, and decide; it is not a pass.
Re-run the review and ask the reviewer to label each finding [P1] <title> or
P1: <title>. To re-check a saved or corrected reply, run from your gstack
checkout:
bun lib/outside-review-result.ts --verdict execution reply.md
(use review or structured for those gates).
Expected result. VERDICT: findings or VERDICT: clean with a P0-P3
FINDINGS: value, and no UNPARSED: line.
CODEX_MODE: model_unusable / MODEL_UNUSABLE
Meaning. Codex rejected the selected model: the account cannot use it
(HTTP 400), the model is retired, or a custom provider's base_url is wrong
(HTTP 404). The CODEX_MODEL: line names the model and where it came from.
Fix. Choose a model your account can use, or correct the provider:
export GSTACK_CODEX_MODEL=<supported-model>
# or edit model / base_url in ${CODEX_HOME:-~/.codex}/config.toml
Expected result. CODEX_MODE: ready.
CODEX_MODE: quota_exhausted / MODEL_QUOTA_EXHAUSTED
Meaning. Codex refused the call because the account behind it hit its
usage limit (You've hit your usage limit, or insufficient_quota). The line
under the marker is Codex's own message, with its reset time and where to buy
more. The model choice is fine. gstack reports outside coverage as
unavailable, never as a pass, and caches the result for 15 minutes, so the
rest of the run (and other skills) make no Codex call. The HINT line says how
many minutes remain.
Fix. Wait for the reset time in Codex's message, or add credits or a higher plan for that account. To use a different account, sign in again:
codex login
To re-check before gstack's 15-minute cache expires (for example, right after buying credits), skip the cached result for one check, or delete it:
export GSTACK_CODEX_PROBE_RETRY=1 # unset it again afterwards
# or
rm -f ~/.gstack/.codex-model-probe # <state root>/.codex-model-probe
Expected result. After the reset (or after codex login, which changes
the auth signature and re-probes at once), CODEX_MODE: ready.
CODEX_MODE: unverified (rate_limited) / MODEL_PROBE_RATE_LIMITED / unavailable: Codex rate-limited the review
Meaning. Codex answered HTTP 429 (too many requests), which usually clears
within seconds. It is a different state from quota_exhausted and is never
cached. At probe time the review still runs, and its own result decides. A 429
during the review itself means that review failed, so coverage is missing,
never a pass.
Fix. Re-run the review in a minute. If 429s persist, check the rate limits for the account or API key on the provider's dashboard.
Expected result. CODEX_MODE: ready, and the review completes.
CODEX_MODE: unverified / MODEL_PROBE_INCONCLUSIVE
Meaning. The short model check timed out or hit a network error, so gstack could not confirm the model works. The review still runs and its own result is checked.
Fix. Nothing now. If the review then fails, its message says why.
CODEX_MODE: not_authed / AUTH_FAILED
Meaning. No Codex credentials were found: no CODEX_API_KEY, no
OPENAI_API_KEY, no auth.json, and no set environment variable named by a
custom provider's env_key in config.toml.
Fix.
codex login
or export the variable your provider's env_key names.
Expected result. CODEX_MODE: ready.
Codex review skipped (codex_reviews disabled) / CODEX_MODE: disabled
Meaning. You turned outside reviews off. This is not run, never a pass and
never an outage.
Fix. To turn them back on:
gstack-config set codex_reviews enabled
outside review: not run (outside reviews are turned off ... ). Fix: gstack-config set codex_reviews enabled ... / OUTSIDE_STATUS: not_run reason=consent_missing
Meaning. The send itself refused. gstack-codex-probe run-with-timeout,
gstack-claude-code, gstack-outside-voice and the unattended
gstack-autoplan runner read codex_reviews before they send repository
content to an outside model. When it is disabled, or when the config file
exists but cannot be read (or holds a value other than enabled or
disabled), they print this line, start no CLI and make no API request. An
unset codex_reviews means the default, enabled, and the first send on a
machine prints the notice below. A review you ask for by
name (/codex, /claude-code) passes --user-invoked and still runs. This is
not run, never a pass and never an outage; /autoplan records it as the
phase's outside outcome (status: disabled, outcome: not_run,
reason: consent_missing).
Fix. To allow outside reviews again:
gstack-config set codex_reviews enabled
gstack-config get --stored codex_reviews # enabled, disabled, unset, or unreadable (exit 2)
If the stored read exits 2, fix the permissions or the malformed
codex_reviews line in the config.yaml it names, then re-run.
NOTICE: gstack outside reviews send the review prompt and code to Codex (...) using ...
Meaning. Outside reviews are on by default. The first one on a machine
says which provider receives your prompt and code and which login or key pays
for it. It shows once (gstack records .codex-review-notice-shown in its state
directory) and never blocks the review.
Fix. Nothing, if that is what you want. To stop sending code to Codex:
gstack-config set codex_reviews disabled
Expected result. Later reviews print Codex review skipped (codex_reviews disabled)
or CODEX_MODE: disabled, and /ship and /review show the outside review as not run.
/ship, /review and /document-release
Shipped without a version change: no version source is configured (no VERSION file, no .gstack/version-path). ...
Meaning. /ship found no version file it owns: there is no VERSION, no
.gstack/version-path, or release automation (release-please, Changesets,
semantic-release, a workspace monorepo, a placeholder package.json version)
owns the version. This is not run for the version bump.
What is kept. Everything else ships. No bump, no CHANGELOG version header,
no vX.Y.Z PR title prefix.
Fix. To have /ship version releases, create VERSION, or pin the file:
echo package.json > .gstack/version-path && git add .gstack/version-path
Expected result. The next /ship bumps that file and writes a CHANGELOG header.
gstack-version-bump: classify: version source is broken: <path> ...
Meaning. The pinned version file does not exist, is empty, unreadable, or
contains no parsable version. /ship stops instead of inventing 0.0.0.0.
What is kept. Nothing was written.
Fix. Repair that file, or correct --version-path / .gstack/version-path.
Expected result. gstack-version-bump classify prints the current version.
BASE_REFRESH: stale <rev> / Base coverage: stale at <rev>
Meaning. /review could not fetch the base branch (offline, no credentials,
or a read-only .git such as Codex's sandbox), so it reviewed against your
local origin/<base> at <rev>.
Fix. Run git fetch origin <base> outside the sandbox and rerun /review if
the base has moved.
gstack-wtree: cannot create a temp index under <dir>; set TMPDIR to a writable directory / gstack-review-log: cannot create a private object directory under <dir>; no working-tree fingerprint. Set TMPDIR to a writable directory.
Meaning. /review fingerprints the working tree in a private temporary
object directory (Codex's sandbox keeps .git read-only). The temp directory
could not be created, so this review has no fingerprint and later steps cannot
reuse its evidence.
Fix. export TMPDIR=<a writable directory>, then run /review again.
gstack-wtree: cannot stage the working tree: <git error>
Meaning. The fingerprint's objects could not be written. Standalone
gstack-wtree writes into the repository's object store.
Fix. Run it through /review (gstack-review-log supplies a private object
directory), or make .git/objects writable.
Plan completion audit: not run (no plan is bound to this branch and no docs/designs/ file matches). Fix: ...
Meaning. /ship and /review audit a PR against the plan bound to its branch
(plan mode, /autoplan, or a Plan: <path> line in the PR body). With none bound
and no matching docs/designs/ file, they no longer pick the newest unrelated
plan; the audit is not run.
Fix. Add Plan: docs/designs/<file>.md to the PR body, or run /autoplan.
Expected result. The next /ship or /review prints the audit against that plan.
PERSISTED: no / PERSIST: gstack-review-log '{...}' / no saved Eng Review: run the PERSIST: line ...
Meaning. /plan-eng-review ran where the caller forbids writes (for example
a delegating agent keeps the reviewer read-only, so it cannot edit the plan it
judges). The review completed: it ends DONE with PERSISTED: no, records
telemetry success, and returns the full report and decision ledger in its
reply. Nothing was saved, so /ship's readiness dashboard has no Eng Review
record and reports NOT CLEARED with "no saved Eng Review". A real failure (a
permitted save that failed, stale evidence) still ends BLOCKED.
What is kept. The reply holds the complete report; its last line is the review-log command with this review's values.
Fix. From the reviewed repository, run the reply's last line without the
PERSIST: prefix:
~/.claude/skills/gstack/bin/gstack-review-log '{"skill":"plan-eng-review",...}'
Or rerun /plan-eng-review where it may write its report and log.
Expected result. gstack-review-read lists the plan-eng-review entry and
/ship's dashboard shows the Eng Review row (CLEAR when its status is clean).
gstack-learnings-search: bun not found on PATH, so learnings could not be read. Fix: ...
Meaning. Learnings search (and gstack-timeline-read, "the timeline") needs
Bun. It exits 127 instead of looking like "no learnings".
Fix. Install Bun from https://bun.sh, then cd <gstack checkout> && ./setup.
Expected result. Skills print your prior learnings again.
LEARNINGS: unavailable (<first stderr line>)
Meaning. A skill's learnings lookup failed (often Bun is missing). Skills used to show nothing here, which looked like "no learnings".
What is kept. Your learnings file is untouched; the skill continues without them.
Fix. Read the reason in parentheses. If it names Bun, install it and re-run
./setup.
gstack-learnings-log: could not create a temp file in <dir>, so the learning was not recorded. Fix: ...
Meaning. The learnings, question and preference logs write through a temp
file in ${TMPDIR:-/tmp}. A sandbox that blocks it used to drop the entry silently.
Fix. export TMPDIR=<a writable directory> and log again.
CALIBRATION: not calibrated: no recorded signals
Meaning. Your logged answers matched no psychographic signal, so /plan-tune has nothing to calibrate from. It no longer says "calibrated".
Fix. Answer more registered questions, then gstack-developer-profile --derive.
gstack: earlier data for this project is in projects/<slug> (a worktree's old bucket). Merge it: ...
Meaning. Linked worktrees now share the main checkout's project identity. Learnings and checkpoints a worktree wrote under its own old bucket still exist.
Fix. Merge them (logs are de-duplicated, conflicts listed, nothing overwritten):
gstack-slug --adopt-legacy --from <slug>
Or stop the reminder: gstack-slug --adopt-legacy --dismiss <slug>.
Design skills
DESIGN_NOT_AVAILABLE: <path> --version exited <code> (or timed out after 10s, is not installed, no timeout/gtimeout/perl ...)
Meaning. Design skills now run "$D" --version with a 10-second deadline
before claiming DESIGN_READY. Exit 137 means macOS killed the binary at launch
(a bad code signature); setup prints the same condition as
design unavailable: <bin> is killed at launch (exit 137) after re-signing.
What is kept. Nothing was generated or spent.
Fix. Re-run setup, which rebuilds and re-signs the binary:
cd <gstack checkout> && ./setup
If the message names a missing timeout tool, install coreutils (gtimeout) or perl.
Expected result. The next design skill prints DESIGN_READY.
GSTACK_DESIGN_IMAGE_MODEL="<value>" is not a gpt-image model name (expected something like gpt-image-2); fix it or unset it to use gpt-image-2
Meaning. The design binary refuses an image tool model override that is not
a gpt-image-* model name, before sending any request.
Fix. export GSTACK_DESIGN_IMAGE_MODEL=gpt-image-2 (or another gpt-image
model your key can use), or unset GSTACK_DESIGN_IMAGE_MODEL.
TASTE_PROFILE_UNAVAILABLE: could not resolve the project slug (gstack-slug failed). Fix: run ./setup.
Meaning. /design-consultation and /design-shotgun load your taste profile. This says it could not be found, instead of acting as if you had none.
Fix. cd <gstack checkout> && ./setup.
cannot save paid image to <path>: <code> (<cause>). Image bytes were received; saved a recovery copy to <tmp path>. Fix: ...
Meaning. The paid image generation succeeded but writing it to the output
path failed (disk, permissions, or every name through -999 taken). gstack never
buys the same image twice and never overwrites an existing image.
What is kept. The image, at the printed recovery path in your temp directory.
Fix. Follow the printed fix (free disk space, make the folder writable, or
pick a new --output), then copy the recovery file to where you want it.
Codex and other env-var hosts
gstack: no install found (tried <path>). Fix: ./setup --host <host> from your gstack checkout; ./setup --status shows it.
Meaning. On Codex, Factory, OpenCode, Cursor, Copilot and Kiro every bash
block finds gstack on its own: an exported GSTACK_ROOT (with bin/ and
lib/), then a repo-local install, then the host's global install
($CODEX_HOME/skills/gstack on Codex). None existed.
Fix.
cd <gstack checkout> && ./setup --host codex # or your host
./setup --status
Expected result. ./setup --status lists the install and says the router is
a real file and its section links resolve.
router is a symlink, which Codex skips / section links: N of M broken (first: ...)
Meaning. ./setup --status checks each registered install. Codex ignores a
symlinked SKILL.md, and a broken section link means a carved skill cannot load
its next step.
Fix. cd <source> && ./setup --host <host> (the command is printed).
saved your edited <path> to <backup> (setup rewrites gstack's SKILL.md copies on every run)
Meaning. Installed router and nested SKILL.md files are now real copies, rewritten by every setup. Your hand edit was saved first.
What is kept. Your edit, at <backup>. Move the customization into your own skill.
error: could not back up an edited SKILL.md copy under <root>; the previous runtime root is unchanged
Fix. Fix permissions on <state root>/backups, then ./setup --host <host>.
ERROR: <dir> is not a gstack checkout; nothing was changed. Re-run Step 2. / INSTALL_DIR is not set: re-run Step 2 ...
Meaning. /gstack-upgrade now checks, before any git command, that it is inside gstack's own checkout. A block ran without the path Step 2 printed, so it stopped instead of touching your project.
Fix. Run Step 2 again and prefix the block with INSTALL_DIR=<printed path>.
On Windows (Git Bash) with INSTALL_DIR set correctly, older /gstack-upgrade
text compared two spellings of the same path and refused anyway; recover with
cd <install dir> && git pull --ff-only --autostash && ./setup (see
the hook false positive).
<host>/<skill>/SKILL.md is <bytes> bytes, over the 160,000-byte limit. Fix: carve sections with usesLazySections() for this skill.
Meaning. For contributors: hosts read at most 160,000 bytes of one SKILL.md.
bun run gen:skill-docs fails in the repo and warns under --install-root.
Setup and auto-update
gstack needs Bun 1.3.3 or newer (1.4.2 recommended); found <version> at <path>. Nothing was installed or changed.
Meaning. Bun older than 1.3.3 silently ignores the build flags that stop
gstack's compiled tools from reading a project's .env, so setup refuses it
before writing anything. The same link appears on two warnings that do not stop
setup: warning: gstack is tested on Bun 1.4.2 (CI pin); found <version> (1.3.3
up to 1.4.2 works but is untested) and warning: could not read the Bun version (...) (setup continued; check bun --version).
Fix.
bun upgrade
./setup
Expected result. bun --version prints 1.4.2 or newer and setup finishes
with no Bun warning.
gstack auto-update: update held (bun-too-old: found Bun <version> at <path>; gstack <version> needs <floor> or newer); nothing was installed or changed. Fix now: ...
Meaning. Team-mode auto-update fetched a release whose setup needs a newer Bun than the one setup would run. It left your checkout and installed skills at the current revision and prints this line once per session start. (An older auto-updater running its first update cannot make this check.)
Fix. bun upgrade; the next session start resumes the update. To update
now: bun upgrade && cd <gstack checkout> && git pull --ff-only && ./setup.
bun-version-unreadable: could not read the version of Bun at <path> (bun --version printed '<output>'); gstack <version> needs <floor> or newer (BUN_VERSION_UNREADABLE)
Meaning. Before moving the checkout, the auto-updater and /gstack-upgrade
compare the Bun that setup would run with the incoming release's security
floor. bun --version printed something that is not a version (shown, with
control characters removed), so the comparison could not run and the update was
held: nothing was installed or changed. /gstack-upgrade prints the same text
after BUN_VERSION_UNREADABLE:. Earlier releases read an unreadable version,
and every version under zsh, as "not below the floor" and went ahead (#3195).
Fix. Reinstall Bun; bun upgrade may not run on a broken install. In Git
Bash or a macOS/Linux terminal:
curl -fsSL https://bun.sh/install | bash
bun --version
Expected result. bun --version prints a version such as 1.4.2, and the
next session start (or /gstack-upgrade) updates.
Fixed in. The first release after v1.91.101.0 (#3195).
gstack auto-update: setup did not finish (<reason>); installed skills may be out of date. gstack retries automatically. Fix now: cd <dir> && ./setup
Meaning. The pull worked but setup (or migrations) failed. gstack no longer
says "just upgraded". It retries after 1 hour, then 6, then 24, and prints this
line once per session start. A (bun not found on PATH) reason means install
Bun first.
What is kept. The previous install stays active.
Fix. Run the printed command.
gstack auto-update: git pull did not finish (exit N: ...); gstack is not updating. Fix now: ...
Fix. cd <gstack checkout> && git pull --ff-only && ./setup.
gstack setup: refusing to register hooks that do not parse (Claude Code would block tool calls with them): <file>:<line>: <error>
On Windows (Git Bash), from 1.91.31: a false positive. If every
TypeScript hook fails with bun could not bundle the TypeScript entry (the
question-log, question-preference, auq-error-fallback and timeline-stop hooks,
/autoplan's publication hook and /plan-ceo-review's mode-handoff hook),
your hooks are fine. Git Bash handed Bun its paths as C:/… while the checker
looked them up as /c/… (#3088, #3100); the checker on main reads both
spellings. The same Git Bash spelling made /gstack-upgrade stop with
ERROR: <dir> is not a gstack checkout; nothing was changed. One recovery
covers both: update by hand, then run setup from the update.
cd ~/.claude/skills/gstack && git pull --ff-only --autostash && ./setup
Use your install directory if it is not ~/.claude/skills/gstack (./setup --status prints it). Expected result: setup finishes with exit 0 and no
refusal line, and later auto-updates go through.
Meaning, everywhere else. Claude Code runs gstack's hook shims through
/bin/sh, and a hook that does not parse exits 2, which blocks the tool call
it guards in every session. Setup parse-checks every hook it registers and
every hook a skill's frontmatter runs (/autoplan, /careful, /freeze,
/guard, /investigate and /plan-ceo-review): the shim, the gstack shell
helpers it sources (such as careful/bin/hook-extract.sh and
bin/gstack-state-root.sh), and the TypeScript it runs with its local imports.
A merge conflict marker line in any of those files fails as unresolved merge conflict marker, even when the file still parses (markers inside a heredoc, a
string or a template literal do). The TypeScript is bundled from the gstack
checkout, so the tsconfig.json or bunfig.toml of the project you run it
from is never read. It registers the hooks that parse, skips the ones listed,
finishes the rest of the install, and exits non-zero. Claude Code runs hooks
straight from ~/.claude/skills/gstack, so a skipped hook that an earlier
setup registered, or that a skill runs, keeps running the broken file until it
is fixed. This is a gstack bug, or a half-applied edit or merge in your
checkout: report the printed <file>:<line>.
Fix.
git -C ~/.claude/skills/gstack status # half-applied edits or merge conflicts?
git -C ~/.claude/skills/gstack checkout -- <file> # or finish the merge
cd ~/.claude/skills/gstack && ./setup
Expected result. Setup finishes with exit 0 and no refusal line.
gstack auto-update: update held (hook-does-not-parse: <file>:<line>: <error>); nothing was installed or changed, and your current hooks keep running. ...
On Windows (Git Bash), from 1.91.31: a false positive. If the held
reason is <hook>.ts:1: bun could not bundle the TypeScript entry, the
release is fine: your installed checker misread Git Bash's C:/… paths
(#3088, #3100), and the auto-updater checks every release with it, so it
holds every release until you update once by hand. Recovery (this also clears
/gstack-upgrade's is not a gstack checkout refusal):
cd ~/.claude/skills/gstack && git pull --ff-only --autostash && ./setup
Use your install directory if it is not ~/.claude/skills/gstack. Expected
result: setup finishes with exit 0, the next session start prints no held
line, and later updates install on their own.
Meaning, everywhere else. Team-mode auto-update fetched a release with a
hook that does not parse or still holds a merge conflict marker (the same
check setup runs). It checked the incoming revision before moving your
checkout, so your checkout, installed skills and registered hooks stay at the
current revision. The incoming release's own bin/gstack-hook-check decides,
so a checker bug in your install cannot hold later releases; your installed
checker decides only when the incoming one crashes, prints nothing or prints
a report it cannot verify. gstack checks again at the next update check and
installs the first release whose hooks parse. This is a gstack bug: report the
printed <file>:<line>.
Fix. Wait for the next release. A manual git pull followed by ./setup
cannot be checked before the pull; setup then refuses the broken hook (see the
entry above).
gstack auto-update: update held (hook-check-unavailable: <reason>); nothing was installed or changed, and your current hooks keep running. ... Fix now (in a terminal; Git Bash on Windows): cd '<dir>' && git fetch origin && bash bin/gstack-ff-update . '@{upstream}' && ./setup ...
Meaning. Neither the incoming release's hook checker nor your installed
one gave a verified report on the incoming hooks (a crash, no output, an
unreadable report, or no Bun to bundle the TypeScript hooks), or the incoming
revision could not be extracted for the check (cannot extract the incoming revision: <git's error>), so the auto-updater cannot tell whether moving your
checkout would break Claude Code tool calls. It left your checkout, skills and
hooks at the current revision. The <reason> names what each checker did.
Fix. Run the printed command. bin/gstack-ff-update moves the checkout
without losing a local edit (see
shelved edits), and setup checks every hook it
registers and says what is wrong (for example that Bun is missing).
On Windows, if the reason is git archive of the incoming revision failed,
your updater predates the fix for #3184: see
the held-install recovery.
HELD hook-check-unavailable: git archive of the incoming revision failed / gstack auto-update: update held (hook-check-unavailable: git archive of the incoming revision failed) ... / gstack auto-update: setup did not finish (bun not found on PATH) ... on Windows
Meaning. From 1.91.89.0 to the release that fixed #3184, the auto-updater
extracted each incoming release with git archive | tar before checking its
hooks. Git for Windows' MSYS tar cannot create the connect-chrome symlink
because it comes before its target in the archive, so every release was held
(~/.gstack/analytics/session-update.log shows the HELD line), and a stale
setup did not finish (bun not found on PATH) line could hide the hold. Your
installed updater cannot deliver its own fix, and the old /gstack-upgrade
can leave conflict markers when a local edit conflicts. The recovery fetches
the new bin/gstack-ff-update from the new revision into a private temp
directory and runs it. It works on a clean install, an install with local
edits that conflict, an install that already has unmerged files, and a
prefix-mode install: regenerable SKILL.md and sections/*.md edits are
re-rendered by setup, and every other edit is kept in a rescue ref (the
command prints it; see shelved edits).
Fix, in Git Bash (use your install directory if it is not
~/.claude/skills/gstack):
cd ~/.claude/skills/gstack && git fetch origin main && d=$(mktemp -d) && git show FETCH_HEAD:bin/gstack-ff-update > "$d/gstack-ff-update" && bash "$d/gstack-ff-update" . FETCH_HEAD; s=$?; rm -rf "$d"; [ "$s" = 0 ] && ./setup
Fix, in Windows PowerShell 5.1 (it runs the same script through Git's
bash.exe, then .\setup.ps1):
Set-Location -LiteralPath "$HOME\.claude\skills\gstack" -ErrorAction Stop; $gb = Join-Path (git --exec-path) '..\..\..\bin\bash.exe'; & $gb -c 'set -f; IFS=; git fetch origin main && d=$(mktemp -d) && git show FETCH_HEAD:bin/gstack-ff-update > $d/gstack-ff-update && bash $d/gstack-ff-update . FETCH_HEAD; s=$?; rm -rf -- $d; exit $s'; if ($LASTEXITCODE -eq 0) { .\setup.ps1 }
In Claude Code, ask it to run the Git Bash form above.
Expected result. The script prints ok <old> <new> or
rescued <old> <new> refs/gstack/rescue/<oid> unmerged=<0|1>, setup
finishes with exit 0, the next session start prints no held line, and later
updates install on their own.
Trying an unreleased fix, and going back. A fix branch can be tried
before it ships; auto-update pauses while a detached revision is checked out.
Replace <branch> with the branch named in the issue.
cd ~/.claude/skills/gstack && git checkout -- 'SKILL.md' '*/SKILL.md' '*/sections/*.md' && git fetch origin <branch> && git checkout --detach FETCH_HEAD && ./setup
Set-Location -LiteralPath "$HOME\.claude\skills\gstack" -ErrorAction Stop; $gb = Join-Path (git --exec-path) '..\..\..\bin\bash.exe'; & $gb -c 'set -f; git checkout -- SKILL.md */SKILL.md */sections/*.md && git fetch origin <branch> && git checkout --detach FETCH_HEAD'; if ($LASTEXITCODE -eq 0) { .\setup.ps1 }
To go back to the released version (this also resumes auto-update):
cd ~/.claude/skills/gstack && git checkout -- 'SKILL.md' '*/SKILL.md' '*/sections/*.md' && git checkout main && ./setup
Set-Location -LiteralPath "$HOME\.claude\skills\gstack" -ErrorAction Stop; $gb = Join-Path (git --exec-path) '..\..\..\bin\bash.exe'; & $gb -c 'set -f; git checkout -- SKILL.md */SKILL.md */sections/*.md && git checkout main'; if ($LASTEXITCODE -eq 0) { .\setup.ps1 }
gstack auto-update: update held (unmerged-files: <n> unmerged file(s) in the install (first: <path>)); an earlier merge, rebase or stash pop left conflicts in the install, and an update would bury them. Fix now (...): cd '<dir>' && git fetch origin && bash bin/gstack-ff-update . '@{upstream}' && ./setup ...
Meaning. Your gstack checkout has unmerged files: conflict markers from a
merge, rebase or git stash pop of your own, or from an auto-updater older
than the fix for this (its merge --autostash left markers in place when a
local edit conflicted). The background updater never moves a checkout with
unmerged files, and it prints no "setup did not finish" fix while they exist,
because setup would run over the markers.
Fix. Run the printed command. It saves everything first, the unmerged
files included, in a rescue ref labelled "needs manual resolution", then
moves the checkout to the release and runs setup. Read the rescue with the
commands under shelved edits. To resolve the
conflict yourself instead, finish or abort your merge (git -C '<dir>' merge --abort, git -C '<dir>' checkout -- <file>); gstack retries at the next
check.
gstack auto-update: update held (autostash-unrecoverable: <reason>); your local edits are where they were and nothing was installed. Look first: git -C '<dir>' status --short. Fix now (...) / autostash-unrecoverable: <reason>
Meaning. bin/gstack-ff-update refused to move the checkout and changed
nothing. The <reason> says why: an untracked file is where the release
puts a tracked one (git refused to update ...: would be overwritten), a
file changed while the update ran, another git command held the index lock
(index.lock exists), or a rescue ref of that name already names another
commit.
Fix. Run the status command the line prints, move the listed untracked
file out of the way (or wait for the other git command to finish), then run
the printed Fix now command. Nothing needs restoring: your edits are still
in the working tree.
gstack auto-update: your local edits in the install (<files>) conflicted with an update, so they were saved in refs/gstack/rescue/<oid> and the install runs the update. ... / rescued <old> <new> refs/gstack/rescue/<oid> unmerged=<0|1>
Meaning. You had edited tracked files in your gstack checkout, and the
release changed the same lines (or you had staged changes or unmerged files).
Instead of leaving conflict markers in the checkout Claude Code runs hooks
from, bin/gstack-ff-update saved your edits as one commit,
refs/gstack/rescue/<oid>, and moved the checkout to the release. The
rescue's first parent is the revision you had, so it holds only your changes;
its second parent holds what you had staged. Edits that did not conflict stay
in place, and regenerable SKILL.md and sections/*.md edits (skill prefix,
gbrain blocks) are re-rendered by setup instead of being rescued. The
session-start line repeats once a day until you delete the ref.
Fix. Inspect first:
git -C ~/.claude/skills/gstack show --stat 'refs/gstack/rescue/<oid>'
To keep your edits, export only your changes and apply them in a scratch
checkout (never with stash apply in the install):
git -C ~/.claude/skills/gstack diff 'refs/gstack/rescue/<oid>^' 'refs/gstack/rescue/<oid>' > ~/gstack-local-edits.patch
git clone https://github.com/garrytan/gstack.git ~/gstack-scratch && cd ~/gstack-scratch && git apply -3 ~/gstack-local-edits.patch
A rescue labelled "needs manual resolution" holds files that were already unmerged: their content can contain conflict markers to resolve. Delete the ref when you are done (this also stops the daily line):
git -C ~/.claude/skills/gstack update-ref -d 'refs/gstack/rescue/<oid>'
gstack setup: refusing to replace <root> for <host>: this runtime directory contains the source checkout setup runs from (<checkout>), so replacing it would delete the checkout. Nothing was changed.
Meaning. Setup installs Codex, Factory, OpenCode, Cursor, Copilot and
Antigravity by building a runtime root (such as ~/.cursor/skills/gstack) and
swapping it in for the old one, and Kiro by writing into ~/.kiro/skills/gstack.
Your gstack checkout lives at that root or inside it, so the swap would delete
the checkout (#3084), and Kiro would link the checkout into itself. Setup refuses before
writing anything. ./setup --status and gstack-doctor suggest the move for
that host instead of ./setup --host <host>.
Fix. Move the checkout out, then run setup from its new place. Setup
prints the exact commands. When ~/.claude/skills/gstack is a symlink to the
checkout, the checkout takes the symlink's place:
rm ~/.claude/skills/gstack && mv ~/.cursor/skills/gstack ~/.claude/skills/gstack
cd ~/.claude/skills/gstack && ./setup --host claude && ./setup --host cursor
Otherwise move it anywhere outside the host's skills directory:
mv ~/.cursor/skills/gstack ~/gstack && cd ~/gstack && ./setup --host cursor
Expected result. Both setups finish with exit 0, and ./setup --status
lists each host as current.
pruned <dir> (<host> is not installed from this checkout): ... / kept <path>: not proven generated (...) / host-render backup: <dir> (...)
Meaning. A gstack checkout keeps skills only for the agents installed from
it, listed in .gstack-installed-hosts in the checkout (#1694). Older installs
rendered every agent's copy, so a global Claude install carried 632 SKILL.md
files (34.7 MB) that Claude Code and Cursor-agent scan. setup removed the copy
for an agent that is not installed from this checkout. Generated files were
moved to the backup directory it printed; links into the checkout were removed;
files it could not prove gstack generated (kept ...) were left in place.
prune.log in the backup lists every path. The same prune runs once from the
upgrade migration. Agents in the record are never pruned.
Fix. Nothing, if you don't use that agent. To get an agent back, install it from this checkout, which records it and renders its skills:
./setup --host <name>
For an instruction-only agent you render by hand (Hermes, OpenClaw, GBrain),
add its name to .gstack-installed-hosts first, then run
bun run gen:skill-docs --host <name>. To restore one file, mv it back from
the backup. A kept file is yours: move or delete it when you no longer need it.
The backup sits in gstack's state root, which no agent scans; delete it once
you are sure you don't need it (about 36 MB for a pre-1.91.65 Claude install).
Expected result. The next ./setup prints no pruned line, and
./setup --status lists the agents you use.
CSO unavailable: its native helper was not built (windows-msvc-compile)
Meaning. Visual Studio's compiler was found but the /cso native helper did not compile. setup prints the first compiler error. It used to say "install Visual Studio".
Fix. Fix the printed compiler error, then re-run ./setup.
CSO unavailable: the native helper's <stage> step failed (...) / CSO publish step failed (...) for <revision>; previous CSO kept: <revision>
Meaning. The /cso native helper is optional. Its build prerequisites were
present, but the build step (compiling the helper) or the publish step
(swapping the new helper into bin/ under a lock) failed. setup used to stop
here (#3071); now it finishes everything else and says which step failed. When
an earlier helper was installed and the failed publish restored it, setup
prints previous CSO kept and /cso keeps using that helper; otherwise /cso
reports not assessed. interrupted means the step was killed before it could
record a result. The outcome is in bin/.gstack-cso-build-result, and
gstack-doctor's cso row reads the same record. The build output is in
bin/.gstack-cso-build.log.
Fix. Read the log, fix what it reports, then retry from your gstack checkout:
bun run build:cso && ./setup
On Windows, setup and the build use PowerShell 7 (pwsh) when it is installed
and fall back to Windows PowerShell 5.1. The root cause of the staged files
vanishing during publish in #3071 is still unknown; attach the log there if you
hit it. CI sets GSTACK_STRICT_BUILD=1, which makes these failures fatal so
build regressions cannot hide.
Expected result. setup prints no CSO line, and the doctor's cso row is
ok.
Docker found at <path>, but native Windows Docker transport is not supported yet; static assessment only. / docker.exe at <path> is outside the trusted install locations (...)
Meaning. On Windows, /cso looks for docker.exe only under the install
folders Windows reports for Program Files, Program Files (x86) and the Windows
directory, by its real path, with no symlink or junction on the way. A
user-writable directory is untrusted, because the Docker child carries
registry credentials; that refusal cannot be overridden.
Even a trusted docker.exe cannot run /cso's isolated containers yet: /cso
admits only a local Unix Docker socket, and Docker Desktop on Windows speaks
over a named pipe. /cso reports this and runs its static assessment only; no
container or runtime check runs.
Fix. For runtime checks, run /cso from Linux or macOS (WSL2 counts as Linux) with a local Docker socket. On Windows, static assessment is the supported mode; if the refusal named a user directory, install Docker Desktop under Program Files.
Snapshot manifest cap exceeded: ... / 64 MiB source cap exceeded: ... / Symlink or special source file: <path>
Meaning. /cso copies the repository into a private snapshot before any audit starts, and refuses the whole run when the snapshot would be incomplete or too large. Each message names the cap that applied:
- Snapshot manifest cap (16 MiB). One entry per tracked or nonignored untracked file; about 50,000 files fit.
- 64 MiB source cap. The full size of every such file outside dependency and VCS directories counts, including files over 1 MiB whose contents are withheld from the audit.
- Symlinks. A tracked symlink anywhere in the repository stops the run.
Fix. No setting raises these caps. Run /cso on a smaller checkout that holds the code you want audited and no symlinks. Add your file count or size to #2993.
INVALID_OUTPUT: Scanner report is malformed, unsupported, or exceeds structural limits. (Semgrep on a real repository)
Meaning. Before the first release after v1.91.101.0, /cso redacted a
scanner's JSON report by splicing an unquoted <REDACTED-...> marker over any
bare number that looked like a phone number or card number, such as Semgrep's
"max_memory_bytes":1145427776 or an epoch timestamp. The report then failed to
parse and the scanner was recorded as not assessed (#3185). Now a redacted
number becomes the quoted string "<REDACTED-...>" and the report parses. A
redaction that would cross JSON tokens still withholds the payload
(REDACTION_FAILED).
Fix. Update gstack (/gstack-upgrade), then run /cso again. If this code
still appears, the scanner itself emitted malformed JSON; report the scanner
and version at https://github.com/garrytan/gstack/issues.
Fixed in. The first release after v1.91.101.0 (#3185).
unsupported argv0 "docker-tools" (OrbStack's docker under /cso)
Meaning. /cso runs the canonical path of docker (symlinks resolved), and
OrbStack's docker is a symlink to one multicall binary, docker-tools, that
decides what to be from the name it was started as. Started under its real name
it refused, so Docker isolation and every scanner were reported missing
(#3187). /cso now starts the same verified binary with the name docker,
including in its cleanup watchdog.
Fix. Update gstack (/gstack-upgrade), then run gstack-doctor; its cso
row should pass with OrbStack running.
Fixed in. The first release after v1.91.101.0 (#3187).
removed the AskUserQuestion preference hook: it breaks Conductor's native AskUserQuestion (#2207). ...
Meaning. In Conductor, setup no longer installs the plan-tune question-preference hook and removes one it added before. The other plan-tune hooks stay.
Fix. To keep it anyway: gstack-config set plan_tune_hooks yes, then ./setup.
Error: browse_extension_id '<v>' is not a Chrome extension ID ... Existing value left unchanged.
Fix. Copy the 32-letter ID (letters a to p) from chrome://extensions, then
gstack-config set browse_extension_id <id>.
Install check (gstack-doctor --check) and the Capy installer
gstack-doctor --check prints one PASS | FAIL | SKIP <id> row per component
and ends with gstack: ok <revision> for=<workflow> (exit 0) or
gstack: fail <id,id> (exit 1). Every FAIL row carries a fix: clause and one
of the codes below. Read the trailer directly; never pipe --check through
tail or another command whose exit status would replace the doctor's
(docs/capy-install.md).
FAIL <id> — <detail>; fix: <command> (COMPONENT_MISSING; ...)
Meaning. A component the selected workflow needs (--for <skill>,
--require <csv>, or by default every host profile installed from this
checkout) is not installed, not built, not current, or not authenticated. The
row names it: claude or codex (a host profile missing or stale), patch
(the Codex render lacks the model behavioral patch), codex-cli (the Codex CLI
is required and absent or not logged in), browse-bundle, browser (Chromium
does not launch or render), cso (the native helper is required and not
built).
Fix. Run the command in the row's fix: clause. For browser, that is
<checkout>/bin/gstack-browser-ensure, which installs Chromium on first use
when the installer skipped it (SKIP browser (lazy; ...)).
Expected result. The row turns PASS and the trailer reads gstack: ok.
FAIL runtime — bun <version> ... (RUNTIME_BELOW_MINIMUM; ...)
Meaning. The Bun on PATH is absent, below gstack's security floor
(1.3.3: older Bun ignores the no-autoload compile flags, see
bun-too-old) or below the supported minimum (1.4.2, the CI pin
and engines.bun). The dashboard (gstack-doctor without --check) warns on
an untested version; --check fails it, because a gate run on an untested
runtime is the incident this check exists for. The default Capy cloud image
ships 1.3.14, so a fresh Capy machine fails here until the installer upgrades
Bun.
Fix. bin/gstack-capy-install upgrades Bun through setup's
checksum-verified recipe before running ./setup. By hand: download
https://bun.sh/install, verify its SHA-256 against GSTACK_BUN_INSTALL_SHA
in bin/gstack-bun-version.sh, then BUN_VERSION=1.4.2 bash <file> and
re-run ./setup.
FAIL pins — bun <version> outside engines.bun >=1.4.2 (package.json:<line>; ...) (PROJECT_PIN_MISMATCH; ...)
Meaning. The target project (--project <path>, default the git toplevel
of the current directory, never the gstack checkout) pins a Bun or Node
version the running one does not satisfy. Pins come from package.json
(packageManager, engines), .tool-versions, .nvmrc, .bun-version and
bun-version: / node-version: lines in .github/workflows/*.yml. A range
(>=1.4.2) and an exact pin are told apart, and a CI pin keeps its platform
lane: a Windows job's pin never fails a Linux run. The dashboard warns; --check
fails. The row is SKIP pins (no bun to evaluate the project pins) when Bun
is absent, never silent.
Fix. Install the pinned version for this platform (the row names each
constraint with its source), or change the project's pin. On Capy the row's
fix is the exact bootstrapper command, bash <skill>/scripts/install.sh
(the same command without --check): it installs the pinned Bun when the pin
names a verified version and ends gstack: ok, or stops with
RUNTIME_PIN_CONFLICT naming the pin to raise.
FAIL revision — installed <version> (requested <ref>) (REVISION_UNMET; ...)
Meaning. --revision <ref> asked the doctor to certify a specific gstack
revision (a VERSION string, tag, branch or commit) and the checkout is at
another. An unmet revision is never ok.
Fix. Move the checkout: <checkout>/bin/gstack-upgrade (the tip of the default
branch; /gstack-upgrade interactively), or
git -C <checkout> fetch origin && git -C <checkout> checkout <ref> && ./setup.
bin/gstack-capy-install --revision <ref> never moves an existing checkout; it
reports gstack: ok <installed> (requested <ref>; run <checkout>/bin/gstack-upgrade to move).
gstack-capy-install: Bun conflict: gstack needs >=<minimum>, <project> pins <constraint> (RUNTIME_PIN_CONFLICT; ...)
Meaning. The installer installs one verified Bun archive: gstack's
supported minimum, or the project's exact pin when that pin is above the
minimum and listed in the message's verified archives:. Here the two do not
meet (for example the project pins bun@1.3.14, or a version gstack has no
verified archive for), so the installer changed nothing and stopped before
./setup.
Fix. Pin a verified version in the project, upgrade gstack to a release
that verifies the pinned version, or install gstack on a machine whose Bun the
project accepts. --project <path> names
the project whose pins apply.
gstack-browser-ensure: NEEDS_SETUP ... (BROWSER_UNAVAILABLE; ...)
Meaning. The first browser skill run on a machine installed without
Chromium (SKIP browser (lazy; gstack-browser-ensure installs on first use))
tried to install it through bun x playwright install chromium under setup's
lock, or Chromium is installed but does not launch.
Fix. Read the installer output above: offline or proxied machines need
network access to the Playwright CDN; on Ubuntu 24.04+ an AppArmor
user-namespace error needs GSTACK_CHROMIUM_NO_SANDBOX=1; then re-run
<checkout>/bin/gstack-browser-ensure. --with-browser on the installer
front-loads the install on machines that want it now.
gstack-capy-install: preflight: <tool> not found (INSTALL_PREFLIGHT_FAILED; ...)
Meaning. The installer checks git, curl, node and jq before
./setup, and installs Bun when it is absent or below the supported minimum.
One of them is missing, or the Bun install script's checksum did not match
GSTACK_BUN_INSTALL_SHA (the download is discarded, nothing runs).
Fix. Install the named tool (apt-get install -y jq, Node from nodejs.org
or nvm), then re-run bin/gstack-capy-install. A checksum mismatch means the
upstream installer changed: update GSTACK_BUN_INSTALL_SHA only after
reviewing the new script.
gstack-upgrade: <cause>; nothing was changed; fix: <command> (UPGRADE_REFUSED)
Meaning. bin/gstack-upgrade moves a checkout only by fast-forward to the
tip of its remote default branch. It refused before changing anything: the
directory is not a gstack checkout, HEAD is detached or on another branch (a
pin), a tracked file is modified, a local commit is not on the remote branch,
the incoming release's Bun floor is above the Bun on PATH, or another updater
holds <state root>/.setup-lock. It never stashes, resets or discards.
Fix. Resolve the named cause with the printed command (commit or move the
listed changes, check out the default branch, move local commits to another
branch), then re-run bin/gstack-upgrade. A reinstall that failed after the
fast-forward is completed by the next run (reinstall pending from <version>).
gstack-upgrade: live gstack sessions hold this installation: pid <p> (<age>m), ...; nothing was changed; fix: ... (UPGRADE_BUSY)
Meaning. Several agents share this checkout (Capy subagents on one
machine, say). Each session start writes a workflow lease,
<git dir>/gstack-leases/<harness pid> (bin/gstack-lease touch, from
bin/gstack-skill-start), and bin/gstack-upgrade refused before changing
anything because a lease whose pid is alive and not in its own process
ancestry exists: fast-forwarding would change gstack under a session
mid-workflow. Leases older than 120 minutes are pruned; a lease whose
process has exited never blocks. The upgrader's own session (the pid chain it
runs under) never counts.
Fix. Wait for those sessions to finish (bin/gstack-lease list shows
every lease with its age and alive|dead), then re-run bin/gstack-upgrade.
When you know they are idle, GSTACK_UPGRADE_FORCE=1 bin/gstack-upgrade
proceeds. At a Capy session start the same refusal appears as
SELF_UPGRADE: skipped (live gstack sessions hold this installation: ...)
and the session continues on the installed revision.
gstack-upgrade: could not reach origin (...) (UPGRADE_FETCH_FAILED)
Meaning. The remote default branch could not be listed or fetched (offline,
proxy, credentials), so --check cannot say whether the checkout is current.
It never reports current without the fetch.
Fix. Check network access to the checkout's origin
(git -C <checkout> remote -v), then re-run.
gstack: fail revision — installed <v> is below the supported minimum <m> (<cause>); fix: ... (REVISION_BELOW_MINIMUM)
Meaning. capy/support.env on the remote default branch declares the
oldest revision whose installer, trailer and fix lines are honest
(GSTACK_MIN_REVISION), and this checkout is older. The Capy skill
bootstrapper reads it from FETCH_HEAD, so even a checkout that predates the
file learns it. --check only reports (fast-forward available); install
mode fast-forwards a clean, unpinned checkout, reinstalls and rechecks under
one lock. The cause names why nothing moved: pinned by --revision, pinned: HEAD is detached, tracked files changed: <files>, diverged: ... or
auto-upgrade disabled (GSTACK_CAPY_AUTO_UPGRADE=0). gstack-doctor --check --min-revision <m> fails the revision row the same way.
Fix. Run the fix clause: the bootstrapper without --check, or
<checkout>/bin/gstack-upgrade. For a pin, drop --revision or pin a
revision at or above the minimum. An interrupted install after the
fast-forward leaves the trailer fail; the next install run completes it.
gstack-capy-skill: owner action — Drive copy is wrapper <n>, minimum <m>; copy <root>/capy/skill/gstack/ (CAPY_WRAPPER_STALE)
Meaning. The Capy skill folder that ran (the bootstrapper's
GSTACK_CAPY_WRAPPER) is older than the checkout's GSTACK_CAPY_MIN_WRAPPER.
The run continues through the checkout's own wrapper (bin/gstack-capy-session);
only a checkout with no such wrapper refuses (gstack: fail wrapper).
Fix. The owner copies <checkout>/capy/skill/gstack/ over the skill folder
(on Capy, the user-scope Drive skills/gstack/). Agents never write it.
INSTALL: fail verdict — <detail>; fix: ... (INSTALL_UNVERIFIED)
Meaning. On a Capy machine (GSTACK_CAPY=1, set by the GSTACK_LAUNCH
line), an unattended or spawned workflow starts only with a passing install
verdict (<state root>/install-verdicts/<sha256 of the checkout path>.json,
named as GSTACK_INSTALL_VERDICT) for this checkout, its current HEAD and a
workflow at least as wide as the one starting: a verdict for --for autoplan
does not cover qa's browser, and a planning-only pins SKIP does not cover a
workflow that runs the repo's tests. gstack-skill-start printed this row and
GSTACK_RESULT: skill=<s> status=refused run=-, exit 3. The same start refuses
an unattended run whose Bun on PATH is below the supported minimum
(INSTALL: fail runtime ... (RUNTIME_BELOW_MINIMUM)) on any host.
Fix. Run the Capy skill installer with --for <workflow> on this machine
and launch with the GSTACK_LAUNCH line it prints.
GSTACK_ALLOW_UNSUPPORTED_RUNTIME=1 admits anyway (printed as
INSTALL_VERDICT: overridden); record it as a deviation.
Browser
[browse] this install's browse CLI (<root>, build <hash>) and the gstack checkout's server bundle (<checkout>, build <hash>) are from different builds, so the server was not started. ...
Meaning. On Windows a host runtime root (~/.codex/skills/gstack and the
other env-var hosts) holds a copy of browse with no node_modules, so its CLI
starts the server bundle in the gstack checkout recorded in .source-path.
The checkout was rebuilt (or updated) without refreshing this runtime root, so
the two builds differ and browse refuses rather than run a mismatched server.
Fix. Re-run setup from the checkout named in the message; it rebuilds and refreshes every runtime root:
cd <checkout> && ./setup
[browse] chain: no flow to run (stdin was empty) (or stdin is a terminal, stdin could not be read (EAGAIN))
Meaning. browse chain with no arguments runs the JSON flow piped to it.
Nothing arrived, so it exits 1 before starting a browser. It used to exit 0 on
Windows having run nothing.
Fix. Pipe the flow, or pass it as an argument:
echo '[["goto","https://example.com"],["text"]]' | browse chain
browse chain 'goto https://example.com | text'
Chromium at GSTACK_CHROMIUM_PATH=<path> failed to launch: ...
Meaning. Headless browse and make-pdf now use GSTACK_CHROMIUM_PATH
(NixOS, macOS 13). That binary could not start.
Fix. Point GSTACK_CHROMIUM_PATH at a working Chromium, or
unset GSTACK_CHROMIUM_PATH to use gstack's bundled one.
Headed Chromium profile <dir> is in use by Chromium PID <pid> (started by PID <ppid>: <name>). ...
Meaning. Headed browse keeps one profile per project
(<project>/.gstack/chromium-profile). A live browser owns this one, and
browse never kills it. ... is locked by PID <pid> on another host (<host>)
means the lock came from another machine through a synced folder.
Fix. Close that browser, or export CHROMIUM_PROFILE=<another dir>. For the
other-host case, stop it there, or remove <dir>/SingletonLock once you are
sure nothing uses it.
[browse] Headed browse now keeps a profile per project ...
Meaning. First use copies your logins from ~/.gstack/chromium-profile
when no browser is using it; otherwise it starts fresh and prints how to import.
browse profiles lists them, browse profiles prune --days 30 removes idle ones.
Blocked: <host> is a cloud metadata or link-local address / ... resolves to a cloud metadata or link-local address
Meaning. Browse refuses 169.254.0.0/16 (including container credential
endpoints), 100.100.100.200, fe80::/10 and fc00::/7 in every numeric spelling,
including after a redirect or a page-driven navigation. The tab is reset to
about:blank and the command fails. This cannot be overridden.
forbidden origin from the sidebar terminal
Meaning. The terminal accepts only gstack's extension. A self-built or forked
extension has a different ID. BROWSE_EXTENSION_ID is no longer read.
Fix. gstack-config set browse_extension_id <your 32-letter id>.
hint: "<word>" looks like a CSS selector
Meaning. Bare browse type <text> types into the focused element. The first
word looked like a selector, so it was typed as text.
Fix. browse type --selector '<sel>' <text> (put text that starts with --
after --).
server-node.mjs not found. Run bun run build (Windows)
Fix. cd <gstack checkout> && bun run build.
[freeze] Hook helpers out of date (partial upgrade?)
Meaning. check-freeze.sh and hook-extract.sh come from different gstack versions.
Fix. cd <gstack checkout> && ./setup (or /unfreeze).
[browse] Could not record the Chromium PID (CDP SystemInfo.getProcessInfo: <error>); browse stop cannot reap a surviving Chromium.
Meaning. For a headless Chromium it launched, browse reads the browser's
process id over CDP so browse stop can kill a browser that outlives the
server. That read failed or took longer than 2 seconds, so this session's
browser is not recorded. Browsers gstack did not launch are never recorded or
killed.
Fix. After browse stop, check for a leftover browser with
ps aux | grep -i chrom and end it with kill <pid>.
No active page. Use "browse goto <url>" first. (after the last tab was closed outside browse)
Meaning. When every tab was closed outside browse (the headed window's tabs
closed by hand, or a page that closed itself), earlier releases failed every
later command this way, status and stop included, until the daemon was
killed (#3167). Now status and stop answer without opening a tab, and any
other command opens one new blank tab first and prints
[browse] warning: every tab was closed outside browse, so this command ran on a new blank tab; open your page again with: browse goto <url>.
Fix. browse goto <url> to open your page again.
Fixed in. The first release after v1.91.101.0 (#3167).
browse: every tab was closed outside browse and a new one could not be opened, because the browser or its context is gone (<cause>); fix: run `browse restart` ... (BROWSE_TAB_UNRECOVERABLE)
Meaning. Every tab was closed outside browse and the browser, or its
context, is gone too, so browse could not open a tab to run the command.
browse status and browse stop still answer.
Fix. browse restart ($B restart inside a gstack skill), then run the
command again.
Fixed in. The first release after v1.91.101.0 (#3167).
Windows
Windows blocked compiled gstack binaries at launch (Smart App Control or another application-control policy, #2595) / bash: .../browse.exe: Permission denied
Meaning. Known issue (#2595, #2124). gstack compiles five binaries on your
machine with Bun: browse, find-browse, design, pdf and
gstack-global-discover. They are unsigned, and a binary built on one machine
never earns the reputation Smart App Control accepts instead of a signature, so
Windows 11 with Smart App Control on refuses to start them. Git Bash reports
that as Permission denied, which looks like a file-permission problem but is
code integrity (PowerShell says An Application Control policy has blocked this file). setup now runs each binary's --version and names the blocked ones and
the skills that need them: the gstack browser fallback (/browse, /qa,
/qa-only, /design-review, /canary, /benchmark, /pair-agent,
/scrape, /make-pdf), the design binary (/design-consultation,
/design-shotgun, /design-html, /plan-design-review) and /retro global.
Every other skill works. gstack-doctor shows the same state in its
browse bundle and binaries rows. The exact Windows error text has not been
verified on a Smart App Control machine; setup prints the raw first line beside
its classification.
Fix. There is no per-file allowlist for Smart App Control. Today's options:
- Run gstack inside WSL (
wsl --install, then install gstack in the Linux distro). WSL runs gstack's Linux build, which Smart App Control does not check. - Or turn Smart App Control off in Windows Security > App & browser control > Smart App Control settings. On Windows 11 with the April 2026 update you can turn it back on later without reinstalling Windows; on older builds turning it off is permanent. gstack's binaries stay blocked whenever it is on.
Signed release binaries are the real fix and are tracked in TODOS.md. After
Windows allows the binaries, re-run ./setup; the message goes away.
Expected result. setup prints no "Windows blocked" line, and
gstack-doctor shows ok for browse bundle and binaries.
setup: -c: line 1: unexpected EOF while looking for matching '"' / setup: -c: line 2: syntax error: unexpected end of file from .\setup.ps1
Meaning. Windows PowerShell 5.1 (powershell.exe, the default PowerShell
on Windows) passes arguments to native programs the legacy way and strips
the double quotes inside them. Until the release that fixed #3130,
setup.ps1 handed Git Bash a bash -c script with double quotes in it, so
bash read a broken script and nothing was installed. PowerShell 7.3 and newer
were not affected.
Fix. Update gstack (in Git Bash: cd ~/.claude/skills/gstack && git pull --ff-only), then run .\setup.ps1 again: its script no longer holds a
double quote. Until then, run ./setup from Git Bash, or .\setup.ps1 from
PowerShell 7 (pwsh).
setup.ps1: argument N is empty; setup takes no empty arguments. Nothing was installed or changed. (SETUP_PS1_EMPTY_ARGUMENT; ...)
Meaning. One of the arguments you gave .\setup.ps1 is an empty string
(for example '' or an unset variable such as $hostName). setup takes no
empty arguments, and the way setup.ps1 hands arguments to Git Bash under
Windows PowerShell 5.1 cannot carry one, so it refuses before running
anything.
Fix. Drop the empty argument, or give it a value:
.\setup.ps1 --host codex
Chromium bootstrap: chromium-install — <bun> x playwright install chromium exited 127 (command not found); install Bun from bun.sh (winget Bun ships no bunx), or skip with ./setup --no-browser (Git Bash) or .\setup.ps1 --no-browser (PowerShell)
Meaning. setup (or bin/gstack-browser-ensure, as BROWSER_UNAVAILABLE <bun> x playwright install chromium exited 127 (command not found)) could
not run the Playwright installer: the shell found no command to run. Until
the release that fixed #3131, setup called bunx, which a Bun installed with
winget does not ship, and reported the failure as exited 127 (offline, proxy, or blocked download?). It now runs bun x through the Bun it
resolved, so exit 127 means that Bun itself, or a command it starts, is
missing. Skills that do not need a browser are installed either way.
Fix. Install Bun from https://bun.sh (it ships bun and bunx), then
run setup again. To install without the browser for now, in Git Bash:
./setup --no-browser
or in Windows PowerShell:
.\setup.ps1 --no-browser
gstack: no Git Bash found to run gstack's bash scripts on Windows (tried ...); fix: ... (BASH_UNAVAILABLE)
Meaning. Windows cannot run a shebang script, so gstack hands its bash
scripts (gstack-wtree, gstack-slug, the hooks' bins, bash -c commands in
regen and pregate selection manifests) to Git Bash. It looks in this order and
the message lists every candidate it tried with the reason it was rejected:
GSTACK_GIT_BASH(or the older nameGSTACK_BASH; when both are set and differ,GSTACK_GIT_BASHwins and a one-line warning says so). A value that does not exist is refused, not skipped.- bash next to
git --exec-path(finds scoop, per-user and portable Git). C:\Program Files\Git,C:\Program Files (x86)\Git,%LOCALAPPDATA%\Programs\Gitand scoop'sgit\current.- A
bashon PATH, only whenuname -sreports MSYS or MINGW. WSL'sbash.exeinSystem32orWindowsAppsis never used: it readsC:\...paths as Linux paths. A PATH bash that is neither (Cygwin, for example) is no longer used either.
Before #3197 these commands spawned the scripts directly, so gstack-restamp
refused with cannot fingerprint the working tree () and gstack-gate-diff
and gstack-evidence silently dropped the fingerprint.
Fix. Install Git for Windows (winget install --id Git.Git -e in
PowerShell), or point gstack at your bash.exe for good. An export in Git Bash
reaches only that shell, so set it where Claude Code reads it:
-
Windows PowerShell 5.1 or PowerShell 7, then restart Claude Code:
[Environment]::SetEnvironmentVariable('GSTACK_GIT_BASH', 'C:\Program Files\Git\bin\bash.exe', 'User') -
Or the
envblock of~/.claude/settings.json:{ "env": { "GSTACK_GIT_BASH": "C:/Program Files/Git/bin/bash.exe" } }
The hooks cache the bash they found in <state root>/cache/windows-bash.json
(keyed by git.exe's path and modification time, rechecked before use); the
override is never cached.
Expected result. gstack-restamp --dry-run prints its diff and
gstack-gate-diff records a candidate ending in a tree id instead of
+nowtree.
Memory and gbrain
Every message below is also in docs/gbrain-sync-errors.md
with more detail, along with the per-page ingest messages (FAILED,
re-queued, quarantined, reconcile progress, kept-on-this-machine notices).
database host unreachable (<code> <host>); your gbrain config is unchanged. Fix: check network or VPN, then re-run /sync-gbrain.
Meaning. A DNS or connection error reaching your gbrain database. It used to look like a broken config, and setup stripped brain features while offline.
Fix. Restore the network or VPN, then /sync-gbrain.
[memory-ingest] ERR: gbrain reported N failure(s) it did not attribute to a staged page ... Refusing to advance state.
What is kept. Nothing is marked saved, so nothing is lost. After three such runs gstack bisects the batch and quarantines the page that breaks it.
Fix. Re-run /sync-gbrain.
[memory-ingest] reconcile: not run (...)
Meaning. After upgrading, gstack re-checks transcripts it had marked saved and re-imports the ones missing from your brain. That check needs a local gbrain.
Fix. /setup-gbrain, then gstack-memory-ingest --reconcile (--dry-run to preview).
[memory-ingest] ERR: another memory ingest (pid <N>) is writing <state file>; not run.
Fix. Wait for it to finish, then re-run /sync-gbrain.
[memory-ingest] ERR: could not save ingest state <path>: <error>.
Fix. Fix permissions or free disk space under your gstack state root, then /sync-gbrain.
[memory-ingest] kept N transcript page(s) for <repo> on this machine: gbrain source <id> ...
On a managed gbrain brain (0.60+), an import lands only in a source bound to a
checkout this host owns, so gstack binds each transcript source to its own
checkout, <state root>/gbrain-transcript-sources/<id>/, before importing.
The partition stays on this machine, unstamped, and imports on a later run when:
- held by another owner: the source is bound to another host or another
directory. gstack never takes it over. Inspect it with
gbrain sources writer status --source <id> --json; the owner releases it (gbrain sources writer transfer prepare <id>on the owning host) or you re-bind it here withgbrain sources set-path <id> <checkout>. - the state root is ephemeral (
GSTACK_EPHEMERAL=1): a binding would point at a directory that vanishes with the machine. Run with a durable root:GSTACK_STATE_ROOT=<durable directory> gstack-memory-ingest --bulk. - bound to gstack's checkout, but it is missing or not active: restore the
checkout from a backup of the state root, or follow
gbrain doctor --json. - could not bind: the message carries gbrain's refusal; inspect the source with the writer status command above.
curated artifacts pushed to git, but gbrain source <id> has 0 indexed pages. Fix: gbrain sync --source <id>, then re-run /sync-gbrain
Fix. gbrain sync --source <id>, then /sync-gbrain.
dream: skipped — the installed gbrain cannot run only the resolve_symbol_edges phase, and the full dream cycle costs about 35 minutes ...
Meaning. /sync-gbrain --dream (and --full's auto-build) now runs only the
call-graph phase. Your gbrain cannot scope it, so nothing ran.
Fix. gstack-gbrain-install, then /sync-gbrain --dream. Or run the full
cycle yourself: gbrain dream --source <id>.
call graph for <source>: unknown: installed gbrain does not expose cycle_freshness
Fix. gstack-gbrain-install.
[gbrain-sync] gbrain source <id>: path unavailable (<path>); gstack skips it.
Meaning. A deleted worktree's code source. gstack never removes it on its own.
Fix. Preview, then remove the ones gstack can prove belonged to this machine:
gstack-gbrain-sync --prune-gone-worktrees --dry-run
gstack-gbrain-sync --prune-gone-worktrees
Eval reports and pass-rates
These come from the paid eval census reports and bun run eval:pass-rates.
The walkthrough for a red census is docs/evals/census-red.md.
eval:pass-rates: unknown flag <flag> / eval:pass-rates: unknown case <id> (not an E2E, judge or paid test file id)
Meaning. The flag or case id is not recognized. The command exits 2 before fetching anything.
Fix. bun run eval:pass-rates --help lists the flags. --case takes a
registry id from test/helpers/touchfiles-data.ts or a paid test file path.
unknown failure_cause "<x>" (written by a newer gstack; update this checkout to read it)
Meaning. A trial record names a failure cause this checkout does not know. A newer gstack wrote it.
Fix. git pull (or rebase your branch onto main), then rerun the command.
--case <id>: registered by <file>, <file>; it needs exactly one
Meaning. More than one paid test file registers the case, so
test-paid-shards.ts --case cannot pick one file.
Fix. Run the red shard's file directly; the report's after a repair: line
prints it: EVALS=1 EVALS_TIER=<tier> bun test <file>.
evidence: <artifact> too large to fetch (<N> MB)
Meaning. The slice artifact holding this red's transcript is over 64 MB, so
eval:pass-rates --run does not download it.
Fix. Download it from the run page, or
gh api repos/<owner>/<repo>/actions/artifacts/<artifact id>/zip > slice.zip.
[headroom] <case> session <key>: max <s> of <s> (<pct>) over <n> sample(s), above the 85% cap. Cut work in the skill or fixture; budgets are never raised ...
Meaning. eval:pass-rates --gate (the weekly report) found a case whose
slowest session used more than 85% of the timeout it armed. It is one timeout
away from a red.
Fix. Cut work in the skill or fixture. Budgets are never raised. Check the
case with bun run eval:pass-rates --headroom --case <id>.
cost unknown (no billing captured for any of <N> trial(s)) / cost $<x> known + <N> of <M> trial(s) cost unknown
Meaning. PTY and Codex sessions record no billing, so their cost is unknown. Only the known sum is shown.
Fix. None needed; this is informational.
· cause provider_stall: no stream event for <N>s ...
Meaning. The session streamed partial messages, then received no event of any kind for at least 120 seconds while a model request was in flight and no tool, permission prompt, hook or subagent was outstanding. Under EVAL_POLICY v1 it is still a failed trial.
Fix. Inspect it with bun run eval:pass-rates --run <run id> --case <id>.
A paid rerun happens only after a repair.
Overlay record with taskCorrect: false and answerError (contract v4)
Meaning. The trial completed with a wrong answer. Under overlay contract v4 that is a valid measurement in the comparison; only an overlay-ON wrong answer fails the case.
Fix. Read answerError in the trial JSON. No rerun is needed.
[detector-corpora] <N> stale entries (refresh from a current census when one fails)
Meaning. Some replay-corpus entries came from an older input series of their case. This is informational; the corpus still replays them.
Fix. When that case goes red in a census, add the new capture to its corpus
and check it with bun test test/detector-corpus-<case>.test.ts.
recommendation substance median <x> < 4 over samples [<a>,<b>,<c>] (boilerplate/weak)
Meaning. The auq-matrix case scores each captured question's recommendation with a 3-sample judge panel; fewer than 2 of the 3 samples reached 4, so their median was below 4.
Fix. Read the logged samples and the captured question. A fix goes in the skill text, followed by one diagnostic run of auq-matrix.
Push guard (redaction)
db.url_with_password HIGH on a default postgres/postgres login whose host is a service name like @db (pre-push BLOCKED)
Meaning. The default postgres:postgres pair is exempt only on localhost
or 127.0.0.1. A service name like @db says nothing about where the URL ends
up deployed, so it still blocks.
Fix. Use postgres://postgres:postgres@localhost:5432/... in local and CI
config, or read the URL from an env var. If the credential is real, rotate it.
Bypass once: GSTACK_REDACT_PREPUSH=skip git push.
If the URL is a reviewed, public dev-only value, list it in
.gstack-redact-allowlist.
.gstack-redact-allowlist (<n> entries) suppressed <m> finding(s) in this push
Meaning. The pushed commit carries .gstack-redact-allowlist at the repo
root. Each line (after trimming; # starts a comment) is one exact matched
span: the whole postgres://USER:PASSWORD@host:port URL for
db.url_with_password, the key for a key pattern, the address for pii.email. A finding is suppressed only when its
matched text equals an entry, so a password alone, a substring, or a different
key still reports and still blocks. The hook reads the file from each pushed
commit, never the working tree, and ignores a file over 64 KiB. Every push that
carries entries prints this line and each suppressed finding's file and line.
It works alongside gstack.redact.allowEmail, which stays a local setting.
Fix. Nothing, if every listed suppression is the reviewed value. Remove an entry that no longer applies; rotate a credential that is real.
pii.ip_public MEDIUM on a four-part version number
Meaning. The line does not declare the value as a version.
Fix. Write it as version: 1.2.3.4, "version": "..." or v1.2.3.4.
MEDIUM findings do not block pushes.
MEDIUM pii.email <file>:<line> from the pre-push hook
Meaning. The pushed lines add an email address that is not yours and not
already in the destination's commit metadata. The hook does not report your
own address (git config user.email), addresses listed in
gstack.redact.allowEmail, or author and committer addresses (mailmapped too)
from the history of the pushed commits, of the remote tip being replaced, and,
for a new branch pushed to a configured remote, of that remote's tracking refs.
An address only another remote knows still reports. If the hook also printed
existing-email suppression was limited for this push, it could not read the
history in 5 seconds, so known authors may be listed too.
Fix. Remove the address, or allow it for this repo:
git config --add gstack.redact.allowEmail <address>. If it is your own
address, check git config user.email. The allowlist only covers pii.email;
it never lets a HIGH finding through.
Unattended runs and artifacts
Workflow commands (gstack-artifact, gstack-gate, gstack-autoplan-timing,
gstack-review-log --findings) end each error line with a code in
parentheses, for example (ARTIFACT_STALE). The codes and anchors come from
lib/result-codes.ts; grep the code, not the prose. Every such command shares
one exit table: 0 ok, 1 fail, 2 usage, 3 refused or needs a flag. The
artifact contract itself is in docs/unattended.md.
ARTIFACT_INVALID_JSON
Meaning. run.json, timing.json or another JSON artifact did not parse.
Fix. Regenerate the run with the tool that writes it. A manifest is never hand-edited; a partial write means the writer was interrupted.
ARTIFACT_UNSUPPORTED_VERSION
Meaning. The artifact's schema_version is one this gstack does not read
(gstack-artifact schema <name> prints the versions it validates).
Fix. Upgrade the consumer (/gstack-upgrade) or regenerate the run with
this gstack.
ARTIFACT_SCHEMA
Meaning. A required field is missing, has the wrong type, or holds a value outside its enum. The message names the file, row and field.
Fix. Compare the row with gstack-artifact schema <name> and regenerate it.
ARTIFACT_MISSING
Meaning. run.json names an artifact that is not in the run directory.
Fix. Copy the whole run directory, not single files. If the file was never written, rerun the phase that writes it.
ARTIFACT_STALE
Meaning. The file's bytes no longer hash to the value run.json recorded,
so the manifest does not describe this file.
Fix. Treat the run as partially rewritten. Regenerate it, or re-bind the file through the tool that wrote it so the manifest is updated with it.
ARTIFACT_PATH_ESCAPE
Meaning. An artifact path resolves outside the run directory: a ..
segment, an absolute path, or a symlink that points elsewhere.
Fix. Keep every artifact inside the run directory under a plain relative name. A run directory is copied as one unit; nothing outside it is part of the contract.
ARTIFACT_MALFORMED_JSONL
Meaning. A JSONL artifact has a line, usually the last, that is not one JSON object. The writer was interrupted mid-append.
Fix. Rerun the phase that writes the file. Do not trim the line by hand; the row it held is lost either way, and the manifest hash would change.
ARTIFACT_DUPLICATE_ID
Meaning. Two rows in one file share an id.
Fix. Ids are run-bound (<run>-...) and unique per file. Regenerate the
rows with the tool that assigns them.
ARTIFACT_UNBOUND_ID
Meaning. A row id does not start with the run id in run.json, so it
could belong to another run.
Fix. Regenerate the row with this run's id.
ARTIFACT_DANGLING_REF
Meaning. A task's findings, blocked_by or depends_on, or a decision's
reference, names an id no artifact in this run defines.
Fix. Write the referenced finding, decision or task first, or drop the
reference. A depends_on entry may also name a pr label that another task
in the file carries.
ARTIFACT_DEPENDENCY_CYCLE
Meaning. Tasks depend on each other in a cycle, so no lane could start.
Fix. Break the cycle in depends_on. The message lists the ids on it.
ARTIFACT_COUNT_MISMATCH
Meaning. A counts value in run.json disagrees with the rows in the file
it summarizes (for example counts.findings: 65 beside 64 rows).
Fix. Regenerate run.json after the last row was written. A count never
replaces the file; the hash and the rows are the record.
ARTIFACT_RUN_INTERRUPTED
Meaning. run.json.status is not terminal, or a voice is still
running/pending. The artifacts describe an unfinished run.
Fix. Resume it (gstack-autoplan resume --out <dir>, PR B-runner) or
consume the artifacts as partial and say so.
ARTIFACT_REVIEWER_MISSING
Meaning. A phase in required_phases is absent from phases, a voice has
no terminal status, or a completed voice has no bound output file (path and
hash) in artifacts.
Fix. Finish the phase. A missing voice is missing coverage; the validator never fills it in as N/A.
ARTIFACT_GUARD_LINE_MISSING
Meaning. The run says session_kind: unattended, but its
review-record.md does not carry the GUARD_NOT_INSTALLED line, so the
record does not state that publication order was unverified.
Fix. Start the run through gstack-skill-start (it prints the line) and
keep the line in the review record.
autoplan guard: not enforced by this host; publication order is unverified (GUARD_NOT_INSTALLED)
Meaning. The autoplan publication hook only executes inside Claude Code.
On every other host, and in every unattended session, the hook is present
but never runs, so phase publication order rests on the snapshot hashes
recorded per phase rather than on the guard.
Fix. Nothing on this host. The line is printed once per run and belongs
in the review record; gstack-artifact validate requires it for an
unattended run. On Claude Code a missing hook file still denies every
guarded call (a broken install must not pass as an absent one).
review-log: status mismatch ... (REVIEW_STATUS_MISMATCH)
Meaning. The row handed to gstack-review-log claims a status,
unresolved, issues_found or critical_gaps that the --findings file
contradicts (for example status: clean beside open findings). The row was
not written.
Fix. Fix the findings file, not the claim, and log again. Omit the claimed fields and let the row be derived from the file.
GATE_REV_STALE
Meaning. The reply names a gate_rev older than the gate list's current
revision, so it may answer items that changed.
Fix. Render the current list (gstack-gate render <file>), read it, and
reply with the printed gate_rev.
GATE_REPLY_UNPARSED
Meaning. Part of the reply matched no gate item or option. Nothing was applied; the unparsed tokens are listed.
Fix. Reply with all, with <id><option> tokens (d3b uc1a), or with
all except <tokens>. A bare yes or no is ambiguous across multi-option
items and is never guessed.
ANSWER_BY_REQUIRED
Meaning. gstack-autoplan answer ran in an unattended run without
--by, or with a value that is neither human nor agent:<id>. The
artifacts must say who answered a gate: an owner typing tokens and an agent
relaying or deciding them look identical in decisions.jsonl otherwise.
Nothing was applied.
Fix. Re-run with --by human when the owner typed the tokens, or
--by agent:<thread or id> when an agent relays or decides. The value is
recorded as answered_by on every decision row the reply answers and in the
review record's gate line.
Queue-aware ship: policy, restamp, tree receipt, ship receipt
gstack-ship-policy, gstack-restamp, gstack-tree-receipt,
gstack-ship-receipt and gstack-evidence bundle|ancestor share the exit
table above and end each error line with a code in parentheses. The policy
file is described in docs/ship-policy.md; the queue flow in
docs/ship-policy.md#stamp-at-merge.
policy source unavailable — fix: git fetch origin <base> --depth=1 (POLICY_SOURCE_UNAVAILABLE)
Meaning. The policy is read from the committed copy on origin/<base>,
and that ref does not resolve here (a shallow or detached gate checkout, or a
fetch that never happened). The tool exits 3 and never falls back to the
working-tree file silently.
Fix. git fetch origin <base> --depth=1, then rerun. To run against the
branch's own file deliberately, pass --policy-from head --allow-repo-commands;
the receipt then says policy: head (unreviewed).
POLICY_INVALID
Meaning. .gstack/ship-policy.json has an unknown key, a wrong type or a
value outside its enum. The message names the key. Unknown keys are errors so
a typo never silently means "default".
Fix. gstack-ship-policy validate lists every problem;
gstack-ship-policy init --explain prints each key, its meaning and default.
POLICY_CONTAINMENT
Meaning. A repo-controlled input escapes its box: an absolute path or a
.. segment in stamp_paths, release_outputs, changelog or mirror; a
glob whose expansion passes 512 regex characters; more than 64 entries in a
list; a release_tool over 512 characters or holding a newline. At write time
the same code names a matched file that resolves through a symlink to outside
the repository.
Fix. Keep paths repo-relative, globs short and lists small. A mirror may
start with .. (a sibling checkout) but may not contain an interior ...
POLICY_EXISTS
Meaning. gstack-ship-policy init found an existing policy file and wrote
nothing.
Fix. Edit the file, or pass --force to replace it with the example.
repo commands not executed (pass --allow-repo-commands) (REPO_COMMANDS_NOT_ALLOWED)
Meaning. The policy declares a command (release_tool), and the tool was
run standalone. File presence never authorizes execution: outside /ship and
/land-and-deploy a repo-declared command runs only with
--allow-repo-commands. The commands were listed and nothing was written.
Fix. Pass --allow-repo-commands when you mean to execute the repo's
code (you already do when you run its tests), or drop the release tool.
QUEUE_STALE
Meaning. gstack-restamp was told the base SHA (--expect-base) or the
merge order (--expect-order) the allocation assumed, and one of them moved:
the base advanced, a predecessor is not merged, or a successor merged first.
The JSON result carries expected, actual and recompute.
Fix. Run the printed recompute command, which recomputes the version
from the current base, then merge with the server-side head condition
(MERGE_CONDITION: sha=<head>).
RESTAMP_VERSION_SOURCE
Meaning. The configured version source (VERSION, .gstack/version-path)
is absent, ambiguous or broken at the base revision or on the branch, so there
is no version to bump from.
Fix. Create VERSION or pin the manifest path in .gstack/version-path
on the base branch. gstack never substitutes 0.0.0.0.
RESTAMP_MERGE_CONFLICT
Meaning. --after <pr> builds the merge of the branch and the
predecessor's head in a throwaway worktree, and that merge conflicts. The
branch is untouched.
Fix. Merge the predecessor (or the base once it merges) into the branch
under /ship Step 3's conflict rules, then run --after again.
RESTAMP_RELEASE_TOOL_FAILED
Meaning. The policy's release_tool exited non-zero inside the staging
worktree. The branch was not written; the journal records the stage.
Fix. Run the tool by hand in the repo, fix it, then rerun gstack-restamp.
RESTAMP_OUTPUT_OUTSIDE_ALLOWED
Meaning. The release tool changed a path outside its allowed-output set
(the version files, lockfiles, the CHANGELOG, stamp_paths and
release_outputs). The restamp refused before writing anything to the branch.
Fix. Add the path to release_outputs on the base branch when it is a
legitimate generated output, or stop the tool from writing it.
RESTAMP_CONFLICT
Meaning. While applying or rolling back, a file no longer held the bytes the journal recorded (someone edited it after the stage ran). Overwriting it would lose that edit, so the restamp stopped.
cannot fingerprint the working tree (<detail>); nothing written is the same
code before anything was staged: gstack-wtree exited <n>: <reason> carries
the helper's own repair (for example a writable TMPDIR), and
gstack-wtree could not start: ... is the Windows bash lookup failing (see
BASH_UNAVAILABLE).
Fix. Read .gstack/tmp/restamp-journal.json, reconcile the named file by
hand, then rerun gstack-restamp (idempotent: a complete stamp is a no-op).
PREDECESSOR MOVED <old> -> <new> — fix: gstack-restamp --after <pr> again (RESTAMP_PREDECESSOR_MOVED)
Meaning. The gate ran on a synthetic tree built against a predecessor
head that is no longer that PR's head, so the gated tree is not the tree that
will merge. The receipt refuses gate-reuse: eligible.
Fix. gstack-restamp --after <pr> again, gate the new synthetic tree, and
read the receipt again.
CHANGELOG_DUPLICATE_HEADING
Meaning. Two sections share one version heading, usually a stamp that
re-headed [Unreleased] beside an entry that already carried the version.
Fix. Merge them into one entry under the new version.
CHANGELOG_TOP_MISMATCH
Meaning. After the stamp, the first entry's heading is not the version in
VERSION.
Fix. gstack-restamp moves the PR's section to the top; if a hand edit
undid that, fix the heading or rerun the restamp.
CHANGELOG_SECTION_MISSING
Meaning. The CHANGELOG has neither an [Unreleased] section nor one
headed by the version currently stamped on the branch, so there is nothing to
re-head.
Fix. Under stamp-at-merge, write the PR's entry under ## [Unreleased];
the restamp turns it into ## [<version>] - <date>.
gate-reuse: not-eligible (<what changed>) — fix: <rerun command> (GATE_REUSE_NOT_ELIGIBLE)
Meaning. Tree identity alone is not gate evidence. The receipt compares the gated identity (tree modulo stamps, runtime pins, lockfile hash, CI workflow hashes, gate command hash, executed selection, runner image) with the current one and names what differs, or the stamp-sensitive gates that must rerun on the final tree.
Fix. Rerun the named lanes with the printed command on the stamped tree,
or pass --force-gate <reason> in /ship to run the full gate again.
EVIDENCE_IDENTITY_UNKNOWN
Meaning. The bundle (or ledger record) lacks a committed tree id, runtime versions, lockfile hashes or the command hash, so no reuse decision can rest on it. Imported assertions without verification count as unknown too.
Fix. Rebuild the bundle on the gate machine after the lanes ran on a
committed head: gstack-evidence bundle --out <file> --label <lane>....
EVIDENCE_BUNDLE_INVALID
Meaning. The bundle is not valid JSON or not gstack-evidence-bundle v1.
Fix. Rebuild it with gstack-evidence bundle; a bundle is never hand-edited.
PREREGISTRATION_NOT_ANCESTOR
Meaning. A preregistration_shas entry in the policy is not an ancestor
of HEAD, so the preregistered eval does not describe this branch.
Fix. Merge or rebase so the commit is in the branch history, or remove the
entry on the base branch. gstack-evidence ancestor <sha>... prints one line
per entry; the ship receipt records the result.
HISTORY_POLICY_VIOLATION
Meaning. The policy says history: merge-only and the plan would squash,
rebase or otherwise rewrite the branch: gstack-ship-policy check-history --plan squash|rebase refuses (exit 3) before anything moves, and without
--plan it reports the violation (exit 1) when the pushed branch is no longer
an ancestor of HEAD.
Fix. git merge origin/<base> and keep every commit; merge the PR with a
merge commit.
RECEIPT_MISSING
Meaning. gstack-ship-receipt read --pr <n> found no
```gstack-ship-receipt block in the PR body.
Fix. /ship Step 18 writes it (gstack-ship-receipt write ... --pr <n>);
for an older PR, write it once by hand with the same command.
RECEIPT_INVALID
Meaning. The block exists but does not validate against the ship-receipt
schema (gstack-artifact schema ship-receipt).
Fix. Regenerate it with gstack-ship-receipt write; never hand-edit the
block.
EXECUTION_UNKNOWN
Meaning. gstack-autoplan recorded a dispatch intent for an attempt (the
prompt was handed to a reviewer) but no terminal record followed: no submit,
no cancellation. The runner was killed, or the parent never came back. The
provider may have finished and charged; snapshot hashes prove which input was
sent, not whether the review completed. The spend ledger keeps the attempt's
reservation.
Fix. If the reviewer finished, submit its result with the same attempt id
(gstack-autoplan submit --out <dir> --phase <p> --voice <v> --result <file> --model <id> --attempt <id>); a late result is still bound. If it did not,
redispatch explicitly with gstack-autoplan next --out <dir> --redispatch <attempt>. resume and next never redispatch an ambiguous attempt on their own.
RUN_LOCKED
Meaning. Another gstack-autoplan process holds <dir>/run.lock (pid,
host and start time are in the file). Every mutating command takes the lock
for its duration, so two writers never interleave.
Fix. Wait for the owner to exit. If its pid is gone (a killed runner),
gstack-autoplan resume --out <dir> takes ownership and reconciles open
attempts; a lock whose pid is alive is never stolen.
RUN_NOT_INITIALIZED
Meaning. The --out directory has no run.json written by
gstack-autoplan, so submit, status, answer and resume have nothing to
act on.
Fix. Start the run with gstack-autoplan next --plan <file> --out <dir>;
the first next initializes the run (restore point, active plan, manifest).
PHASE_NOT_AWAITING
Meaning. submit named a phase or voice that is not waiting for a result:
the phase is already closed, not yet opened, or the voice already has a bound
result.
Fix. Run gstack-autoplan status --out <dir> and submit for the attempt
the last next printed (ATTEMPT: ... phase=<p> voice=<v>).
ATTEMPT_MISMATCH
Meaning. The --attempt id is not the open attempt for that phase and
voice, or that attempt was already submitted. A result is bound exactly once;
a second submit of the same attempt is refused rather than overwriting the
first.
Fix. Pass the attempt id printed by next for this voice. To replace a
bound result, reopen the phase through answer (a scope-changing choice) or
start a new run.
RESULT_RECEIPT_MISSING
Meaning. The result file does not begin with the full-read receipt the
prompt asked for: INPUT: <phase> <snapshot sha256>. Either the reviewer did
not read the prompt file through EOF, or the result belongs to a different
snapshot (the plan changed between dispatch and submit).
Fix. The reviewer's first line must be the INPUT line from its prompt
file. If the plan changed, run next again for a fresh snapshot and
redispatch; a result for an older snapshot is never bound to the current phase.
RESULT_FINDINGS_MISSING
Meaning. The result has no valid canonical findings block. Every reviewer returns, beside its prose, one fenced block:
```gstack-findings
{"severity":"High","title":"...","file":"lib/x.ts","line":12,"fix":"..."}
```
one JSON object per line with at least severity (Critical, High,
Medium, Low, Informational) and title. An empty fence means the
reviewer found nothing; a missing fence or a malformed row means the result
cannot be reconciled and is refused.
Fix. Ask the reviewer for the fence (the prompt names it), or add the rows from its prose yourself and submit again. Reconciliation runs on these rows, not on the prose; a prose-only review has no completeness check.
MODEL_FAMILY_CONFLICT
Meaning. The outside voice's model is in the same family as the native reviewer (both Anthropic, both OpenAI, ...), or its family cannot be determined from the id. Two voices from one family are one voice; the record says which was which.
Fix. Run the outside voice on a model from another family and pass its id
with --model. Families recognized: anthropic, openai, google, xai, deepseek,
qwen, zai, moonshot, meta, mistral, minimax. An id the table cannot classify
is refused; name the model fully (openai/gpt-6-astra, not astra).
SPEND_CAP_EXCEEDED
Meaning. spent + reserved + next_estimate would exceed --spend-cap.
The ledger (<dir>/spend.json) counts unsettled reservations and unknown
charges at their reservation, so a crashed attempt keeps its cost held until
it is settled or released.
Fix. Raise --spend-cap, settle attempts whose actual cost is known
(submit --usage-usd <n>), release attempts that never ran (gstack-autoplan release --attempt <id>), or stop here. Host-dispatched subagent spend is
unknown, never zero.
CONSENSUS_MISSING
Meaning. next for the Eng phase found a closed prior phase without its
consensus record (<phase>-consensus.md plus its reconciliation rows), so the
Eng prompt would be sent without that phase's summary.
Fix. gstack-autoplan resume --out <dir> re-closes the phase through
reconciliation. The Eng dispatch is refused until every closed phase has its
summary; it is never sent partially.
OUTSIDE_RUNNER_UNAVAILABLE
Meaning. gstack-outside-voice run --runner <r> cannot run here:
codex-cli found no codex binary or no login, api found neither
OPENAI_API_KEY nor ANTHROPIC_API_KEY for the model's family, or
host-subagent was called without --result <file> --model <id>. When
another runner is configured (--fallback or GSTACK_OUTSIDE_RUNNER), the
line reads codex-cli unavailable; using <runner> and the review proceeds on
that runner; it is a full outside voice, not a degraded one. With no other
runner, this is missing coverage.
Fix. gstack-codex-login logs Codex in from OPENAI_API_KEY
(unattended machines); or set the API key and pass --runner api; or run the
review on a cross-family subagent and submit its file with --runner host-subagent --result <file> --model <id>.
REALITY_ROW_MISSING
Meaning. gstack-plan-reality check --phase <p> <review> found no
REALITY: <row> … line for a row the review must carry (every tier 1 row:
premise-table, already-done, surface-check, binding-decisions,
numbers, deferred-asks, pr-count; plus the tier 2 rows whose scope was
passed with --scope), or a tier 1 row was marked n/a. The check prints
PLAN_REALITY: phase=<p> rows=<n>/<m> missing=<ids> verdict=<v> and exits
1. In an unattended run the verdict is incomplete and the phase does not
close; interactively it is repair: produce the missing rows and re-check, at
most twice, then warn at the gate with each row still missing.
Fix. gstack-plan-reality rows --phase <p> [--scope <csv>] prints each
row's check. Work it and end the review with one line per row,
REALITY: <row> pass|finding <summary> <file:line>.
REALITY_RECEIPT_MISSING
Meaning. A REALITY: line names its row and status but carries no
receipt. A receipt is path:line, path:line-line or a commit sha; it is what
makes the row a check rather than a claim.
Fix. Add where the check was made to the line, for example
REALITY: already-done pass 0 hits since 2026-10-09 for bin/gstack-doctor 91bbd9e
or REALITY: surface-check finding bun upgrade --version absent on 1.3.14 bin/gstack-capy-install:52.
Pre-gate: generated-file registry, gstack-pregate, issue links
Both tools read their pin file (.gstack/generated.json, .gstack/pregate.json)
from origin/<base>, never the working tree, and run repo-declared commands only
under --allow-repo-commands or inside /ship. Details: docs/pregate.md.
REGEN_REGISTRY_INVALID
Meaning. .gstack/generated.json is not valid JSON, has an unknown key, a
missing id/inputs/outputs, or a duplicate id.
Fix. gstack-regen validate prints every error with its JSON path; compare
with gstack-regen init --explain and fix the file on the base branch.
REGEN_CONTAINMENT
Meaning. A registry glob escapes the repository (.., absolute, more than 64
per list) or a command is not one line under 512 bytes.
Fix. Repo-relative globs only; one line per command.
REGEN_STALE
Meaning. A registered output differs from what its command produces in a
scratch git worktree of the current tree: the output is stale or hand-edited.
Fix. Run the printed command (gstack-regen write --only <id> does it in
place) and commit the outputs. Never hand-merge a generated file: on a conflict
take either side, then regenerate.
REGEN_COMMAND_FAILED
Meaning. A registered command exited non-zero in the scratch worktree.
Fix. Run it by hand in the repository and read its output; the scratch
worktree has no node_modules of its own, so a command that needs them must
reference the repository's (gstack's entries do through bun run).
PREGATE_CONFIG_INVALID
Meaning. .gstack/pregate.json has an unknown key, a wrong type, a glob
that escapes the repository, a runner without {files}, or secrets in warn.
Fix. gstack-pregate validate. secrets and required-lane obligations
cannot be downgraded; every other check id in warn is honoured only from the
base branch's copy.
PREGATE_SECRETS
Meaning. gstack-redact found a HIGH finding, or a MEDIUM finding in the
secret category, in the lines this branch adds.
Fix. Rotate the credential, remove it from the added lines (history included), rerun. Not downgradable.
PREGATE_STRAYS
Meaning. A file was added at the repository root, in a new top-level
directory, or under a scratch name (zz-*, tmp*, scratch*).
Fix. Delete or move it. An intentional root file goes in strays.allow.
PREGATE_LITERALS
Meaning. A changed exported constant (or a named timeout/retry literal) still appears with its old value in a test file the branch did not update — nightly- and Heavy-only files included.
Fix. Update every listed file:line, or keep the old value.
PREGATE_NO_LANE
Meaning. A touched file maps to no test through static imports, repo-path
string literals, the bin/<name> → test/<name>*.test.ts convention or a
dependencies declaration. That is a failure, not a pass.
Fix. Add a test that names the file, or declare its lane (an explicit []
for files with no behaviour, such as docs) in .gstack/pregate.json. --explain
prints every mapping and why. For a file with no lane it also prints the import
specifiers in test files that resolved to no repo file (for example a
tsconfig.json paths alias such as @/lib/x whose target is missing) and any
extends hop that was not followed (only relative extends, at most 4 hops). If
a test does cover the file, declare it under dependencies; if the resolver
should have found it, report the specifier as a resolver gap.
PREGATE_LANE_FAILED
Meaning. A local lane the touched files select exited non-zero under
set -o pipefail, or reported zero tests (ZERO-RUN).
Fix. Read the printed lane log, fix the failure, rerun
gstack-pregate --stage tests. A | tail-hidden exit is still an exit.
PREGATE_INCOMPLETE
Meaning. A check timed out, its tool is unavailable, repo commands were not allowed, or the working tree changed while the check ran. Incomplete is never pass and blocks publication.
Fix. Pass --allow-repo-commands, install the tool, raise --timeout, or
keep the tree still, then rerun.
PREGATE_STALE
Meaning. pregate.json was recorded against a different working-tree
fingerprint than the one being published (the final stamp, a regen, any edit).
Fix. Rerun gstack-pregate after the final generation and stamp; Step 18
refuses to write the ship receipt otherwise.
PREGATE_REMOTE_UNCLEARED
Meaning. A requires-remote obligation (a platform lane this machine cannot
run, named by workflow.yml/job) has no receipt.
Fix. Dispatch that job, then
gstack-pregate clear --remote <workflow.yml/job> --run-url <url> (or
--bundle-lane <label> from an evidence bundle). The ship receipt carries each
obligation with its receipt.
ISSUE_UNREADABLE
Meaning. An issue named by a Fixes #N/Closes #N trailer or --issue N
could not be read through gstack-issue-guard.
Fix. Check gh auth status and the number. Until it reads, the link is
rendered as Refs #N, never Fixes.
Eval plans, lane ownership, evidence verification, the owner brief
Codes printed by bin/gstack-eval-plan, bin/gstack-lane-check,
gstack-evidence verify, bin/gstack-owner-brief, bin/gstack-contributor-mode
and bin/gstack-banned-terms (plan E1–E3). Each ends an error line in
parentheses; grep the code.
PREREG_INCOMPLETE
Meaning. gstack-eval-plan prereg --check <file> found a required
preregistration section missing or empty: ## Bars, ## Arms, ## Decoys,
## Stop rules or ## Held-out exposure (every held-out set the plan
touches, its prior exposure and the reservation on it).
Fix. Fill the named sections before any priced call. A plan that cannot name its stop rule or its held-out exposure is not preregistered; the pilot refuses to start without a passing check.
EVAL_PRICE_MISSING
Meaning. An arm's model has no row in lib/pricing.ts, so the admission
estimate would be $0, which the ledger would admit without limit.
Fix. Add the row from the provider's pricing page (USD per million tokens
with as_of), or drop the arm. The table prices standard text tokens only;
see the excluded billing categories in lib/pricing.ts.
EVAL_DRY_RUN_FAILED
Meaning. The $0 stub-model dry run over the real input file found a
problem the priced run would have paid for: a row without the fields the
prompt needs, an output budget too small for a reasoning model, or no retry
prompt text.
Fix. Repair the named rows or flags (--output-budget, --retry-prompt)
and rerun gstack-eval-plan dry-run. The checklist it prints is the record
the pilot requires.
EVAL_ARMS_UNRESOLVED
Meaning. gstack-models resolve --role eval-arms produced no arm and the
repo policy document fallback (--policy-doc, default the first of
docs/model-policy.md, MODEL_POLICY.md, docs/MODELS.md) named no priced
model.
Fix. Pass --arms <model,model>, point --policy-doc at the document
that lists the project's allowed models, or fix the model policy config.
EVAL_BUDGET_EXHAUSTED
Meaning. The ledger refused the next reservation: spent + reserved +
unknown charges + the estimate exceeds the cap (SPEND_CAP_EXCEEDED
underneath). The run stops with status budget_exhausted; nothing is retried.
Fix. Read the spend table (gstack-eval-plan pilot report), settle or
release the open attempts, then raise --cap on purpose or stop.
LANE_CONFLICT
Meaning. gstack-lane-check found a planned file that an in-flight
branch or pull request already changes; the line names the PR number, the
branch and the hunk count.
Fix. Give the file one owner: wait for that PR to merge, stack this lane on its branch, or drop the file from the plan. Two lanes editing one file is the merge-conflict incident the check exists to prevent.
LANE_REF_UNAVAILABLE
Meaning. A --branch or --pr could not be resolved: the ref is not
fetched, or gh is missing or not logged in.
Fix. git fetch origin <branch>, or gh auth status. An unresolved lane
is reported as such and never counted as clear.
EVIDENCE_UNVERIFIED
Meaning. gstack-evidence verify <bundle> could not verify a lane claim
on this machine: the committed tree differs from the bundle's, the recorded
log is absent or its hash differs, or the probe was not rerun.
Fix. Check out the bundle's commit and run --rerun to execute the lane
command here, or treat the claim as an imported assertion that never counts
toward gate reuse.
BANNED_TERM_FOUND
Meaning. The diff adds a term listed in .gstack/banned-terms.json
outside that term's allowed_paths. The finding names the term, the file and
line, and the allowed locations.
Fix. Remove the term from the added line or move the text to an allowed
path. /review and /document-release run this check on every diff.
BANNED_TERMS_INVALID
Meaning. .gstack/banned-terms.json is not valid: terms must be a
non-empty list of strings, and each allowed_paths entry must be a plain
repo-relative glob (no .., no absolute path, no symlink escape).
Fix. gstack-banned-terms init writes a commented example with every key
at its default; gstack-banned-terms validate checks the file.
CONTRIBUTOR_SOURCE_UNAVAILABLE
Meaning. gstack-contributor-mode detect found no maintainers list in
.gstack/ship-policy.json and could not ask the collaborators API (gh
missing, not logged in, or no PR author resolvable), so the mode is off and
source=off is recorded.
Fix. Add maintainers to the ship policy (the explicit list always wins),
run gh auth login, or pass --contributor to force the mode for this review.
BRIEF_INPUT_MISSING
Meaning. gstack-owner-brief render --out <dir> needs gate.json,
decisions.jsonl and findings.jsonl in the run directory; one is missing.
Fix. Reach the gate first (gstack-autoplan next --out <dir> until
gate_pending); the brief renders what the gate wrote.
DRAFT_DIRECTION_UNRESOLVED
Meaning. The phase-close rewrite found a draft direction stands until the owner decides line whose decision id is still pending, so it was left as is.
Fix. Answer the gate for that id, then rerun gstack-owner-brief close;
only decided lines are rewritten, each to its chosen option and the date.
warning: left <n> locally edited file(s) with CRLF line endings that gstack pins to LF
Meaning. Git for Windows checks out with core.autocrlf=true, so gstack's
nested shell scripts (skill bin/ helpers, the Claude hook shims,
lib/cso/images/*, scripts/app/*) were written with CRLF until
.gitattributes pinned them to LF (#3110). Git Bash runs them anyway, but a
Linux bash reading the same checkout (a synced tree, WSL, a bind mount) fails
with set: pipefail: invalid option name, and a hook that fails that way
blocks the tool call it guards. Pulling the new rule does not fix existing
copies, because git never rewrites a file whose content did not change. Setup
finds those files with one git ls-files --eol pass and checks them out again
as LF. It leaves a file you edited locally alone and names it in this warning.
The same section covers skipped the CRLF repair (another setup held the
install registry lock) and the CRLF repair could not rewrite every file.
Fix. For edited files, stash and restore them; git writes them back with LF and keeps your edits:
cd ~/.claude/skills/gstack && git stash && git stash pop
If the repair was skipped, re-run ./setup once the other setup finishes. If
it failed, git checkout -- . && ./setup in the checkout discards local edits
and rewrites every file. Use your install directory if it is not
~/.claude/skills/gstack (./setup --status prints it). Expected result: the
next ./setup prints no CRLF warning.
Field wave lane CD: pregate guards, regate, sealed output
PREGATE_GUARD_FAILED
Meaning. A guard from the base branch's .gstack/pregate.json (a repo
command such as a serial-test manifest check or a CLI golden test) matched a
file the branch touched and exited non-zero. Guards cover what the import graph
cannot see: manifests, subprocess goldens, registries. They cannot be
downgraded to warn.
Fix. Read the guard's lines in the table (exit code, output tail, log path)
and do what its fix line says, commit, then rerun
gstack-pregate run --allow-repo-commands. A lane reports done only after this
passes and carries the PREGATE: line in its handoff.
MIGRATION_COLLISION
Meaning. gstack-pregate regate read migrations (dir, id) from the
base branch's .gstack/ship-policy.json and found a migration number this
branch adds that the base branch already has, or that another open PR adds
under a different file name. Both PRs would pass their own gates and the
second merge would break the schema order. When the open-PR lookup cannot
finish (gh missing or failing, 1000 open PRs, a PR whose file list GitHub
truncates and cannot be fetched in full) the line says
MIGRATION_COLLISION: unknown (...) and REGATE: not-needed is withheld.
Fix. Take the next free migration number when this PR is next to merge,
rename the file and its id, commit, then rerun
gstack-pregate regate --against <gated-head> --bundle <file>. The renamed
migration is a code change, so the gate reruns.
SEALED_OUTPUT_REFUSED
Meaning. The repo declares a sealed policy in .gstack/pregate.json
(base branch ∪ the pushed commit) and a commit this push publishes adds a
sealed path or transcript (binary, empty and later-deleted files included), or
adds a line with a sealed-answer marker, a seed or a local home path, and the
destination is public or its visibility is unknown (a new or unreadable
repository counts as public). An unreadable or malformed sealed policy refuses
too. The managed pre-push hook and gstack-pregate sealed print one line per
finding: rule, file[:line], commit. They never print the matched text.
Fix. Remove the output from every published commit before the branch
reaches the public repository (a later commit that deletes it still publishes
it). A reviewed false positive gets one line in the base branch's
.gstack/sealed-allow.tsv: rule<TAB>path<TAB>sha<TAB>reason. A private
destination sets "visibility": "private" under sealed in the base branch's
.gstack/pregate.json. Capy's pull-request tool may not run local hooks, so
run gstack-pregate sealed yourself before any push or PR creation.
EVAL_GATE_UNPRICED
Meaning. gstack-eval-plan price-gate found no call-plan file for the paid
gate: no --plan-file, no price_file in the base branch's
.gstack/pregate.json, or the file has no GSTACK_EVAL_CALLS lines. (A
declared price_file missing from HEAD is EVAL_PLAN_STALE, not this.) A gate whose cost is unknown cannot
be approved as priced.
Fix. The consuming repo commits a call-plan file in the format of
docs/eval-calls.md and names it in price_file. Only when the
owner accepts an unpriced gate, rerun with --allow-unpriced "<reason>": it
exits 0, prints GATE_UNPRICED_OVERRIDE: <reason> (owner-authorized override; not authorization to spend) and records the reason in the receipt.
EVAL_GATE_OVER_CAP
Meaning. The gate's priced total is above --cap-usd, or one call's
reservation (its share of the input plus max_out_tokens) is above --cap-usd,
or above --smoke-cap-usd for a scope=smoke row. A provider refuses a
reservation the cap cannot hold, so the run would fail after spending.
Fix. Cut questions, arms or max_out_tokens in the call plan and
regenerate it, or raise the cap deliberately with the owner. Never split calls
to slip under it.
EVAL_PLAN_STALE
Meaning. The call-plan file prices something other than what is being
approved: its GSTACK_EVAL_GATE command is not the exact --gate string, its
GSTACK_EVAL_HEAD is behind HEAD with files other than the plan file changed
since, an input's sha256 at HEAD differs from its GSTACK_EVAL_INPUT line, or
the base policy's price_file is not in the HEAD commit. --allow-unpriced
does not override it.
Fix. Regenerate the plan file at HEAD for the exact gate command (the
repo's own producer), commit it, and rerun gstack-eval-plan price-gate.
PLAN_SECTIONS_INVALID
Meaning. gstack-autoplan next reviews only the plan's ## Implementation plan section and keeps the ## Review record private, so the plan needs
exactly one non-empty Implementation plan section followed by one Review record
section, both outside code fences. A plan with neither heading is wrapped
automatically; one heading alone, duplicate or reversed headings, or an empty
section is refused before anything is written. The error names the problem.
Fix. Add the missing heading, or rerun the first next with --scaffold:
it writes <out>/plan-source.md with the missing heading added, reviews that
copy and prints SCAFFOLDED:. Your plan file is never edited. Duplicate,
reversed or empty sections are ambiguous and must be fixed by hand.
PLAN_AMEND_PENDING
Meaning. The owner accepted at least one plan change at the gate (each was
printed as an APPLY: line), and the run's plan has not absorbed it, so a rerun
would review the old plan. next refuses until every accepted change is
incorporated or withdrawn; export refuses a complete run with one pending.
Fix. Edit the plan, then gstack-autoplan revise --out <dir> --plan <file> --incorporates <decision id,...>. revise binds each named id to the old and
new Implementation-plan hashes and refuses when the Implementation plan did
not change; an id you do not name stays pending. To drop an accepted change
instead, gstack-autoplan revise --out <dir> --withdraw <decision id> records
the withdrawal in the journal.
AMENDMENT_UNKNOWN
Meaning. gstack-autoplan revise --incorporates or --withdraw named a
decision id that is not a pending amendment: the owner never accepted it as a
plan change at the gate (for example a gate answered before gstack recorded
amendments), or it was already incorporated or withdrawn. Nothing was changed.
Fix. Name only the ids the refusal lists after pending:. With none
pending, revise without --incorporates:
gstack-autoplan revise --out <dir> --plan <file>.
GSTACK_VERSION_CHANGED
Meaning. run.json.gstack_version names a different gstack than the one
running. For an older run (or one written before the field existed) this is a
notice: the command journals gstack_version_changed, runs the forward
migrations (for example rewriting decision ids minted before 1.91.95) and
continues. For a run written by a newer gstack the command refuses, because an
older runner cannot know the newer state.
Fix. Nothing for the notice. For the refusal, upgrade gstack
(bin/gstack-upgrade) on this machine and rerun the command.
ARTIFACT_GATE_UNSETTLED
Meaning. gstack-artifact validate found a run whose status is complete
while an approval item of its last gate revision is still pending, or an
accepted plan change (run.json.amendments) is still pending. The runner never
writes that state; it comes from a hand edit or an older runner.
Fix. Answer the gate with gstack-autoplan answer until no approval is
pending, and revise --incorporates or --withdraw each pending amendment.
ios-qa launch: GSTACK_IOS_LAUNCH_ENV ... so the app was not launched (<detail>); fix: ... (IOS_LAUNCH_ENV_INVALID)
Meaning. gstack-ios-qa-daemon forwards GSTACK_IOS_LAUNCH_ENV to
xcrun devicectl device process launch --environment-variables whenever it
launches the app under test. The value must be a JSON object whose keys are
environment variable names and whose values are strings, and it may not set a
GSTACK_ variable (that namespace belongs to the debug bridge). Anything else
(invalid JSON, an array, a number value, a GSTACK_ key) refuses the launch,
because launching without the variables an env-gated debug bridge needs would
look like a working app with no bridge. The detail names the key and its type,
never a value. Added in the release after v1.91.101.0.
Fix. In the shell that starts the daemon (macOS Terminal or Claude Code on the Mac with the device):
export GSTACK_IOS_LAUNCH_ENV='{"DEBUG_BRIDGE":"1"}'
or unset GSTACK_IOS_LAUNCH_ENV, then restart gstack-ios-qa-daemon.
Outside voice unavailable: <cause>. Fix: <fix>. Continuing to ... (OUTSIDE_VOICE_UNAVAILABLE)
Meaning. /plan-ceo-review, /plan-eng-review or /plan-devex-review ran
without a second opinion. The outside provider (Codex, or Claude Code under the
Codex host) did not run or failed, and the native fallback could not supply a
bounded review either. That fallback dispatches the built-in Plan subagent in
the background next to a five-minute background timer, and stops whichever is
still running when the other ends. The cause names which part was missing:
Plan agent type not offered, TaskStop not offered, background tasks unavailable, no complete report within five minutes, or a failed status.
The review-log row carries the same cause as unavailable_reason. Claude Code
2.1.277 removed the TaskOutput tool the fallback used before, so on older
gstack every native fallback on current Claude Code ended here.
What is kept. The native plan review itself is complete. The row is
recorded with status: unavailable, never as a clean review; a late or partial
reviewer result is not counted.
Fix. Repair the provider the line names: install it, or run codex login
(in your terminal or Git Bash). For the native fallback, run Claude Code 2.1.277
or later with background tasks enabled (CLAUDE_CODE_DISABLE_BACKGROUND_TASKS
unset, not in bare mode). Then re-run the review.
Expected result. An OUTSIDE VOICE (...) report, or CODEX SAYS (plan review — outside voice) when the provider runs.