diff --git a/docs/designs/FIX_WAVE_2026_10_06.md b/docs/designs/FIX_WAVE_2026_10_06.md new file mode 100644 index 000000000..105ea0a71 --- /dev/null +++ b/docs/designs/FIX_WAVE_2026_10_06.md @@ -0,0 +1,1368 @@ + +# gstack fix wave (October 6, 2026): Codex works on macOS again, no shell injection, honest failures + +## Implementation plan + +### Context + +Base: origin/main `55f8151` (v1.91.30.0): PR #3033 (Bun 1.4.2, honest eval reports) has merged, and the branch merged it at `9207fab`. While this review ran, the branch gained A1 (`f0531a2`), A3 (`a853e41`), A2 (`597e483`), a Codex watchdog fix (`2e1d3e7`), A4 (`ffde420`) and A5 (`e77c532`); CEO-18 under "Accepted review amendments" lists the rework each still owes. #3033 also edits `browse/src/cli.ts` (B2, B10), `lib/cso/process.ts` (B5), `lib/redact-patterns.ts` (B3), `.github/docker/Dockerfile.ci` and `.github/workflows/free-tests.yml` (A1's zsh install), `docs/troubleshooting.md` (A2's anchor), `docs/TESTING_INTERNALS.md` (A6), `review/SKILL.md.tmpl`, `TODOS.md` and the release files, so those hunks are reconciled at that merge. + +Triage on 2026-10-06 covered every issue and PR opened since the October 3 wave triage (#3018 to #3052, plus #3012 and #3016). Every claim below was checked against main with a probe or a code read; contributor code was read as evidence only. Nothing is merged from a contributor branch. Each fix is rewritten here, with `Co-authored-by` credit for the contributors whose diagnosis or design it uses. + +The severe items share two patterns: + +1. **Shell assumptions that break real users.** Sourced helpers assume bash (macOS default is zsh, and Claude Code's Bash tool runs it), and reply templates put untrusted text inside double quotes, where backticks run as commands. +2. **Failures reported as the wrong thing.** A Codex usage limit reads as "inconclusive; proceeding", an oversize autoplan journal reads as a retryable identity failure, a shell-built autoplan init reads as "init missing", and a Windows `browse chain` with piped input exits 0 having run nothing. + +### Release promises (the finish line) + +- **P1. Codex outside voice works from zsh.** Every sourced gstack helper locates itself correctly when sourced by bash or zsh, from any working directory. A free test sources each one under both shells, and CI installs zsh so the test cannot skip there. +- **P2. Untrusted text never runs as shell.** No generated skill passes reviewer, diff or error text to a command inside double quotes. A lint fails on the pattern. +- **P3. Every failure names its real cause and its real fix.** Codex quota, oversize journals, shell-built autoplan inits, Windows chain input, Windows Docker and Windows browse dependencies each get a specific message with a fix line, documented in `docs/troubleshooting.md` or `docs/autoplan-guard-troubleshooting.md`. +- **P4. A red paid eval gets measured and fixed, not rerun blindly.** /ship runs the measure-then-fix loop from #3034 (approved 2026-10-05), and the weekly periodic red (#3038) is fixed with that loop. + +### Tier 1: must ship + +**A1. #3024: sourced helpers break under zsh (PRs #3027, #3049, #3052).** Probe on main: `zsh -c 'source bin/gstack-codex-probe && _gstack_codex_select_model exec'` prints `Module not found "/../scripts/resolve-codex-generation-model.ts"` and `CODEX_MODEL: invalid`, so since v1.91.16.0 every Codex outside pass on macOS zsh reports `model_unusable`. `bin/gstack-egress-lib.sh` resolves its directory to the caller's cwd under zsh (probe: prints `/tmp`), so receipts and the state root are looked up in the user's project. +- Fix: one self-locate idiom, resolved once at source time: `BASH_SOURCE[0]` under bash, `${(%):-%x}` under zsh (inside `eval` so bash never parses it), else the default install path. Apply it to `bin/gstack-codex-probe` (5 sites) and `bin/gstack-egress-lib.sh`. `bin/gstack-paths` is executed, never sourced, so it keeps `BASH_SOURCE` under its bash shebang and is outside the sourced-helper guard. Keep the probe's "no command execution at source time" rule (parameter expansion only). +- Rejected alternatives: `$0` (#3049) is the function name when read inside a function and `zsh` under `zsh -c`; `functions_source` (#3027, #3052) is rejected for simplicity (CEO-28), not version support. +- Class guard: a free test enumerates every file the generated skills `source` or `.` (derived from the rendered SKILL.md and section files, not a hand list), sources each under bash and zsh from an unrelated cwd, and asserts the resolved directory and one real call per helper (model resolver, state root, egress home). A lint fails on `BASH_SOURCE` in a sourced helper outside the shared idiom. CI installs zsh in the free lane and the Docker image; under `CI=true` a missing zsh fails the test instead of skipping it. +- Credit: @marcosdetry (report and diagnosis), @akshaykalia (egress-lib finding), @scottgigante-hubflow, @woodsonl, @bezdekt. + +**A2. #3051: Codex usage limit reported as "inconclusive; proceeding".** Code read: `_gstack_codex_model_probe` matches only model and broken-install patterns, so a quota error falls through to `MODEL_PROBE_INCONCLUSIVE`, is never cached, re-probes for 30 s in every skill, and drops Codex's own message with the reset time. +- Fix: a new probe outcome `quota_exhausted` (exit 4), matched on Codex's usage-limit and `insufficient_quota` lines on a failed, non-timeout call (a plain rate-limit or HTTP 429 line is the separate, uncached `rate_limited` outcome in CEO-14). It prints Codex's error line verbatim, maps to `CODEX_MODE: quota_exhausted`, and every caller treats it as `unavailable` missing coverage (never a pass) and skips the remaining Codex calls in that run. Cached for 15 minutes under the same signature as `MODEL_UNUSABLE`. +- Add reason codes `quota_exhausted` and `rate_limited` to `lib/gate-outcomes.ts`, each with its `docs/troubleshooting.md` anchor; `classifyOutsideReview` recognizes the same lines in a review's stderr so a mid-run quota error reads as `unavailable (quota_exhausted)` and a mid-run 429 as `unavailable (rate_limited)`. +- Tests: probe with a stub `codex` that prints the real message (from the issue) and exits 1; a stub that prints it and exits 0 must stay `MODEL_OK`; timeout keeps its inconclusive contract; every generated preflight block branches on `quota_exhausted`. + +**A3. #3046: reply templates run backticks as shell.** Code read: `review/greptile-triage.md:102,108` post `-f body=""`, and the templates below put `` `` `` and quoted diff code in the body. In double quotes bash runs each backtick span. `scripts/resolvers/preamble/generate-test-failure-triage.ts:74-75` does the same with raw test error output in `--body "..."`. +- Fix: both templates put the body in a file the agent writes with the host's file-write tool at a `mktemp` path, and pass `-F body=@file` / `--body-file`; no heredoc carries untrusted text (CEO-12). Audit every generated skill for the same shape (untrusted placeholder inside a double-quoted command argument) and fix each. +- Class guard: extend the generated-bash lint as specified in CEO-12 (every command, every quoting shape, reviewed placeholder allowlist). +- Credit: @dgrant. + +**A4. #3050: oversize autoplan journal reported as a retryable identity failure.** Code read: `lib/claude-public-transcript.ts:555` throws `OwnedReadError('identity')` when the journal exceeds 32 MiB; the hook treats `identity` as deny-and-retry, and a journal only grows, so /autoplan can never enter Phase 1 in a long session (reported journals: 52, 58 and 73 MiB). +- Fix: a distinct `too_large` reason, classified as a hard cause with the supported fallback ("This session's journal is 52 MiB, over the MiB limit. Start a new session for /autoplan, or run /plan-ceo-review, /plan-eng-review and the other reviews individually.", with the real size and the cap in force). New row in `docs/autoplan-guard-troubleshooting.md`. The cap, both oversize paths and the follow-up are specified in CEO-2 under "Accepted review amendments" below. +- Tests: an oversize journal yields `too_large`, never `identity`; a real identity mismatch still yields `identity`. + +**A5. #3040: weekly OSV scan red.** From run 37280512718: `fast-uri` 3.1.6 (two High, fixed 3.1.8), `ip-address` 10.3.1 (pinned by our own override; fixed 10.7.1; reachable through `socks` in browse's proxy path), `hono` 4.13.5 (fixed 4.13.7), `dompurify` 3.4.14 in `lib/diagram-render` (fixed 3.4.16), `braces` 3.0.3 in `lib/diagram-render` (High, no fixed release). +- Fix: raise the overrides to the fixed versions, rebuild the diagram-render bundle, and run the browse proxy tests. `braces` gets an `[[IgnoredVulns]]` entry with `ignoreUntil` only after confirming it is build-time only and never in the shipped bundle; otherwise replace the path that pulls it. + +**A6. #3034: /ship measures a red paid case before rerunning the gate (approved).** As specified in #3034: classify the red line, run that case alone N times (default 10, parallel) on the current head, fix at the cause until it passes at least 90%, then run the full gate once; report a per-case table in the PR body. Diagnostic trials never change a recorded verdict and are labeled diagnostic, never uploaded as verdicts (the EVAL_POLICY distinction goes in `docs/TESTING_INTERNALS.md`). Behavior cases measure by panels of 3 and judge cases by 3-sample panels. Pre-registered N, a per-case spend cap shown before running, and a repair-round limit that stops with a named-red report. Generic over the project's documented eval command, with gstack's as the default. Free test pins the steps and the "diagnostic, not verdict" rule; one paid case uses a seeded flaky fixture. + +**A7. #3038: weekly periodic red (`auto-decide-preserved`, outcome=timeout) and #2996 (marathon red).** After #3033 lands, measure `auto-decide-preserved` 10 times alone on main with A6's loop, root-cause the timeout, and fix it to at least 90% without raising budgets. Confirm the marathon lane is green on main after #3033 and close #2996 with the run link, or fix what is still red. + +### Tier 2: ship if green + +- **B1. PR #3045: shell-built autoplan init gets the generic "init missing" denial.** When no init binds but a Bash call looks like a snapshot init through variables, substitution, chaining or pipes, the denial names that as the cause and prints the literal re-run. The binding rule does not change. Credit @CharlesBerg85. +- **B2. PR #3039: Windows `browse chain` with piped input exits 0 having run nothing.** Read stdin synchronously (`fs.readFileSync(0)`) only when stdin is not a TTY; empty input gets the existing usage error and exit 1. Test pipes a flow into the real CLI against a stub daemon. Credit @VaibDeveloper. +- **B3. PR #3048: redaction false positives on TypeScript parameter types and JSX expressions.** Rewrite as one rule instead of two keyword heuristics: an `env.kv` value that is a bare identifier reference or a dotted property chain (optionally joined by `+`) is source syntax, not a literal. True-positive controls must still flag: a YAML bare high-entropy token, a quoted literal, a brace-wrapped literal, and a literal concatenated to a property read. Credit @tianxchen. +- **B4. #3026: Windows runtime roots cannot start the browse server.** Defect 1 (module-scope `server.ts` resolution) was fixed in v1.91.19.0. Defect 2 remains: `server-node.mjs` imports `playwright` and the other externals, and the Windows runtime roots (copies, not symlinks) have no reachable `node_modules`. Fix in setup for every runtime-root host: make the source checkout's `node_modules` reachable from the runtime root on Windows (a directory junction; no admin rights needed), and run setup's "Node can load Playwright" check against the installed runtime root, not the checkout. Verified by the Windows CI lane; not testable live here. +- **B5. #3028: /cso on Windows never finds Docker.** Bind `docker.exe` at setup the same way git is bound (`GSTACK_CSO_TRUSTED_DOCKER`), accept it only from a directory a standard user cannot write, and put its directory in the child `TRUSTED_PATH` so credential helpers resolve. Coordinate with the CSO thread (GSTA-24). Investigate the side note (a Java repo detected as Python) and fix it if it reproduces. +- **B6. #3018: root router sends requests to skills the user disabled.** Main already drops skills disabled through gstack's own `disabled_skills` config from the rendered router (C8, v1.91.19.0: `omitDisabledSkills` in `scripts/gen-skill-docs.ts`). The remaining gap is skills turned off in Claude Code's `skillOverrides`, which a render cannot see, plus the wording. Route only to skills present in the session's skill list, say so in the router, and replace "When in doubt, invoke the skill" with when to invoke and when not to. Routing evals must stay green. +- **B7. PR #3016 (residual): spawned sessions and consent.** Main already withholds the artifacts-sync consent prompt from spawned and headless sessions. Remaining gap: the spawned auto-choose rule's exception list covers destructive options but not consent or publishing to a remote. Add "granting consent or publishing data off this machine" to that exception in the skill-start spawned block and the hook directives. A consent the user already gave interactively (a saved `artifacts_sync_mode`) stays in force; the PR's change to stop syncing in that case is not taken. +- **B8. PR #3032 (lean version): a hook that does not parse blocks every tool call.** Claude Code runs gstack's hook shims through `/bin/sh`; a shim with a syntax error exits 2, which blocks the tool call in every session. Before registering hooks, setup runs `bash -n` on each shim and a parse check on its TypeScript entry, and refuses to register with the file and line. About 50 lines plus a test, instead of the PR's 4,600. +- **B9. #3020: /ship dispatches Greptile triage when no PR exists.** Check for a PR inline first; with none, skip the dispatch and record "Greptile: runs on the PR once it exists". Running Greptile in parallel during /ship (push early) is a larger change and goes to TODOS.md. +- **B10. #3030: browse clarity.** BROWSER.md notes that the default GPU flags disable WebGL and `GSTACK_DISABLE_GPU=off` restores it; the config-mismatch message says another session started the daemon with `--headed`/`--proxy` and offers both remedies. +- **B11. PR #3029: /sync-gbrain offers a `--dream` build for a source with no code pages.** gstack's own sync always uses `--strategy code`, so the PR's soft-delete claim does not apply to gstack's path. The misleading offer is real for sources added outside gstack. Probe code scope (`code-def` on an impossible symbol, read `.status`) before offering, if the probe is confirmed against a live gbrain at or above 0.60; otherwise move to TODOS.md. +- **B12. PR #3047 (minimal): Copilot on Windows runs bash blocks in PowerShell.** Add one line to the Copilot tool glossary: on Windows, run each bash block through Git for Windows' `bash.exe`, never PowerShell or WSL `bash`. The PowerShell repair skill and README section are not taken. + +### Not in this wave + +- **#3037 (Windows Bun aborts):** fixed by #3033's Bun 1.4.2 upgrade; close when it merges. +- **#3012 (question tools only for unanswered questions):** main's tool-resolution and failure fallback already cover the reported stop; the all-skill preamble churn is not worth the eval risk. Close with thanks. +- **#3021 (context-bill benefit estimates):** a feature, not a fix; stays open for a later feature pass. +- **Dependabot #2923, #2988, #3043:** routine bumps outside a severe wave. #3043's agent-sdk jump (0.2.117 to 0.3.288) is a deliberate harness bump and needs its own PR with the Claude Code pin. +- Older open feature requests and issues triaged in the October 3 wave. + +### Tracker hygiene + +- On merge, close each fixed issue with the release, the upgrade command for the reporter's host, and an invitation to reopen with `./setup --status` output. +- Close every adopted PR at merge with a link to the wave PR and a thank-you; close #3012 and #3016 with the reasons above; close #3027, #3049 and #3052 as superseded by A1 with credit. + +### Validation + +- Focused free tests per item, written to fail on main first. Run the free suite once at the end on Ubicloud. +- Paid: A6's seeded-flake case, A7's measurement, routing evals for B6, and the PR gate. Diagnostic trials follow A6's rules. +- Windows items (B2, B4, B5, B12) are verified by the Windows CI lane to the extent CEO-24 states; the PR names each Windows behavior that was not exercised. +- One PR, separate commits per item, patch version bump. + +### Accepted review amendments + +Each amendment below was accepted by the /autoplan review. Where an amendment names an item, it adds to or overrides that item's text above. + + + +- **CEO-1 (A6, free-suite flakes).** As #3034 specifies, when a free-suite shard fails during /ship, /ship reruns that shard's exact file list (which also catches order-dependent failures) N times (default 10), in parallel, under a 10-minute wall-clock cap, with the runner's own flaky-retry turned off so retries never inflate the measured rate. This needs an executable runner change, not just workflow wording: `scripts/ubicloud/test-free.sh` sets `GSTACK_FREE_RETRY_FLAKY=1` unconditionally today, and a free test proves the diagnostic launcher disables retries and records every outcome. Each rerun gets its own state (temp HOME, temp state root and its own port range), and the first rerun runs at the shard's original concurrency as the baseline; parallel reruns are labeled as such so contention they add is not read as the original failure. It runs the reruns on one Ubicloud VM, as N parallel processes, when `UBICLOUD_API_KEY` is set and the project documents a Ubicloud test command (gstack: `bun run test:ubicloud`); otherwise locally. The rerun uses the project's documented test command with the shard's file list as arguments; when the runner cannot take a file list, it reruns the whole failing command. It reports the pass rate and a failure capture before calling the failure pre-existing or pushing; when the cap hits, it reports the partial count (for example 6 of 10 completed). These reruns are labeled diagnostic and never change the recorded verdict. The free test that pins A6's steps also pins this step in the generated ship skill. +- **CEO-2 (A4 cap and follow-up).** This wave keeps the owned-journal cap at 32 MiB (`OWNED_TRANSCRIPT_MAX_BYTES`, already in the working tree) and does not raise it on timing evidence: both outside reviews showed a larger cap only moves the same cliff. The summed multi-file batch read (`lib/claude-public-transcript.ts` around line 341) keeps its own 32 MiB constant and stops throwing a plain `Error` with a hard-coded "32 MiB": it reports `too_large` with the real size and cap, and so does the `ownedParentTranscript`-narrowed loop. Free boundary tests inject a small cap through a test seam and cover the owned read, the snapshot path and the narrowed loop: at the cap reads; one byte over yields `too_large`, never `identity`; a real identity mismatch still yields `identity`. No multi-MiB fixture enters the free suite. A P1 TODOS.md entry, targeted at the next wave, specifies a bounded or incremental owned-journal read that keeps every ownership and ancestry check; its acceptance is an /autoplan run that completes in a session whose journal is over 100 MiB, with phase-entry latency and peak memory measured on macOS and Linux developer machines. +- **CEO-3 (A6 paid case shape).** The seeded-flake paid case covers only the measure, fix, re-measure and gate segment of /ship. Its fixture project has no remote; its CLAUDE.md documents a single-case command (`./evals.sh case `) and a gate command (`./evals.sh gate`), both local stubs. The fixture's inner case is kind `rule`, so CEO-9's rule budget applies (10 trials, at least 9 passing). Each stub invocation claims the lowest free trial slot by creating `slots/` with `mkdir`, which is atomic, so parallel trials never race; the trial in slot 2, 5 or 9 fails before the fix (3 of 10). Its failure output prints a `failure_cause` and `failure_detail` naming the nondeterministic line in one fixture file (it reads an unsorted list that the seed shuffles and compares it to a sorted expectation); the deterministic fix is to sort the list, after which it passes 10 of 10. The fixture's free test command passes instantly and its tree is clean, so /ship reaches the eval step at once; the stub's first gate run is red on the flaky case, and the case stops once /ship has run the gate command after the fix; it never reaches version, push or PR steps. Declared fixture answers: approve the CEO-9 spend question; any other question fails the case as a contract violation. Inner trials cost nothing and finish in seconds. The case is kind `behavior` (a panel of 3 independent trials, PASS at 2 or more with no contract violation), runs in the periodic tier, and asserts that /ship measured the case alone, fixed the named line, re-measured at or above 90%, and only then ran the gate command once. +- **CEO-4 (A7 replay first).** A7 starts by replaying the captured transcript and public events of periodic run 37272185151 (`auto-decide-preserved`, outcome=timeout) against the known Step 0E handoff-line class (fixed in v1.91.19.0 by `gstack-ceo-mode-handoff`), as a free regression, before any paid trial. If the replay matches that class, it is a regression of that fix: fix it at the cause and keep the replay as a free regression test. If it does not match, the replay becomes the failure capture that starts A6's loop. The baseline measurement (10 trials alone, after #3033 lands) runs on main; the closing measurement runs on the wave head and needs at least 9 of 10 before #3038 is closed. If 3 repair rounds do not reach 9 of 10, A7 alone stops: the PR body carries the named-red report with every trial, #3038 stays open with that evidence, a P1 TODOS.md entry is filed, and the rest of the wave continues. This does not make any PR check red, because the case runs in the periodic lane. +- **CEO-5 (B6 pinned prose).** The tests that pin "When in doubt, invoke the skill" (`test/gen-skill-docs.test.ts` around line 2117 and `test/helpers/autoplan-preconfigured-fixture.ts` line 7) are updated in the same commit as the wording, to assert the new when-to-invoke and when-not-to-invoke text. The TODOS.md "C8 router wording behind a routing panel" entry is closed with the routing-eval run link. +- **CEO-6 (TODOS.md writes).** The implementation PR adds TODOS.md entries, in the repo's TODOS format, for: B9's full version (push early and run Greptile in parallel during /ship, #3020); B11 if the live gbrain probe cannot be confirmed; the P1 bounded or incremental owned-journal read (CEO-2); B5's side note if it does not reproduce (see CEO-10); the P1 entry for A7 if it stops red (CEO-4); every Tier 2 item reverted under CEO-11; the executed-subcommand probe (CEO-28); and a gstack helper that posts PR and issue text by passing arguments as an array, never through a shell string (the stronger form of CEO-12, deferred). +- **CEO-7 (P3 doc anchors).** Every new failure state in this wave (a new refusal, denial or error, not a reworded informational line) has a doc anchor and a fix line: A2 `quota_exhausted` and `rate_limited`, B2, B4, B5 and B8's setup refusal in `docs/troubleshooting.md`; A4 `too_large` and B1's shell-built-init denial in `docs/autoplan-guard-troubleshooting.md`. B10's reworded config-mismatch message points to BROWSER.md's existing "Daemon discipline" section. Each message carries its anchor literally (`docs/troubleshooting.md#` or `docs/autoplan-guard-troubleshooting.md#`), and a free test enumerates every such reference in the source tree and checks the anchor exists (extending `test/troubleshooting-anchors.test.ts`). +- **CEO-8 (Windows coverage is named).** Each Windows item names its free test and adds it to the curated Windows-safe subset (`scripts/lib/windows-curation.ts`): B2 the piped-stdin `browse chain` test against a stub daemon; B4 a setup test that the runtime root resolves `playwright` through the junction and that the Node load check runs against the runtime root; B5 a binding test for `GSTACK_CSO_TRUSTED_DOCKER` (accepted only from a non-user-writable directory, present in `TRUSTED_PATH`); B12 a generated-text test for the Copilot glossary line. +- **CEO-9 (A6 measurement defaults).** The pass target is counted in trials of the case's kind: `rule` cases run 10 trials and need at least 9 passes; `behavior` cases run 4 panels of 3 (12 trials) and need every panel to pass and at least 11 of 12 trials; `judge` cases run 10 outputs, each scored by its 3-sample panel, and need at least 9 passing outputs. Before running, /ship always shows the estimate (per-trial cost from pass-rate history times trials). As #3034 specifies, it asks first when a case's per-trial cost is over a set amount (default $2 per trial) and, when the case has no pass-rate history, it shows "estimate unknown" and asks once. The repair-round limit is 3; after it, /ship stops with a named-red report. The eval command comes from the same discovery /ship already uses for test commands (CLAUDE.md/AGENTS.md, then package scripts); gstack's `scripts/test-paid-shards.ts --tier --case ` is the default; when no single-case command is documented, /ship asks once and records the answer in its report. +- **CEO-10 (B5 side note, bounded).** The "Java repo detected as Python" side note becomes one fixture test: a Maven repository (`pom.xml`, Java sources) that also contains helper Python scripts must be detected as Java. Fix detection precedence only if that fixture misdetects; otherwise record the report in TODOS.md (CEO-6). Coordinating with the CSO thread (GSTA-24) is a process note, not a deliverable. +- **CEO-11 (Tier 2 "ship if green").** A Tier 2 item ships when its focused tests and every required CI check pass on the wave head. A Tier 2 item that stays red after diagnosis within the wave is reverted as its own commit and moved to TODOS.md with the failure evidence, and the PR continues. A red Tier 1 item holds the whole PR, with one exception: A7 may stop red under CEO-4, because its case runs in the periodic lane and no PR check depends on it. The PR is never called ready while any check is red. +- **CEO-12 (A3: free text never enters a shell command).** The rule is structural: in a generated bash block, a free-text placeholder (bodies, titles, messages, error output, reviewer or diff text) may appear only as the contents of a file whose path came from `mktemp`; the command gets `--body-file`, `-F body=@file` or `--title "$(cat "$file")"`. A bash block creates the private path with `mktemp`, the agent writes the text to it with the host's file-write tool, and the next block uses it. There is no heredoc fallback: if a host cannot write the file, the post is left unsent and the skill says so. Identifier placeholders that do enter commands (branch names, paths, PR numbers, comment ids) are on a reviewed allowlist, each with a validation grammar the skill applies before use (for example a branch name matching `^[A-Za-z0-9._/-]+$`); a value that fails its grammar is not used. The lint fails on any non-allowlisted placeholder in a command argument of any command, in any quoting (double, single, or an unquoted heredoc), and reads `\\` continuation lines as one command. Hostile-payload tests run through the real generated text: a body containing backticks, `$(...)`, an apostrophe and a line equal to a common heredoc delimiter, and a branch name containing `;`. The same commit fixes the literal `\\n` sequences and triple backticks inside the current test-failure templates (`ship/sections/tests.md`, generated from `scripts/resolvers/preamble/generate-test-failure-triage.ts`), which post broken issue bodies today even without untrusted input. +- **CEO-13 (shared-prompt edits keep eval coverage).** B7 changes only the runtime skill-start spawned block and the hook directives (`hosts/claude/hooks/spawned-directive.ts`, `hosts/claude/hooks/auq-error-fallback-hook.ts`); it does not edit the static all-skill preamble (`scripts/resolvers/preamble/generate-ask-user-format.ts`), which keeps it consistent with the #3012 decision. A2's preflight change renders into every skill with an outside voice; its coverage is the free generated-preflight tests plus the outside-voice paid cases already in the PR gate (`test/skill-e2e-outside-voice.test.ts`, `test/skill-e2e-outside-plan-disabled.test.ts`). Validation adds AGENTS.md's cheap gates before any paid run: `bun run gen:skill-docs` freshness (all hosts), `bun test test/parity-suite.test.ts`, prompt-size and parity limits, the credential scan, `bun run typecheck`, `bun run typecheck:test`, `test/module-size-ratchet.test.ts` and `test/spawnsync-timeout-tripwire.test.ts`. +- **CEO-14 (A2 quota versus rate limit).** Only Codex's usage-limit line ("You've hit your usage limit", including its reset time) and `insufficient_quota` map to `quota_exhausted` (exit 4, cached 15 minutes). A plain HTTP 429 or rate-limit line maps to a separate `rate_limited` outcome (exit 0, like the inconclusive contract, with `_GSTACK_CODEX_PROBE_STATE=rate_limited`): never cached, printed verbatim, reported as `CODEX_MODE: unverified (rate limited)`, and the review's own result still decides. Stub tests: usage-limit line with exit 1 gives `quota_exhausted` and is cached; `insufficient_quota` gives `quota_exhausted`; a bare 429 gives `rate_limited` and is not cached; any of these lines with exit 0 stays `MODEL_OK`; a timeout keeps the inconclusive contract. +- **CEO-15 (A1 nested sources).** A1's class guard enumerates sourced files transitively: every file the rendered skills source, plus every file those helpers source in turn (for example `bin/gstack-state-root.sh`, sourced by `bin/gstack-codex-probe`). Under both shells it makes one real call each to the model resolver, the auth probe, the state root and the egress home. The lint allows `BASH_SOURCE` only inside the one shared self-locate block, matched byte for byte across the helpers, and fails on other bash-only constructs in sourced helpers (`${!name}` indirection, `declare -n`, `mapfile`/`readarray`, `[[ ... =~ ... ]]` with `BASH_REMATCH`). +- **CEO-16 (B6 both texts).** B6 changes both places that say "When in doubt, invoke the skill": the root router (`SKILL.md.tmpl`) and the CLAUDE.md routing-injection text that `bin/gstack-skill-start` (around line 498) offers to new projects, plus gstack's own `CLAUDE.md` routing section. gstack never rewrites a user's existing committed CLAUDE.md; the release note shows the new wording for users who want to update theirs. +- **CEO-17 (Tier 2 mechanics named).** B1 detects a shell-built init when a Bash call names the snapshot tool or its `init` argument through any of: a variable holding the tool path, `$(...)` or backticks around the tool or its arguments, `&&`/`;`/`||` chains, a pipe, or a redirect; negative controls keep the generic denial for `bun "" scope ...` and for an unrelated `bun other.ts init`. B2: with no arguments, `chain` reads stdin only when stdin is not a TTY; a TTY with no arguments, or empty piped input, gets the existing usage error and exit 1 instead of waiting. B4: setup creates the junction from Git Bash with `cmd //c mklink /J`, replaces a stale junction, leaves an existing real `node_modules` directory untouched and says so. B5 accepts `docker.exe` only under `%ProgramFiles%`, `%ProgramFiles(x86)%` or `%SystemRoot%` (an allowlist, not an ACL check). B8 parses each shim with the interpreter named by its shebang (`bash -n` for today's `#!/usr/bin/env bash` shims; Claude Code's `/bin/sh` only execs the shebang) and parses each TypeScript entry with `bun build --no-bundle` into a temp directory; a failure refuses registration with the file and line. A5: `braces` gets `ignoreUntil = 2026-12-31` only after confirming it is absent from the shipped `lib/diagram-render/dist` bundle; if it is present, A5 replaces or drops the importing package before shipping. +- **CEO-18 (rework owed by commits already on the branch).** A1 (`f0531a2`): add the auth-probe call and the bash-only-construct lint (CEO-15) and the nested-source enumeration; `bin/gstack-paths` stays untouched. A2 (`597e483`): its `_QUOTA_SIG` (`bin/gstack-codex-probe` around line 287) and its tests (`test/codex-model-probe.test.ts`) map rate limits, "too many requests" and 429 to quota; split them per CEO-14, keep the reason name `quota_exhausted` everywhere (probe, `lib/gate-outcomes.ts`, docs anchor `codex-quota-exhausted`), and add the `rate_limited` row. A3 (`a853e41`): replace its heredoc-based bodies with CEO-12's file-write mechanism, extend its lint to the structural rule, the allowlist grammars and every quoting shape, and add the hostile-payload tests. A4 (`ffde420`): add the batch and narrowed-loop `too_large` paths and the seam-injected boundary tests (CEO-2). A5 (`e77c532`): run CEO-22's acceptance scan. The Codex watchdog fix (`2e1d3e7`, freeze the command before reaping its children) is in the wave without a plan item; it gets a regression test and a CHANGELOG line under A2's Codex reliability heading. +- **CEO-19 (Tier 2 tests and fallbacks).** B7: a free test asserts the consent and publishing exception in the skill-start spawned block and both hook directives. B9: a free test on the generated ship text asserts the PR check comes before the Greptile dispatch; with no PR, `gh` missing or not logged in, or a non-GitHub remote, /ship skips the dispatch and records "Greptile: not run (); runs on the PR once it exists". B10: a free test asserts the new config-mismatch message text and the BROWSER.md WebGL line. B11: a free test with a stub `gbrain` covers code scope present, absent, gbrain below 0.60 and a probe error; below 0.60 or on a probe error, the current offer stays unchanged (no regression). +- **CEO-20 (B3 keeps its controls).** B3's exemption applies only in source contexts: a file whose `+++ b/` path is TypeScript or JSX, a value after a `:` type annotation, or a value inside `{...}`. A known secret prefix or a high-entropy check takes precedence over the exemption, so a YAML bare high-entropy token still flags. +- **CEO-21 (B8 refusal behavior, including upgrades).** Setup parse-checks every hook shim and TypeScript entry in the staged tree before it swaps the new install into place. Any failure aborts the swap, so the previous working install (and every registration pointing into it) stays active; setup prints the file and line and exits non-zero. A fresh install with a failing hook registers none of the failing hooks, registers the rest, and exits non-zero. Tests cover a fresh install and an upgrade over an existing install with live registrations, where the new tree has one unparseable shim. +- **CEO-22 (A5 acceptance).** #3040 closes only after osv-scanner, run with the workflow's `--config=.osv-scanner.toml` against both lockfiles on the wave head (or a dispatched weekly OSV workflow run), reports no unignored findings; the PR body links that run. +- **CEO-23 (what "fixed" means for a red case).** A6 and A7 report measurements as "observed k/n after fix at ", never as a proven rate. A red case is called fixed only when there is a named causal change and every failure in the closing measurement is explained; 9 of 10 alone does not close it. Longer-term reliability comes from the weekly census, reported separately. +- **CEO-24 (Windows claims match the evidence).** The PR states exactly what the Windows lane exercised. B12's Copilot interpreter choice and B5's actual Docker operation are reported as unverified until exercised on Windows. `.github/workflows/windows-setup-e2e.yml` gains one journey step after setup: start the browse server from the installed runtime root and pipe a two-command flow into `browse chain`, asserting real output (covers B2 and B4 end to end). +- **CEO-25 (B6 premise check).** Before rewording the router, one live Claude Code check confirms that a skill set to `"off"` in `skillOverrides` is absent from the session's skill list. If it is not absent, B6 drops the "route only to skills in the session's list" sentence and ships only the wording change. +- **CEO-26 (contributors hear early).** When each rewrite commit lands on the branch, the superseded contributor PRs (#3027, #3049, #3052, and #3039, #3045, #3048, #3032, #3029, #3047, #3016 as adopted) get a comment linking the commit and the credit line; they are closed at merge as planned. +- **CEO-27 (B11 decided on the first probe).** B11 ships only if the live gbrain probe succeeds on the first try against gbrain 0.60 or later; otherwise it goes to TODOS.md (CEO-6) without further investigation in this wave. +- **CEO-28 (A1 keeps sourcing, with the reason recorded).** A1 keeps sourced helpers because skills call their shell functions (`_gstack_codex_timeout_wrapper`, `_gstack_codex_select_model`, `_gstack_egress_run`) across a block and read the variables they set; `${(%):-%x}` is chosen over `functions_source` for simplicity, not version support. Turning the probe into an executed subcommand (`gstack-codex-probe select-model exec` printing its status lines) goes to TODOS.md as the next step that removes the shell dependency. + + + +- **DX-1 (A1 locates itself or says why it cannot).** The two helpers share one self-locate block with one resolution order: `${BASH_SOURCE[0]}` under bash or `${(%):-%x}` under zsh, made absolute with `$PWD` when relative; else `$GSTACK_ROOT/bin` when `GSTACK_ROOT` is exported and holds the helper (the variable the runtime prelude in `scripts/resolvers/runtime-root.ts` already uses); else the helper fails loudly instead of guessing: `gstack: cannot locate gstack-codex-probe (shell: ). Source it from bash or zsh, or export GSTACK_ROOT=. https://github.com/garrytan/gstack/blob/main/docs/troubleshooting.md#sourced-helper-location`. The Claude-only default path in `bin/gstack-codex-probe` and the `$(pwd)` fallback in `bin/gstack-egress-lib.sh` (both in commit `f0531a2`) are removed. The class guard adds an unknown-shell case and a bare-name (`source gstack-codex-probe` via PATH) case. CEO-15's byte-for-byte lint applies after this rework; the two blocks committed in `f0531a2` differ today and must be unified first. Outside CI, a missing zsh prints `zsh not installed: install zsh to run the sourced-helper guard (CI requires it)` and skips; under `CI=true` it fails, as A1 already states. +- **DX-2 (one token per state, absolute doc links).** The rate-limit state is spelled `rate_limited` everywhere: probe output `CODEX_MODE: unverified (rate_limited)`, probe state, gate reason and docs anchor `codex-rate-limited`. Its troubleshooting row says: at probe time the review still runs; a mid-review 429 means that review failed, so coverage is missing. Every new user-facing message prints a full `https://github.com/garrytan/gstack/blob/main/docs/.md#` URL (the pattern `autoplan/bin/phase-publication-hook.ts` already uses), so the link works from any project; CEO-7's anchor test checks these URLs. +- **DX-3 (quota cache has a way out).** The `quota_exhausted` output prints Codex's own line, when the gstack cache expires, and the retry-now command: `GSTACK_CODEX_PROBE_RETRY=1` bypasses the negative cache for one probe (documented in the anchor row, along with deleting `/.codex-model-probe`). The probe HINT committed in `597e483` ("usage or rate limit", no anchor) is rewritten to match. A stub test covers recovery: cached `quota_exhausted`, then `GSTACK_CODEX_PROBE_RETRY=1` with a stub that now succeeds, gives `MODEL_OK` without changing model, config or credentials. +- **DX-4 (A4 recovery keeps the user's work).** The `too_large` denial reads: "This session's journal is MiB, over the 32 MiB limit /autoplan can verify. Run /context-save, start a new session (resume and compact keep writing this journal), run /context-restore, then /autoplan ; or run /plan-ceo-review, /plan-devex-review and /plan-eng-review individually." followed by its full anchor URL. The docs row says the cap has no user override and why (the guard must read the whole parent journal to verify ownership). +- **DX-5 (no dead-end refusals).** When a CEO-12 post cannot be sent (file write failed, or an identifier failed its grammar), the skill prints the cause with the actual value, the path of the body file it composed, and the exact safe command to post it by hand (for example `gh pr comment --body-file `). Branch names are validated with `git check-ref-format --branch` instead of a narrow regex, so valid refs with `+`, `@` or non-ASCII characters are accepted. After a successful post the block deletes its temp file. B5's refusal names the `docker.exe` path it found, says a user-writable directory is untrusted for a child that carries registry credentials, gives the fix (install Docker Desktop under Program Files, or run /cso static-only), states it is not overridable, and carries its anchor. CEO-21's setup output lists the hooks it registered and skipped, says "this is a gstack bug; report :", and on an upgrade says "your previous install is still active". B4, when it leaves a real `node_modules` in place and the Node/Playwright check then fails, prints the command that fixes it and its anchor. +- **DX-6 (upgrades are protected before the checkout changes).** `bin/gstack-session-update` fast-forwards the live checkout before setup runs (`git merge --ff-only` around line 198), and Claude Code runs hooks straight from that checkout, so CEO-21's staged check alone cannot protect auto-updates. The updater parse-checks every hook shim and TypeScript entry at the incoming revision (from `git archive ` into a temp directory) before the fast-forward, using the same check as CEO-21. A failure holds the update, the same way the existing `BUN_HELD` hold works, and records the reason and file:line in the update status. A test runs the updater against a fixture remote whose incoming revision has one unparseable shim, with existing registrations, and asserts the checkout did not move and the old hooks still run. A manual `git pull` followed by `./setup` cannot be checked before the pull; setup reports it with DX-5's message. +- **DX-7 (measure-loop controls are named and capped).** A6's controls are gstack config keys with the CEO-9 defaults: `ship_measure_rule_trials` (10), `ship_measure_behavior_panels` (4), `ship_measure_judge_outputs` (10), `ship_measure_ask_per_trial_usd` (2), `ship_measure_budget_usd` (25: a total cap per red case across baseline, repair rounds and panel scoring, enforced by the coordinator before each batch, including parallel workers), `ship_measure_max_rounds` (3) and `ship_rerun_backend` (`local` by default; `ubicloud` only when set, so Ubicloud spend is opt-in). /ship prints the per-kind table (trials, pass bar, panels, budget) before it runs; the same table is in `docs/TESTING_INTERNALS.md` and in the ship skill's user-facing text. When the budget is exhausted, /ship stops with the named-red report. A documented skip ("this red is infrastructure, do not measure") labels the case `unmeasured` in the PR body; it never counts as a pass. A6's "default 10" means the rule-kind default; behavior and judge cases use CEO-9's per-kind counts. +- **DX-8 (the target journey is measured).** The PR records one timed run of the persona's journey on a macOS machine with zsh as the login shell: start of `/gstack-upgrade` (or `./setup`) to the first skill preflight that shows `CODEX_MODE: ready`, and a second run with a stub quota response that shows `CODEX_MODE: quota_exhausted` with the reset line; it records steps, any restart needed and elapsed time against the 2-5 minute target. The implementer uses Garry's paired Mac when available; without a Mac the PR says the journey is unmeasured. A free CI check on the existing macOS runner sources the probe under zsh and runs `_gstack_codex_select_model exec` (no paid call). +- **DX-10 (DX amendments govern where they differ from CEO ones).** Where a DX amendment and a CEO amendment differ, the DX amendment governs: DX-7's opt-in `ship_rerun_backend` replaces CEO-1's automatic Ubicloud choice when `UBICLOUD_API_KEY` is set; DX-5's `git check-ref-format --branch` replaces CEO-12's example branch regex; DX-2's `rate_limited` spelling replaces CEO-14's `rate limited`. +- **DX-9 (users can verify and learn what changed).** Each issue-close comment (Tracker hygiene) includes a copy-paste check with its expected output; for #3024: `zsh -c 'source ~/.claude/skills/gstack/bin/gstack-codex-probe && _gstack_codex_select_model exec'` should print `CODEX_MODEL: (exec; source: ...)`, not `invalid`. The CHANGELOG entry has a "What changes for you" list: the new messages and their links, `quota_exhausted` skipping Codex for the rest of the run, setup and the auto-updater refusing unparseable hooks, the router wording (with a one-line command users can run to update their own CLAUDE.md section), the B7 consent rule, and the DX-7 controls. B10's BROWSER.md line states the effect plainly: "set `GSTACK_DISABLE_GPU=off` to enable WebGL (`off` is the only recognized value)". + + + +- **ENG-1 (rework inventory covers every landed commit).** CEO-18's list is extended to every commit on the branch; each item ships only when its row says "matches" or its rework is done. B1 `2328cd3`: add CEO-17's negative controls. B2 `cdc0fc7`: see ENG-10. B3 `4441c8f`: see ENG-11. B6 `e103e57`: regenerate every host (`bun run gen:skill-docs --host all`; `.agents/`, `.factory/`, `.kiro/`, `.opencode/`, `.slate/` still carry the old wording), update `CLAUDE.md:532` and `test/helpers/autoplan-preconfigured-fixture.ts:7` (CEO-5, CEO-16), run CEO-25's live check. B7 `7bfe46b`: matches. B8 `659b197`: see ENG-2 and ENG-3. B9 `23520a9`: `PR: unknown` must skip with the reason instead of dispatching, plus CEO-19's test. B10 `5b06b7b`: DX-9 wording check. B4 `9618336` and B12 `3df850f`: see ENG-9 for B4; B12 matches (CEO-8's Windows curation entry still owed). A5 `e77c532`: the plan now records the two extra ignores it added (sprintf-js via roarr, katex in diagram-render) and their `ignoreUntil = 2026-11-30` (consistent with the file's other entries, replacing CEO-17's 2026-12-31); each of the three new ignores names a tracking issue filed at ship time, as the file header requires. +- **ENG-2 (honest scope for the setup hook gate).** On the Claude install, `CANONICAL_GSTACK_ROOT` (`~/.claude/skills/gstack`) is the git checkout the hooks run from, so by the time `./setup` runs the new files are already live; there is no staged swap there. CEO-21's "abort the swap, previous install stays active" applies only to copy-based runtime roots. On the Claude checkout the setup gate refuses to register the failing hooks, registers the rest, reports file and line, and exits non-zero; it never prints DX-5's "your previous install is still active". DX-6 (the updater's pre-fast-forward check) is the protection for auto-updates. The landed gate's early `exit 1` before any registration is replaced accordingly. +- **ENG-3 (the hook check covers what the hook runs).** The hook list is derived from the registration code (every `_hook_command_path` argument plus the phase-publication hook), not a hand list; a free test asserts the two stay equal. Each TypeScript entry is checked with a local-import bundle into a temp directory (`bun build --target=bun` with node_modules packages external), so a broken or missing local import fails the check; a missing entry file fails instead of being skipped. Tests: broken imported module, missing entry, and an upgrade with existing registrations (both for setup and for DX-6's updater check, which runs after the existing `BUN_HELD` check). +- **ENG-4 (A2's run-scoped skip has a carrier).** Skill blocks run in fresh shells, so "skip the remaining Codex calls in that run" is carried by the 15-minute negative cache; a test runs two consecutive fresh-shell preflight blocks against a quota stub and asserts the second makes no Codex call. +- **ENG-5 (self-locate works where it matters and fails loudly elsewhere).** DX-1's fallback accepts a set (not necessarily exported) `GSTACK_ROOT` that holds the helper. The shared block ends with `return 1 2>/dev/null || exit 1` after printing its message and before any bash/zsh-only syntax, so dash and sh reach the message instead of a syntax error. The generated preflight (`scripts/resolvers/constants.ts`, which today runs `source ... 2>/dev/null || true`) keeps the source step's diagnostic and reports a distinct `CODEX_MODE: helper_unavailable` with that line and its anchor, instead of falling through to `not_authed`. Class-guard cases: dash (message, no syntax error), relative-path source followed by `cd`, bare-name source via PATH, and the full generated preflight under each. +- **ENG-6 (quota and rate-limit matching reads Codex's error, not the echoed prompt).** Both the probe's signatures and `classifyOutsideReview`'s stderr matching read only Codex's own error lines (the trailing `ERROR:` / `stream error` lines), not the whole stderr, which echoes the session prompt. Tests: an echoed prompt that contains "usage limit" and `insufficient_quota` with a non-quota exit stays `execution_failed`; a 429 body that also carries `insufficient_quota` resolves to `quota_exhausted` (quota takes precedence over rate limit). +- **ENG-7 (Dockerfile.ci installs zsh).** `.github/docker/Dockerfile.ci` adds zsh, as A1 already states, so the A1 guard runs (and fails loudly when missing) inside the eval container too. +- **ENG-8 (identifiers are shell-safe, not only git-valid).** `git check-ref-format --branch` accepts `fix;id` and `` fix`id` ``, so DX-5's branch check also rejects shell metacharacters (`$`, backtick, `;`, `|`, `&`, `(`, `)`, `<`, `>`, quotes, `\`, whitespace) and refuses with DX-5's message; numeric ids must match `^[0-9]+$`. Identifiers that come from remote data (PR head branch names, file paths from a diff or PR) travel as data, read from the agent-written file into quoted variables, never substituted into shell source. Tests include metacharacter identifiers that git accepts. +- **ENG-9 (B4 uses the landed design, and B5 is honest about Windows Docker).** B4 keeps the landed runtime design (`9618336`: the Windows runtime root starts the server from the source checkout's bundle through `.source-path`) instead of the plan's `mklink /J` junction, which avoids a runtime-root delete following the junction into the checkout. It adds a version-skew guard: when the runtime root's CLI version and the checkout's server version differ, the CLI refuses with an anchor and the `./setup` fix. CEO-17's junction mechanics and CEO-8's junction test are replaced by a test that the runtime root resolves the checkout bundle and refuses on version skew; CEO-24's journey step becomes a real isolated install (the current `windows-setup-e2e.yml` runs `bun run build` but never `./setup`), then the installed `browse` from an unrelated directory piping a two-command `chain` flow. B5 resolves the allowlisted roots through the known-folder API (PowerShell `[Environment]::GetFolderPath`), not the process environment, resolves the candidate's real path before the prefix check, and rejects reparse points; its refusal says "outside the trusted install locations". Because `lib/cso/docker.ts` accepts only `unix:///` endpoints, B5 is scoped to discovery: with `docker.exe` bound, /cso reports a clear "native Windows Docker transport is not supported yet; static assessment only" outcome with an anchor, and native named-pipe transport goes to TODOS.md. #3028 is closed with that explanation, not as fixed operation. +- **ENG-10 (B2 never boots a daemon for empty input).** Empty piped input, a TTY with no arguments, and a `readFileSync(0)` error (`EAGAIN`, `EOF`) all produce the usage error and exit 1 before `ensureServer()`. Tests run with no daemon and with browser dependencies unavailable. +- **ENG-11 (B3 classifies by context; replaces CEO-20's precedence rule).** Identifiers like `SessionState,` exceed the 3.0 bits/char entropy threshold, so "entropy takes precedence" cannot hold. B3 instead passes the file path into the scanner (`lib/redact-engine.ts` options) and classifies a value as a source expression only in a TypeScript/JSX file after a `:` type annotation or inside a JSX `{...}` expression; entropy and known-prefix rules apply to everything classified as a literal. The landed word-shape heuristic (`readsAsCodeName`) is replaced. Controls that must flag: `API_KEY=VelvetRiverOrbitSunset;` in a `.env`/YAML file, `password: {RiverStoneAmberCedar}` in YAML, a JWT in a `.ts` file, a quoted literal, a brace-wrapped literal, a literal concatenated to a property read. Controls that must not flag: `session: SessionState,` and `key={turn.requestId + turn.role}` in `.ts`/`.tsx`. +- **ENG-12 (A6 runner owns the measurement; budget is an admission budget).** A6 ships as a runner feature, `scripts/ship-measure.ts`, which /ship calls and which owns: per-kind aggregation (rule: N trials; behavior: panels of 3, each at its own 2-of-3 threshold, not one 2-of-12 panel as `runCaseDiagnosis` would produce today; judge: outputs scored by 3-sample panels, with standalone judge ids accepted), unique artifact directories per baseline, repair round and trial, the diagnostic label, and the per-case table. DX-7's `ship_measure_budget_usd` is an estimated admission budget: before admitting a batch it reserves the estimated cost of every concurrent trial (judges included), reconciles actual costs after each batch, and stops when reserved plus spent would exceed the budget; the PR table shows estimated and actual spend. Local reruns (CEO-1) cap parallelism at max(1, floor(cores / shard concurrency)), queue the rest and record the parallelism used. Free tests drive `scripts/ship-measure.ts` with fake runners for every kind, the budget stop and the unmeasured skip. +- **ENG-13 (A6 seeded case cannot pass without the fix).** The stub fails a trial only while the list is unsorted and its slot number mod 10 is 2, 5 or 9; slots reset per measurement batch. The case asserts the closing measurement ran at least 10 trials against the sorted file, and a negative control (a "fix" that edits an unrelated line) re-measures at 7 of 10 and never reaches the gate command. +- **ENG-14 (A3 file location and fixed temp paths).** The implementation verifies, against the Claude Code version pinned in `.github/docker/Dockerfile.ci`, where the host's write tool can create the body file without a permission prompt in default mode and in headless or spawned sessions (candidates: a project-local `.gstack/tmp/` excluded through `.git/info/exclude`, or `$GSTACK_STATE_ROOT/tmp`), uses the first that works with mode 0600, and records the result in the PR. `review/greptile-triage.md`'s fixed `/tmp/greptile_line.json` and `/tmp/greptile_top.json` move to `mktemp` paths. +- **ENG-15 (anchor test resolves locally).** CEO-7's anchor test accepts both relative anchors and DX-2's `https://github.com/garrytan/gstack/blob/main/docs/...#anchor` URLs and resolves both against the docs in the working tree, so a new anchor does not fail CI before it reaches main. + +## Review record + +### Phase 0: Intake (autoplan) + +- Base branch: `main` (GitHub; origin/main `5885157a9`, v1.91.29.0). Branch `garrytan/fix-wave-oct6`; the only change on it is this untracked plan file. +- Design doc: `gstack-design-doc-find` returns this plan itself, so it is the problem/constraints source. No `/office-hours` doc; prerequisite offer not repeated (this plan is a triaged fix wave, not an exploration). +- Scope: `dxRequired=true` (22 term matches: shell 6, command 5, Claude Code 3, CLI 2, argument 2, flag, SDK, SKILL.md, agent). UI scope: none (0 view/rendering terms) so Phase 2 (design) is skipped. gstack is also a developer tool and agents are its primary users, so DX would run regardless. +- Restore point: `/home/user/.gstack/projects/garrytan-gstack/garrytan-fix-wave-oct6-autoplan-restore-20261006-174857.md`. +- Context read: CLAUDE.md, TODOS.md (related entries: C8 router wording P3, Step 0E handoff/auto-decide-preserved history, placeholder convention for generated bash P3, gate-outcome telemetry P3), `git log -30`, design-doc lookup. + +### Phase 0.5: Outside reviewer preflight + +`CODEX_MODEL: gpt-6-astra (exec; source: /home/user/.codex/config.toml model)`, `MODEL_OK`, `CODEX_MODE: ready`. Codex runs for every phase's outside voice. (The preflight ran under bash; under zsh the same block would print `CODEX_MODEL: invalid`, which is A1.) + +### Phase 1: CEO review (SELECTIVE EXPANSION) + +#### Pre-review system audit + +- No stashes. The branch diff against main is only this plan. Hot files in the last 30 days: release files, `test/helpers/touchfiles-data.ts`, ship goldens, `plan-eng-review` sections, `docs/TESTING_INTERNALS.md`, paid-retry tests. This wave touches the same ship/test-policy surfaces that #3033 is still editing. +- Prior learnings: `gstack-learnings-search` returned none. Brain context: unreachable (no cache); treated as cold. Question tuning: off. +- Retrospective: the October 3 severe wave (v1.91.19.0) already fixed three neighbors of this wave's items: C8 (router drops gstack-disabled skills), the Step 0E handoff helper (the last root cause of `auto-decide-preserved` reds), and #3026 defect 1. Each recurrence below is checked against that fix before new work is planned. +- Taste calibration. Good references: `lib/gate-outcomes.ts` (one table, one anchor per reason, tested), `_gstack_codex_model_probe` (explicit contract comment, exit codes, cache signature). Patterns to avoid: keyword-heuristic fixes (the two-heuristic shape of PR #3048, the regex-per-spelling detector history in TODOS.md) and 4,600-line guard PRs for a 50-line check (#3032). +- Landscape (web search, Layer 1): `${(%):-%x}` is the accepted zsh equivalent of `${BASH_SOURCE[0]}`; it is unreliable only inside nested functions called after sourcing (zsh 5.0.5 report), which the plan's resolve-once-at-source-time rule avoids. `$0` and `functions_source` are the known wrong answers, matching the plan's rejections. Layer 3: nothing contradicts the conventional idiom. + +#### 0A. Premise challenges + +1. **"Codex outside voice is broken for macOS zsh users since v1.91.16.0" (A1). Valid.** Reproduced here with zsh 5.9: `zsh -c 'cd /tmp && source bin/gstack-codex-probe && _gstack_codex_select_model exec'` prints `Module not found "/../scripts/resolve-codex-generation-model.ts"` and `CODEX_MODEL: invalid`. `bin/gstack-egress-lib.sh` under zsh from `/tmp` resolves `_gstack_egress_lib_dir=/tmp`. Only three files use `BASH_SOURCE` (`bin/gstack-codex-probe` 5 sites, `bin/gstack-egress-lib.sh`, `bin/gstack-paths`); skills source `gstack-codex-probe` (33 rendered sites) and `gstack-egress-lib.sh` (15). `gstack-paths` is executed, not sourced, by skills, so its fix is defensive. Do-nothing cost: every outside pass on the default macOS shell reports `model_unusable` and the user loses cross-model coverage without knowing why. +2. **"A Codex quota error reads as inconclusive" (A2). Valid.** The probe classifies only model 400/404 and broken-install signatures; anything else falls to `MODEL_PROBE_INCONCLUSIVE` and is never cached. Correction of detail: the re-probe costs one real Codex round trip per skill (up to the 30 s wrapper), not always 30 s. +3. **"Reply templates run backticks as shell" (A3). Valid.** `review/greptile-triage.md:102,108` and `scripts/resolvers/preamble/generate-test-failure-triage.ts:74-75` confirmed. TODOS.md already carries "placeholder convention for generated bash" (P3); the A3 lint is the first enforced piece of it. +4. **"A >32 MiB journal is reported as a retryable identity failure" (A4). Valid, but the fix as written treats the symptom.** `lib/claude-public-transcript.ts:555` throws `OwnedReadError('identity')` on size; the hook retries inside a 2 s deadline. The plan's fallback ("start a new session") pushes users off /autoplan in exactly the long sessions where it gets used. The ancestry check reads the whole journal from its first user message, so lifting the cap needs a measured full read, not a tail read. Kept as written plus a measured, conditional cap raise (CEO-D6, taste). +5. **"The root router routes to skills the user disabled" (B6). Partly stale.** Main already removes skills disabled in gstack's own `disabled_skills` from the rendered router (`omitDisabledSkills`, `scripts/gen-skill-docs.ts:171-190`, v1.91.19.0). What remains: skills turned off in Claude Code's `skillOverrides` (gstack cannot see them at render time) and the "When in doubt, invoke the skill" wording, which `test/gen-skill-docs.test.ts:2117` and `test/helpers/autoplan-preconfigured-fixture.ts:7` pin. Plan text corrected by baseline edit. +6. **"#3033 overlaps only cli.ts, cso/process.ts and redact-patterns.ts". False.** `pr3033-files.txt` also lists `.github/docker/Dockerfile.ci`, `.github/workflows/free-tests.yml`, `docs/troubleshooting.md`, `docs/TESTING_INTERNALS.md`, `review/SKILL.md.tmpl`, `TODOS.md` and the release files, all of which this wave edits. Plan text corrected by baseline edit. +7. **"A6 belongs in a severe-fix wave". Accepted as the user's decision.** #3034 is a feature, approved by Garry on 2026-10-05 for this wave, and it is the tool A7 uses. Kept. +8. **"A7's timeout is a new defect". Unproven.** TODOS.md records five earlier `auto-decide-preserved` reds caused by the skipped Step 0E handoff line, fixed in v1.91.19.0 by `gstack-ceo-mode-handoff`. The first step must replay the captured run (37272185151) against that known class before paying for trials (CEO-D7). +9. **"B8: a hook that does not parse blocks every tool call". Valid, low frequency, high impact.** The shims are `#!/usr/bin/env bash` files that Claude Code runs through `/bin/sh` (per the shim header); a syntax error exits 2, which a PreToolUse hook reads as "block". It needs a broken checkout to trigger, so the lean 50-line version is the right size. +10. **A5 reachability. Valid with a correction.** `fast-uri` and `hono` arrive only through the dev dependency `@anthropic-ai/claude-agent-sdk` -> `@modelcontextprotocol/sdk` (eval harness, not shipped runtime); `ip-address` is runtime through `socks` (browse proxy). Overrides remain the right fix for all three; the browse proxy test run is the one that matters for `ip-address`. + +#### 0B. Existing code leverage + +| Sub-problem | Existing code | Plan reuses it? | +|---|---|---| +| A1 self-locate | 3 `BASH_SOURCE` sites; `bin/gstack-state-root.sh` sourced through them | Yes: one idiom replaces the sites; no new helper file | +| A2 quota outcome | `_gstack_codex_model_probe` contract + cache file `.codex-model-probe`; `GATE_OUTCOMES` table; `classifyOutsideReview` execution() stderr path; `test/troubleshooting-anchors.test.ts` | Yes: new probe exit code, new gate reason row, same cache signature | +| A3 safe bodies | generated-bash lint (existing) | Yes: extended, not new | +| A4 too_large | `HARD_CAUSE` map in `autoplan/bin/phase-publication-hook.ts:524`; `docs/autoplan-guard-troubleshooting.md` | Yes | +| A6/A7 measure loop | `scripts/test-paid-shards.ts --case`, `eval:pass-rates --run --case`, `failure_cause`/`failure_detail`, detector replay corpora, `test:ubicloud` | Yes, per #3034 | +| B1 shell-built init | `shellInitAttempt`-style diagnostic in the hook (PR #3045 design) | Rewritten, binding unchanged | +| B6 router | `omitDisabledSkills` | Kept; prose covers `skillOverrides` | +| B8 hook parse | `setup` hook registration via `bin/gstack-settings-hook add-event`, `_hook_command_path` | Yes: check sits before registration | +| B5 Docker | `GSTACK_CSO_TRUSTED_GIT` binding + `TRUSTED_PATH` in `lib/cso/process.ts` | Yes: same pattern for Docker | + +No sub-problem rebuilds existing code. + +#### 0C. Dream state + +``` + CURRENT STATE THIS PLAN 12-MONTH IDEAL + Outside voice silently missing on One self-locate idiom + a class test Every helper and generated block is + zsh; quota/oversize/Windows failures that sources every helper under bash shell-portable by construction, every + surface as the wrong cause; untrusted and zsh; quota/too_large/shell-init failure prints a typed reason with a + text reaches the shell; paid reds reasons with fixes; a lint on unsafe doc anchor, and a red eval is measured + get rerun blind at ~$90/push. bodies; /ship measures a red case. and fixed before anyone reruns a gate. +``` + +The plan moves toward the ideal on all four axes. The remaining gap is coverage: the class guards cover sourced helpers and gh/glab/git/curl bodies, not every generated bash block. + +#### 0E. Mode + +`Mode: SELECTIVE EXPANSION; approved decisions: none.` (printed by `gstack-ceo-mode-handoff`). Provenance: autoplan override (explicit), no question asked. The skill's file-count rule would recommend SCOPE REDUCTION (well over 15 changed files), but the user's standing preference is one PR per wave and autoplan fixes the mode, so scope stays whole and each expansion is decided individually. No new approach decision was needed in 0D. + +#### 0F/0G. HOLD checks and cherry-picks + +HOLD checks: +1. Complexity: about 45 files across 19 items, no new services. Fewer moving parts would mean splitting the wave, which contradicts the user's one-PR preference; each item is independently revertable as its own commit. Kept (CEO-D2). +2. Minimum changes: each item is already the minimum for its promise. Deferral candidates considered and kept: B4 and B5 (Windows; verified by CI lanes only), B11 (already conditional). Kept (CEO-D3). +3. Invariants: P1-P4 kept; repairs needed to meet them are in scope. + +10x check: the 10x version is a typed failure taxonomy that every helper, probe and hook emits into (one reason table, one doc anchor each, telemetry per reason), so "names its real cause" is enforced by construction rather than item by item. This plan builds two more rows of that table (`quota`, `too_large`), which is the right increment; the telemetry half stays a TODO (gate-outcome telemetry, P3). + +Platform potential: the shared self-locate idiom and its class test become the gate for every future sourced helper; the A6 loop becomes the standard answer to any red paid case. + +Delight scan and cherry-picks (each auto-decided per the 6 principles; see Decision Audit Trail): + +| # | Proposal | Effort | Risk | Decision | Reason | +|---|---|---|---|---|---| +| X1 | A4: measure full owned-read+parse time for 64/128 MiB journals and raise the cap when it fits the hook's 2 s loop | S | Medium (guard code) | ACCEPTED (taste) | In blast radius, small; makes /autoplan work in long sessions instead of only explaining why it cannot | +| X2 | A6: same measure-first rule for free-suite flakes (rerun the failing shard's exact file list N times, Ubicloud when configured), from #3034's design notes | S | Low | ACCEPTED | Same file as A6, <1 h CC, part of the approved issue the plan omitted | +| X3 | A2: cache `quota_exhausted` until Codex's stated reset time | S | Medium (parsing free text) | SKIPPED | 15-min TTL plus the verbatim line covers it; parsing a localized timestamp is fragile (P5) | +| X4 | Per-reason gate-outcome telemetry for `quota`/`too_large` | S | Low | SKIPPED (duplicate) | Already a TODO (gate-outcome telemetry, P3); not duplicated (P4) | +| X5 | B9 full version: push early and run Greptile in parallel during /ship (#3020's real request) | L | Medium | DEFERRED to TODOS.md | New flow outside blast radius; the plan already defers it (P3) | +| X6 | A6 paid case drives a fixture project whose eval command is a seeded local stub, so inner trials are free and fast | S | Low | ACCEPTED (taste) | Keeps the approved paid case inside the ~10-minute eval target without changing what it proves | +| X7 | A7: replay the captured red run against the known Step 0E handoff class before paid trials | S | Low | ACCEPTED | AGENTS.md: reuse captured public events in free regressions before paying | + +#### Decision ledger (CEO) + +| ID and owner | Contract and evidence | Current | Proposed | Status | Exact approval and scope | +|---|---|---|---|---|---| +| CEO-D1 (Step 0E) | Mode | SELECTIVE EXPANSION | n/a | approved | autoplan override; mode only | +| CEO-D2 (0G HOLD 1) | One PR, ~45 files | keep whole wave | split | approved (keep) | P6 + user's one-PR preference | +| CEO-D3 (0G HOLD 2) | B4/B5/B11 deferral candidates | keep | defer | approved (keep) | P1; "reduce scope on a complete plan: no" | +| CEO-D4 (0A-5) | B6 text partly stale | plan text | corrected text (baseline edit) | approved | factual correction only, behavior unchanged | +| CEO-D5 (0A-6) | #3033 overlap list | 3 files | full list (baseline edit) | approved | factual correction only | +| CEO-D6 (X1) | A4 cap | 32 MiB + too_large | measured conditional raise | approved (taste, provisional) | P2; surfaced at gate | +| CEO-D7 (X7) | A7 first step | measure 10x | replay captured run first | approved | P1/AGENTS.md eval discipline | +| CEO-D8 (X2) | A6 coverage | paid only | + free-suite flakes | approved | P2 | +| CEO-D9 (X6) | A6 paid case shape | unspecified inner runner | seeded stub eval command | approved (taste, provisional) | P1 + 10-minute eval preference; surfaced at gate | +| CEO-D10 (X3) | quota cache TTL | 15 min | until reset time | declined | P5 | +| CEO-D11 (X4) | gate telemetry | TODO P3 | in wave | declined (duplicate) | P4 | +| CEO-D12 (X5) | Greptile in parallel | TODO | in wave | deferred | P3; TODOS.md entry written by the implementation PR | +| CEO-D13 (0A-4/0A-8) | Test-pinning prose | n/a | tests updated with B6 wording | approved | P1; prompt cleanups audit pinning tests | + + +- **CEO-1 (A6, free-suite flakes).** As #3034 specifies, when a free-suite shard fails during /ship, /ship reruns that shard's exact file list (which also catches order-dependent failures) N times (default 10), in parallel, under a 10-minute wall-clock cap, with the runner's own flaky-retry turned off so retries never inflate the measured rate. This needs an executable runner change, not just workflow wording: `scripts/ubicloud/test-free.sh` sets `GSTACK_FREE_RETRY_FLAKY=1` unconditionally today, and a free test proves the diagnostic launcher disables retries and records every outcome. Each rerun gets its own state (temp HOME, temp state root and its own port range), and the first rerun runs at the shard's original concurrency as the baseline; parallel reruns are labeled as such so contention they add is not read as the original failure. It runs the reruns on one Ubicloud VM, as N parallel processes, when `UBICLOUD_API_KEY` is set and the project documents a Ubicloud test command (gstack: `bun run test:ubicloud`); otherwise locally. The rerun uses the project's documented test command with the shard's file list as arguments; when the runner cannot take a file list, it reruns the whole failing command. It reports the pass rate and a failure capture before calling the failure pre-existing or pushing; when the cap hits, it reports the partial count (for example 6 of 10 completed). These reruns are labeled diagnostic and never change the recorded verdict. The free test that pins A6's steps also pins this step in the generated ship skill. +- **CEO-2 (A4 cap and follow-up).** This wave keeps the owned-journal cap at 32 MiB (`OWNED_TRANSCRIPT_MAX_BYTES`, already in the working tree) and does not raise it on timing evidence: both outside reviews showed a larger cap only moves the same cliff. The summed multi-file batch read (`lib/claude-public-transcript.ts` around line 341) keeps its own 32 MiB constant and stops throwing a plain `Error` with a hard-coded "32 MiB": it reports `too_large` with the real size and cap, and so does the `ownedParentTranscript`-narrowed loop. Free boundary tests inject a small cap through a test seam and cover the owned read, the snapshot path and the narrowed loop: at the cap reads; one byte over yields `too_large`, never `identity`; a real identity mismatch still yields `identity`. No multi-MiB fixture enters the free suite. A P1 TODOS.md entry, targeted at the next wave, specifies a bounded or incremental owned-journal read that keeps every ownership and ancestry check; its acceptance is an /autoplan run that completes in a session whose journal is over 100 MiB, with phase-entry latency and peak memory measured on macOS and Linux developer machines. +- **CEO-3 (A6 paid case shape).** The seeded-flake paid case covers only the measure, fix, re-measure and gate segment of /ship. Its fixture project has no remote; its CLAUDE.md documents a single-case command (`./evals.sh case `) and a gate command (`./evals.sh gate`), both local stubs. The fixture's inner case is kind `rule`, so CEO-9's rule budget applies (10 trials, at least 9 passing). Each stub invocation claims the lowest free trial slot by creating `slots/` with `mkdir`, which is atomic, so parallel trials never race; the trial in slot 2, 5 or 9 fails before the fix (3 of 10). Its failure output prints a `failure_cause` and `failure_detail` naming the nondeterministic line in one fixture file (it reads an unsorted list that the seed shuffles and compares it to a sorted expectation); the deterministic fix is to sort the list, after which it passes 10 of 10. The fixture's free test command passes instantly and its tree is clean, so /ship reaches the eval step at once; the stub's first gate run is red on the flaky case, and the case stops once /ship has run the gate command after the fix; it never reaches version, push or PR steps. Declared fixture answers: approve the CEO-9 spend question; any other question fails the case as a contract violation. Inner trials cost nothing and finish in seconds. The case is kind `behavior` (a panel of 3 independent trials, PASS at 2 or more with no contract violation), runs in the periodic tier, and asserts that /ship measured the case alone, fixed the named line, re-measured at or above 90%, and only then ran the gate command once. +- **CEO-4 (A7 replay first).** A7 starts by replaying the captured transcript and public events of periodic run 37272185151 (`auto-decide-preserved`, outcome=timeout) against the known Step 0E handoff-line class (fixed in v1.91.19.0 by `gstack-ceo-mode-handoff`), as a free regression, before any paid trial. If the replay matches that class, it is a regression of that fix: fix it at the cause and keep the replay as a free regression test. If it does not match, the replay becomes the failure capture that starts A6's loop. The baseline measurement (10 trials alone, after #3033 lands) runs on main; the closing measurement runs on the wave head and needs at least 9 of 10 before #3038 is closed. If 3 repair rounds do not reach 9 of 10, A7 alone stops: the PR body carries the named-red report with every trial, #3038 stays open with that evidence, a P1 TODOS.md entry is filed, and the rest of the wave continues. This does not make any PR check red, because the case runs in the periodic lane. +- **CEO-5 (B6 pinned prose).** The tests that pin "When in doubt, invoke the skill" (`test/gen-skill-docs.test.ts` around line 2117 and `test/helpers/autoplan-preconfigured-fixture.ts` line 7) are updated in the same commit as the wording, to assert the new when-to-invoke and when-not-to-invoke text. The TODOS.md "C8 router wording behind a routing panel" entry is closed with the routing-eval run link. +- **CEO-6 (TODOS.md writes).** The implementation PR adds TODOS.md entries, in the repo's TODOS format, for: B9's full version (push early and run Greptile in parallel during /ship, #3020); B11 if the live gbrain probe cannot be confirmed; the P1 bounded or incremental owned-journal read (CEO-2); B5's side note if it does not reproduce (see CEO-10); the P1 entry for A7 if it stops red (CEO-4); every Tier 2 item reverted under CEO-11; the executed-subcommand probe (CEO-28); and a gstack helper that posts PR and issue text by passing arguments as an array, never through a shell string (the stronger form of CEO-12, deferred). +- **CEO-7 (P3 doc anchors).** Every new failure state in this wave (a new refusal, denial or error, not a reworded informational line) has a doc anchor and a fix line: A2 `quota_exhausted` and `rate_limited`, B2, B4, B5 and B8's setup refusal in `docs/troubleshooting.md`; A4 `too_large` and B1's shell-built-init denial in `docs/autoplan-guard-troubleshooting.md`. B10's reworded config-mismatch message points to BROWSER.md's existing "Daemon discipline" section. Each message carries its anchor literally (`docs/troubleshooting.md#` or `docs/autoplan-guard-troubleshooting.md#`), and a free test enumerates every such reference in the source tree and checks the anchor exists (extending `test/troubleshooting-anchors.test.ts`). +- **CEO-8 (Windows coverage is named).** Each Windows item names its free test and adds it to the curated Windows-safe subset (`scripts/lib/windows-curation.ts`): B2 the piped-stdin `browse chain` test against a stub daemon; B4 a setup test that the runtime root resolves `playwright` through the junction and that the Node load check runs against the runtime root; B5 a binding test for `GSTACK_CSO_TRUSTED_DOCKER` (accepted only from a non-user-writable directory, present in `TRUSTED_PATH`); B12 a generated-text test for the Copilot glossary line. +- **CEO-9 (A6 measurement defaults).** The pass target is counted in trials of the case's kind: `rule` cases run 10 trials and need at least 9 passes; `behavior` cases run 4 panels of 3 (12 trials) and need every panel to pass and at least 11 of 12 trials; `judge` cases run 10 outputs, each scored by its 3-sample panel, and need at least 9 passing outputs. Before running, /ship always shows the estimate (per-trial cost from pass-rate history times trials). As #3034 specifies, it asks first when a case's per-trial cost is over a set amount (default $2 per trial) and, when the case has no pass-rate history, it shows "estimate unknown" and asks once. The repair-round limit is 3; after it, /ship stops with a named-red report. The eval command comes from the same discovery /ship already uses for test commands (CLAUDE.md/AGENTS.md, then package scripts); gstack's `scripts/test-paid-shards.ts --tier --case ` is the default; when no single-case command is documented, /ship asks once and records the answer in its report. +- **CEO-10 (B5 side note, bounded).** The "Java repo detected as Python" side note becomes one fixture test: a Maven repository (`pom.xml`, Java sources) that also contains helper Python scripts must be detected as Java. Fix detection precedence only if that fixture misdetects; otherwise record the report in TODOS.md (CEO-6). Coordinating with the CSO thread (GSTA-24) is a process note, not a deliverable. +- **CEO-11 (Tier 2 "ship if green").** A Tier 2 item ships when its focused tests and every required CI check pass on the wave head. A Tier 2 item that stays red after diagnosis within the wave is reverted as its own commit and moved to TODOS.md with the failure evidence, and the PR continues. A red Tier 1 item holds the whole PR, with one exception: A7 may stop red under CEO-4, because its case runs in the periodic lane and no PR check depends on it. The PR is never called ready while any check is red. +- **CEO-12 (A3: free text never enters a shell command).** The rule is structural: in a generated bash block, a free-text placeholder (bodies, titles, messages, error output, reviewer or diff text) may appear only as the contents of a file whose path came from `mktemp`; the command gets `--body-file`, `-F body=@file` or `--title "$(cat "$file")"`. A bash block creates the private path with `mktemp`, the agent writes the text to it with the host's file-write tool, and the next block uses it. There is no heredoc fallback: if a host cannot write the file, the post is left unsent and the skill says so. Identifier placeholders that do enter commands (branch names, paths, PR numbers, comment ids) are on a reviewed allowlist, each with a validation grammar the skill applies before use (for example a branch name matching `^[A-Za-z0-9._/-]+$`); a value that fails its grammar is not used. The lint fails on any non-allowlisted placeholder in a command argument of any command, in any quoting (double, single, or an unquoted heredoc), and reads `\\` continuation lines as one command. Hostile-payload tests run through the real generated text: a body containing backticks, `$(...)`, an apostrophe and a line equal to a common heredoc delimiter, and a branch name containing `;`. The same commit fixes the literal `\\n` sequences and triple backticks inside the current test-failure templates (`ship/sections/tests.md`, generated from `scripts/resolvers/preamble/generate-test-failure-triage.ts`), which post broken issue bodies today even without untrusted input. +- **CEO-13 (shared-prompt edits keep eval coverage).** B7 changes only the runtime skill-start spawned block and the hook directives (`hosts/claude/hooks/spawned-directive.ts`, `hosts/claude/hooks/auq-error-fallback-hook.ts`); it does not edit the static all-skill preamble (`scripts/resolvers/preamble/generate-ask-user-format.ts`), which keeps it consistent with the #3012 decision. A2's preflight change renders into every skill with an outside voice; its coverage is the free generated-preflight tests plus the outside-voice paid cases already in the PR gate (`test/skill-e2e-outside-voice.test.ts`, `test/skill-e2e-outside-plan-disabled.test.ts`). Validation adds AGENTS.md's cheap gates before any paid run: `bun run gen:skill-docs` freshness (all hosts), `bun test test/parity-suite.test.ts`, prompt-size and parity limits, the credential scan, `bun run typecheck`, `bun run typecheck:test`, `test/module-size-ratchet.test.ts` and `test/spawnsync-timeout-tripwire.test.ts`. +- **CEO-14 (A2 quota versus rate limit).** Only Codex's usage-limit line ("You've hit your usage limit", including its reset time) and `insufficient_quota` map to `quota_exhausted` (exit 4, cached 15 minutes). A plain HTTP 429 or rate-limit line maps to a separate `rate_limited` outcome (exit 0, like the inconclusive contract, with `_GSTACK_CODEX_PROBE_STATE=rate_limited`): never cached, printed verbatim, reported as `CODEX_MODE: unverified (rate limited)`, and the review's own result still decides. Stub tests: usage-limit line with exit 1 gives `quota_exhausted` and is cached; `insufficient_quota` gives `quota_exhausted`; a bare 429 gives `rate_limited` and is not cached; any of these lines with exit 0 stays `MODEL_OK`; a timeout keeps the inconclusive contract. +- **CEO-15 (A1 nested sources).** A1's class guard enumerates sourced files transitively: every file the rendered skills source, plus every file those helpers source in turn (for example `bin/gstack-state-root.sh`, sourced by `bin/gstack-codex-probe`). Under both shells it makes one real call each to the model resolver, the auth probe, the state root and the egress home. The lint allows `BASH_SOURCE` only inside the one shared self-locate block, matched byte for byte across the helpers, and fails on other bash-only constructs in sourced helpers (`${!name}` indirection, `declare -n`, `mapfile`/`readarray`, `[[ ... =~ ... ]]` with `BASH_REMATCH`). +- **CEO-16 (B6 both texts).** B6 changes both places that say "When in doubt, invoke the skill": the root router (`SKILL.md.tmpl`) and the CLAUDE.md routing-injection text that `bin/gstack-skill-start` (around line 498) offers to new projects, plus gstack's own `CLAUDE.md` routing section. gstack never rewrites a user's existing committed CLAUDE.md; the release note shows the new wording for users who want to update theirs. +- **CEO-17 (Tier 2 mechanics named).** B1 detects a shell-built init when a Bash call names the snapshot tool or its `init` argument through any of: a variable holding the tool path, `$(...)` or backticks around the tool or its arguments, `&&`/`;`/`||` chains, a pipe, or a redirect; negative controls keep the generic denial for `bun "" scope ...` and for an unrelated `bun other.ts init`. B2: with no arguments, `chain` reads stdin only when stdin is not a TTY; a TTY with no arguments, or empty piped input, gets the existing usage error and exit 1 instead of waiting. B4: setup creates the junction from Git Bash with `cmd //c mklink /J`, replaces a stale junction, leaves an existing real `node_modules` directory untouched and says so. B5 accepts `docker.exe` only under `%ProgramFiles%`, `%ProgramFiles(x86)%` or `%SystemRoot%` (an allowlist, not an ACL check). B8 parses each shim with the interpreter named by its shebang (`bash -n` for today's `#!/usr/bin/env bash` shims; Claude Code's `/bin/sh` only execs the shebang) and parses each TypeScript entry with `bun build --no-bundle` into a temp directory; a failure refuses registration with the file and line. A5: `braces` gets `ignoreUntil = 2026-12-31` only after confirming it is absent from the shipped `lib/diagram-render/dist` bundle; if it is present, A5 replaces or drops the importing package before shipping. +- **CEO-18 (rework owed by commits already on the branch).** A1 (`f0531a2`): add the auth-probe call and the bash-only-construct lint (CEO-15) and the nested-source enumeration; `bin/gstack-paths` stays untouched. A2 (`597e483`): its `_QUOTA_SIG` (`bin/gstack-codex-probe` around line 287) and its tests (`test/codex-model-probe.test.ts`) map rate limits, "too many requests" and 429 to quota; split them per CEO-14, keep the reason name `quota_exhausted` everywhere (probe, `lib/gate-outcomes.ts`, docs anchor `codex-quota-exhausted`), and add the `rate_limited` row. A3 (`a853e41`): replace its heredoc-based bodies with CEO-12's file-write mechanism, extend its lint to the structural rule, the allowlist grammars and every quoting shape, and add the hostile-payload tests. A4 (`ffde420`): add the batch and narrowed-loop `too_large` paths and the seam-injected boundary tests (CEO-2). A5 (`e77c532`): run CEO-22's acceptance scan. The Codex watchdog fix (`2e1d3e7`, freeze the command before reaping its children) is in the wave without a plan item; it gets a regression test and a CHANGELOG line under A2's Codex reliability heading. +- **CEO-19 (Tier 2 tests and fallbacks).** B7: a free test asserts the consent and publishing exception in the skill-start spawned block and both hook directives. B9: a free test on the generated ship text asserts the PR check comes before the Greptile dispatch; with no PR, `gh` missing or not logged in, or a non-GitHub remote, /ship skips the dispatch and records "Greptile: not run (); runs on the PR once it exists". B10: a free test asserts the new config-mismatch message text and the BROWSER.md WebGL line. B11: a free test with a stub `gbrain` covers code scope present, absent, gbrain below 0.60 and a probe error; below 0.60 or on a probe error, the current offer stays unchanged (no regression). +- **CEO-20 (B3 keeps its controls).** B3's exemption applies only in source contexts: a file whose `+++ b/` path is TypeScript or JSX, a value after a `:` type annotation, or a value inside `{...}`. A known secret prefix or a high-entropy check takes precedence over the exemption, so a YAML bare high-entropy token still flags. +- **CEO-21 (B8 refusal behavior, including upgrades).** Setup parse-checks every hook shim and TypeScript entry in the staged tree before it swaps the new install into place. Any failure aborts the swap, so the previous working install (and every registration pointing into it) stays active; setup prints the file and line and exits non-zero. A fresh install with a failing hook registers none of the failing hooks, registers the rest, and exits non-zero. Tests cover a fresh install and an upgrade over an existing install with live registrations, where the new tree has one unparseable shim. +- **CEO-22 (A5 acceptance).** #3040 closes only after osv-scanner, run with the workflow's `--config=.osv-scanner.toml` against both lockfiles on the wave head (or a dispatched weekly OSV workflow run), reports no unignored findings; the PR body links that run. +- **CEO-23 (what "fixed" means for a red case).** A6 and A7 report measurements as "observed k/n after fix at ", never as a proven rate. A red case is called fixed only when there is a named causal change and every failure in the closing measurement is explained; 9 of 10 alone does not close it. Longer-term reliability comes from the weekly census, reported separately. +- **CEO-24 (Windows claims match the evidence).** The PR states exactly what the Windows lane exercised. B12's Copilot interpreter choice and B5's actual Docker operation are reported as unverified until exercised on Windows. `.github/workflows/windows-setup-e2e.yml` gains one journey step after setup: start the browse server from the installed runtime root and pipe a two-command flow into `browse chain`, asserting real output (covers B2 and B4 end to end). +- **CEO-25 (B6 premise check).** Before rewording the router, one live Claude Code check confirms that a skill set to `"off"` in `skillOverrides` is absent from the session's skill list. If it is not absent, B6 drops the "route only to skills in the session's list" sentence and ships only the wording change. +- **CEO-26 (contributors hear early).** When each rewrite commit lands on the branch, the superseded contributor PRs (#3027, #3049, #3052, and #3039, #3045, #3048, #3032, #3029, #3047, #3016 as adopted) get a comment linking the commit and the credit line; they are closed at merge as planned. +- **CEO-27 (B11 decided on the first probe).** B11 ships only if the live gbrain probe succeeds on the first try against gbrain 0.60 or later; otherwise it goes to TODOS.md (CEO-6) without further investigation in this wave. +- **CEO-28 (A1 keeps sourcing, with the reason recorded).** A1 keeps sourced helpers because skills call their shell functions (`_gstack_codex_timeout_wrapper`, `_gstack_codex_select_model`, `_gstack_egress_run`) across a block and read the variables they set; `${(%):-%x}` is chosen over `functions_source` for simplicity, not version support. Turning the probe into an executed subcommand (`gstack-codex-probe select-model exec` printing its status lines) goes to TODOS.md as the next step that removes the shell dependency. + + + + + +#### 0H. CEO plan and spec review loop + +- CEO scope summary: `/home/user/.gstack/projects/garrytan-gstack/ceo-plans/2026-10-06-fix-wave-oct6.md` (written, read back). Amendment checkpoint: `/home/user/.gstack/projects/garrytan-gstack/autoplan-ceo-fgTQov/ceo-implementation.md`. Final spec input: `/home/user/.gstack/projects/garrytan-gstack/autoplan-ceo-7cqX6C/ceo-implementation.md` (111 lines, read in full). +- Iteration 1 (score 6, FAIL, 19 issues): P3 anchors missing for B1/B2/B4/B5; Windows tests unnamed; AGENTS.md cheap gates missing; batch-path cap and hard-coded "32 MiB"; A4 text contradicted CEO-2; amendments had no heading; A6 units/defaults undefined; eval-command lookup; CEO-3 kind; CEO-4 branches; Tier 2 "green" undefined; A3 lint narrower than P2; B5 side note open-ended; CEO-1 feasibility. Fixed by CEO-2 rewrite, CEO-6 to CEO-13, the A4 baseline edit and the amendments heading. +- Iteration 2 (score 7, FAIL, 21 issues): owned-read path through `ownedParentTranscript`; transient 429 cached as quota; A7 repair-limit exit; no-history spend estimate; heredoc delimiter collision; nested sources; B8/B10 anchors; B6's two texts; drift from #3034 wording (file list, per-trial cap); main-vs-head measurement; CEO-3 fixture detail; Ubicloud signal; timing statistic; B8 interpreter; B1 shapes; B4/B5 mechanics; braces date; B2 TTY; boundary-test cost; CEO-3 stopping point. Fixed by revised CEO-1/2/3/4/7/9/12 and CEO-14 to CEO-17. +- Iteration 3 (score 6, FAIL, 26 issues; cap reached): stale base (#3033 merged at `55f8151`; A1 `f0531a2`, A3 `a853e41`, A2 `597e483` landed on the branch during this review), other quoting shapes, `rate_limited` exit code and mid-review classification, auth-probe and bash-only constructs, OSV acceptance, Tier 2 tests/fallbacks, TODO cross-refs, heredoc check ownership, summary drift, Tier 1 exception, A2 commit contradicting CEO-14 and its reason name, amendments not folded into items, 64 MiB step, CEO-3 parallel slot/kind/start, CEO-12 host/temp path, anchor source of truth, idiom text, B8 refusal mode, CEO-12/CEO-16 summary scope, `gstack-paths` is executed not sourced, B3 control conflict, Ubicloud retry inflation and VM layout, per-call cost. After the loop, CEO-18 to CEO-22, revisions to CEO-1/2/3/6/7/11/12/14/15 and five more baseline edits (base, A1 `gstack-paths`, A2 match lines, A2 reason rows, A3 class guard) address 25 of 26; not reviewer-confirmed. Open: amendments still override item text via the heading rule rather than being folded into A6 and the Tier 2 items. +- Metrics appended to `analytics/spec-review.jsonl`: iterations 3, issues_found 66, issues_fixed 38 (iteration 1 and 2 issues not re-raised by the next review), remaining 26, quality_score 6. +- Document approval (0H A/B/C): auto-decided **A) Approve these documents and continue to 0I** (P6; both documents reflect the decisions). Approves document versions only. + +#### 0I. Temporal interrogation + +``` + HOUR 1 (foundations): #3033 merged (55f8151) and A1/A2/A3 are already committed; start from CEO-18's rework list, + not from scratch. Read lib/gate-outcomes.ts + test/troubleshooting-anchors.test.ts before + adding any message (CEO-7). Install zsh locally (present on the Capy image: zsh 5.9). + HOUR 2-3 (core logic): Ambiguities: which placeholders go on CEO-12's allowlist (expect dozens of path/branch + placeholders); where /ship's eval step sits in ship/sections/tests.md.tmpl (Step 6.3, line ~108) + and how CEO-1's shard file list is recovered from the free runner's output. + HOUR 4-5 (integration): Surprises: the A2 preflight edit regenerates every skill with an outside voice (golden and + parity-suite churn); B6 wording touches tests pinned in two places; B7 must stay out of the + static preamble; #3033's edits to ship/review sections may move anchors the A3 lint reads. + HOUR 6+ (polish/tests): They will wish they had: the A7 replay fixture captured before any paid trial; the CEO-2 + timing numbers recorded; Windows tests registered in scripts/lib/windows-curation.ts early so + the Windows lane runs them on the first push. +``` + +Effort: human team about 2 weeks; CC + gstack about 6-8 hours of implementation plus paid runs (A6 seeded case panel, A7 10-trial measurement, B6 routing panel, PR gate). Feasibility blockers: none. Pending choices: none beyond the taste decisions surfaced at the gate. + +| ID and owner | Contract and evidence | Current | Proposed | Status | Exact approval and scope | +|---|---|---|---|---|---| +| CEO-D14 (0H spec loop) | Spec findings, iterations 1-3 | plan as amended | CEO-7 to CEO-22 + baseline edits | approved | P1/P5 per finding; auto-decided | +| CEO-D15 (CEO-4/CEO-11) | A7 may stop red after 3 rounds; wave continues | A7 Tier 1 holds PR | A7-only exception | approved (taste, provisional) | P6; surfaced at gate (alternative: PR waits for A7) | +| CEO-D16 (CEO-14) | 429 vs quota | committed code maps 429 to quota | split, `rate_limited` uncached | approved | P1; requires rework of `597e483` | +| CEO-D17 (CEO-16) | B6 texts | router only | router + injection + gstack CLAUDE.md | approved (taste, provisional) | P1/P5; never rewrite user files | +| CEO-D18 (0H approval) | Documents | n/a | approve | approved | auto-decided A | + +#### Dual voices (CEO) + +- Voice input: `/home/user/.gstack/projects/garrytan-gstack/autoplan-ceo-243P7X/ceo-implementation.md` (sha256 `097e02e4...4acb`), created after the spec loop. +- Native CEO voice (Capy subagent, nativeDispatchPrompt verbatim): completed; returned `INPUT: ceo 097e02e48d86a22b2f0dc2b58ec51ab4da2d35492a4920a1cb294454dcdc4acb` (matches). Findings: (1) CRITICAL the zsh fix is held by a 19-item PR because A6 is Tier 1; (2) HIGH A1 fights sourcing instead of removing it; (3) HIGH raising A4's cap only postpones the cliff; (4) HIGH CEO-12's lint is a heuristic shell parser and the heredoc fallback relies on model compliance; (5) MEDIUM 9/10 is not a measured rate; (6) MEDIUM plan drifted from the branch (watchdog `2e1d3e7`, `OWNED_TRANSCRIPT_MAX_BYTES` already in tree); (7) MEDIUM contributor goodwill; (8) MEDIUM Tier 2 is broad (B6, B11, B10). Premises it flagged as assumed: Capy machine as CI class, every host has a file-write tool, `` covers all untrusted text, `skillOverrides` reflected in the session skill list. Bottom line: approve with 1-4 applied; Garry decides on a hotfix split. +- Codex CEO voice (gpt-6-astra, `codex exec`, read-only sandbox): `OUTSIDE_STATUS: completed provider=codex host=claude`, exit 0. Full output: + +```text +1. **High — The release bundles emergency repairs with a new product policy.** A3 addresses command execution; A5 addresses reported vulnerable dependencies. Yet CEO-11 lets A6's new eval workflow hold both indefinitely. Separate commits do not provide independent release or rollback boundaries. The plan never establishes why users exposed to those defects should wait for generic flake management. + **Fix:** Ship the security and blocking compatibility repairs independently. Put A6 and the associated eval-policy changes in a separate PR with their own acceptance criteria. +2. **Critical — P2's security promise still depends on the agent handling hostile text correctly.** CEO-12 exempts paths and branch names by category, although repository-controlled names can contain shell syntax. Its fallback also asks the model to notice a heredoc delimiter collision before executing the payload. That leaves the security boundary dependent on the same agent composing the command. Template lint cannot establish that runtime values remain data. + **Fix:** Require a structured data path into a helper that invokes commands with argument arrays. Validate any exempt identifier against an explicit grammar. Where safe text transport is unavailable, leave the operation unsent. Test hostile payloads through the actual execution path, including filenames and delimiter collisions. +3. **High — "Nine out of ten" is being promoted from a diagnostic observation into a repair verdict.** A7 closes the issue at 9/10, although `auto-decide-preserved` is classified as a rule case (test/helpers/touchfiles-data.ts:1708). Even assuming independent trials, an 80%-reliable implementation has approximately a 38% chance of passing that threshold. Labeling the runs "diagnostic" protects the historical record; it does not justify declaring the defect fixed. + **Fix:** Use the measured rate to guide investigation. Require a demonstrated causal repair and explain every remaining failure before closing a rule defect. Report longer-term reliability separately from repair acceptance. +4. **Medium — The free-flake measurement changes the conditions it claims to measure.** CEO-1 prescribes ten concurrent reruns on one VM. Without resource and state isolation, that can measure contention introduced by the experiment instead of the original failure. The existing Ubicloud wrapper (scripts/ubicloud/test-free.sh:13) also explicitly enables retries, so this requires an executable runner change, not just workflow wording. + **Fix:** Preserve the original concurrency and environment for baseline measurements. Give parallel trials isolated state and sufficient resources; label deliberate contention tests separately. Prove the diagnostic launcher disables retries and preserves every outcome before making this mandatory for users. +5. **High — B8 protects new registrations while leaving affected customers blocked.** CEO-21 explicitly preserves existing registrations when their implementation fails parsing. Existing hooks point into the installed tree, and the current healer retains hooks based on executable availability (bin/gstack-settings-hook:815). An executable file with invalid syntax can therefore remain registered after setup detects the problem. A nonzero setup exit does not restore the user's session. + **Fix:** Make upgrade recovery part of acceptance. Validate staged hook implementations before activation and retain the previous working implementation on failure. For an already broken installation, provide a deterministic recovery path that preserves required enforcement. Test an upgrade with an existing registration, not only fresh installation. +6. **High — "Windows verified" exceeds the proposed evidence.** B2 uses a stub daemon; B4 proves module resolution; B5 proves executable binding; B12 proves a sentence exists. Those checks do not establish that an installed user can launch Chromium, execute a piped flow, or have Copilot choose the correct interpreter. Supporting more hosts without proving their first successful workflow creates recurring onboarding failures and gives users a reason to choose simpler host-native tooling. + **Fix:** Add a Windows acceptance journey from installation through a real browser operation and piped chain, including an upgrade. Report Copilot execution and Docker operation as unverified until exercised; generated wording is not behavioral coverage. +7. **Medium — A4 postpones failure for the users most invested in the product.** CEO-6 acknowledges the need for bounded reading but leaves it as an undated TODO. Choosing a larger cap from Linux median timings preserves the same eventual session cliff. The reader still loads the entire journal and parses its history (lib/claude-public-transcript.ts:560); restarting abandons accumulated context precisely when orchestration should be most valuable. + **Fix:** Ship the accurate diagnosis as the immediate repair, but assign a dated follow-up for bounded or incremental reading that preserves ownership checks. Measure complete phase-entry latency and peak memory on supported developer environments, and make long-session completion the product acceptance criterion. + +Recommendation: Split the wave and revise acceptance criteria because urgent repairs currently depend on an unproven eval policy while shell safety, upgrade recovery, and Windows usability remain insufficiently demonstrated. +``` + +(The validator also printed `VERDICT: clean` / `FINDINGS: none` because the CEO voice output carries no `[P0]`-style severity tags; the findings above are real and integrated below. Markdown links in Codex's output were flattened to `path:line` text here.) + +``` +CEO DUAL VOICES — CONSENSUS TABLE: + Dimension Claude Codex Consensus + 1. Premises valid? yes, 4 assumed yes; P2 premise CONFIRMED (core valid; P2 and B6 premises need proof) + 2. Right problem to solve? yes yes (execution) CONFIRMED + 3. Scope calibration correct? no: A6 holds A1-A5 no: split A6 out CONFIRMED concern -> USER CHALLENGE UC1 + 4. Alternatives sufficiently explored? no: exec helpers, no: arg-array CONFIRMED gap (taste T5, T8) + structural lint helper, staged hooks + 5. Competitive/market risks covered? no: contributor no: Windows first CONFIRMED gap (different risks; CEO-24, CEO-26) + goodwill journey + 6. 6-month trajectory sound? no: A4 cliff, 9/10 no: A4 cliff, 9/10 CONFIRMED concern (CEO-2 revised, CEO-23) +CONFIRMED = completed subagent + outside; primary cannot replace outside. +``` + +Integration (each finding's disposition): +- Split A6 out / hotfix A1-A5 first (Claude 1 + Codex 1): both models recommend changing the user's one-PR, A6-in-Tier-1 structure. **User Challenge UC1**, not auto-decided; the original direction stands. Factual note for the gate: CEO-3 already runs A6's paid case in the periodic tier, so the paid case cannot turn a PR check red; what can hold the PR is A6's free tests and its /ship prose. +- A4 cap (Claude 3 + Codex 7): both reject raising the cap on timing. CEO-2 revised: cap stays 32 MiB, batch and narrowed-loop paths fixed, P1 dated follow-up with a long-session acceptance. Reverses CEO-D6 (X1 now DEFERRED). Mechanical (both voices + P5). +- 9/10 (Claude 5 + Codex 3): CEO-23 added. Mechanical. +- P2 structural rule and no heredoc fallback (Claude 4 + Codex 2): CEO-12 rewritten (structural rule, identifier grammars, unsent on failure, hostile-payload tests). The argument-array helper (Codex 2) is the stronger form: **taste T8**, deferred to TODOS (CEO-6). +- Free-flake isolation and runner retry (Codex 4): CEO-1 revised. Mechanical (P1). +- B8 upgrade recovery (Codex 5): CEO-21 rewritten to validate the staged tree before swap. Accepted (P1; in blast radius of B8). +- Windows evidence (Codex 6): CEO-24 (honest PR claims + one windows-setup-e2e journey step). **Taste T6** for the journey step. +- A1 executed subcommands (Claude 2): **taste T5**; recommend keep sourcing (already committed, skills call shell functions across a block) and record the reason plus a TODO (CEO-28). +- Plan drift (Claude 6): Context and CEO-18 updated with every commit on the branch at review time; the watchdog commit gets a test and CHANGELOG line. +- Contributor goodwill (Claude 7): CEO-26. Mechanical (P6). +- Tier 2 breadth (Claude 8): B11 pre-declared (CEO-27, mechanical). Deferring B6 is **taste T7**; recommend keep (real user bug #3018, routing panel already required). +- B6 premise (Claude premises): CEO-25. Mechanical. + +#### Section 1: Architecture Review + +Current scope: mode SELECTIVE EXPANSION (autoplan override). Accepted: Tier 1 A1-A7, Tier 2 B1-B12, amendments CEO-1 to CEO-28 (CEO-D1..D18 plus the voice dispositions above). Deferred: B9 full Greptile-in-parallel, bounded owned-journal read (P1), executed-subcommand probe, argument-array posting helper. Rejected: X3 (quota cache until reset), X4 (duplicate telemetry TODO). Pending at the gate: UC1 and taste T1-T8. + +``` + generated SKILL.md blocks (bash or zsh, Claude Code / Codex / Copilot hosts) + | | | + source/. | gh/glab/git/curl | PreToolUse hooks | ./setup + v v v + bin/gstack-codex-probe --(nested .)--> gstack-state-root.sh mktemp file <-- agent file-write tool (A3/CEO-12) + | self-locate idiom (A1) | + | _gstack_codex_model_probe --> quota_exhausted | rate_limited (A2/CEO-14) --> lib/gate-outcomes.ts rows --> docs anchors (CEO-7) + v + codex exec --> outside-review-result.ts classifyOutsideReview (stderr: quota/rate) + bin/gstack-egress-lib.sh (A1) autoplan/bin/phase-publication-hook.ts + |-- shellInitAttempt diagnostic (B1) + ship/sections/tests.md.tmpl '-- readOwnedClaudePublicTranscript -> too_large (A4/CEO-2) + |-- measure loop (A6, CEO-1/3/9/23) --> scripts/test-paid-shards.ts --case / test:ubicloud (retry off) + '-- Greptile PR check (B9) + setup --> staged tree parse check (B8/CEO-21) --> swap --> gstack-settings-hook add-event + --> Windows junction + Node/Playwright check at runtime root (B4) --> GSTACK_CSO_TRUSTED_DOCKER (B5) + browse/src/cli.ts chain stdin (B2), config-mismatch text (B10); lib/redact-patterns.ts (B3); router + skill-start (B6/B7) +``` + +New coupling: `lib/gate-outcomes.ts` gains two Codex-specific rows (justified: it is the one reason table). The A6 loop couples /ship to `scripts/test-paid-shards.ts --case` only through the documented-command discovery (CEO-9), so other projects are not coupled to gstack's runner. Scaling: not applicable to a CLI/skill toolkit beyond the hook's per-phase-entry journal read (bounded at 32 MiB). Single points of failure: the codex probe (every outside voice routes through it; A1 and A2 are its two failure classes) and setup's hook registration (B8). Rollback: each item is its own commit; revert the commit and re-run `./setup` (minutes). Security architecture: no new endpoints; the new trust decisions are Docker binding (B5, allowlisted directories) and free-text transport (A3). Production failure per integration point: Codex 429 (handled, CEO-14), journal growth (handled with explicit `too_large`, follow-up P1), Windows junction stale (CEO-17), staged hook parse failure (CEO-21). Elegant-to-a-new-engineer: one self-locate block, one reason table, one structural rule for free text. Platform: the measure loop and the reason table are reusable by every later skill. Issues found: 3 (hotfix coupling -> UC1; codex probe as single point of failure, accepted as is with A1/A2 guards; staged hook swap, CEO-21). + +#### Section 2: Error & Rescue Map + +``` + METHOD/CODEPATH | WHAT CAN GO WRONG | CLASS / REASON + ----------------------------------------|---------------------------------------|--------------------------- + codex-probe self-locate (A1) | sourced from zsh, nested function | wrong dir -> CODEX_MODEL invalid + _gstack_codex_model_probe (A2) | usage limit / insufficient_quota | quota_exhausted (exit 4) + | HTTP 429 / rate limit | rate_limited (exit 0, unverified) + | timeout 124 | inconclusive (unchanged) + | stale cache after reset | quota cache 15 min TTL + classifyOutsideReview stderr (A2) | quota or 429 mid-review | unavailable (quota_exhausted|rate_limited) + generated gh/glab posts (A3) | file-write tool missing | post unsent, stated + | identifier fails grammar | value not used, stated + readOwnedClaudePublicTranscript (A4) | journal > 32 MiB (owned/snapshot/loop) | too_large (hard cause) + osv-scanner (A5) | new advisory before merge | scan red -> fix or justified ignore + /ship measure loop (A6) | no single-case command | ask once, record + | no cost history / over $2 per trial | ask once + | 3 rounds without 9/10 | named-red report, stop + | Ubicloud unavailable | local reruns + phase-publication-hook (B1) | shell-built init | specific denial + literal rerun + browse chain (B2) | TTY or empty stdin | usage error, exit 1 + setup junction (B4) | stale junction / real node_modules | replace / leave + report + CSO docker binding (B5) | docker.exe outside allowlist | not bound; anchor names fix + setup hook parse (B8) | unparseable shim on upgrade | swap aborted, old install active, exit != 0 + Greptile dispatch (B9) | no PR / gh missing / GitLab | skipped, recorded with reason + sync-gbrain offer (B11) | gbrain < 0.60 / probe error | current offer unchanged + + EXCEPTION CLASS / REASON | RESCUED? | RESCUE ACTION | USER SEES + ---------------------------|----------|-------------------------------------------------|------------------------------- + quota_exhausted | Y | skip remaining Codex calls; cache 15 min | Codex's line + reset time + anchor + rate_limited | Y | proceed unverified; review decides | verbatim line, "rate limited" + too_large | Y | hard deny with fallback | size, cap, start new session / run reviews by hand + post unsent | Y | stop that post | "body not sent: " + named-red (A6/A7) | Y | stop loop, PR body report | per-case table with every trial + swap aborted (B8) | Y | keep old install | file:line, exit non-zero + wrong dir under zsh (A1) | Y (fix) | idiom resolves at source time | correct CODEX_MODEL line +``` + +No catch-all handlers are introduced; every rescued state has a named reason and a visible message. GAPS: 0 after CEO-12/14/21 (before the review: 3 — 429 cached as quota, heredoc delimiter collision, broken registration kept on upgrade). + +#### Section 3: Security & Threat Model + +| Threat | Likelihood | Impact | Mitigated? | +|---|---|---|---| +| Untrusted reviewer/diff/error text runs as shell in reply or issue posts (A3) | High (templates run on every /review and /ship Greptile pass) | High (arbitrary command on the user's machine) | Yes: CEO-12 structural rule, no heredoc fallback, hostile-payload tests | +| Repository-controlled identifiers (branch names, paths) carry shell syntax | Medium | High | Yes: CEO-12 grammar validation before use | +| Docker binary hijack through a user-writable PATH entry (B5) | Medium on Windows | High (CSO runs scanners with credentials) | Yes: allowlisted install directories only (CEO-17) | +| Spawned session auto-grants consent or publishes data off-machine (B7) | Low-Medium | High (privacy) | Yes: exception added to runtime spawned block and hooks; free test (CEO-19) | +| Unparseable hook blocks every tool call (B8) | Low | High (session unusable) | Yes: staged parse check, swap aborted (CEO-21) | +| Dependency advisories (A5) | Present today | Medium | Yes: overrides + acceptance scan (CEO-22); `braces` ignore only if absent from shipped bundle | +| Redaction false positives hide real secrets after the rule change (B3) | Medium | High | Yes: source-context exemption, known-prefix/entropy precedence, true-positive controls (CEO-20) | +| Junction points runtime root at an unexpected directory (B4) | Low | Medium | Partly: junction target is the source checkout setup already trusts; stale junction replaced. No new privilege. | + +Issues found: 8 threats mapped; High severity before review: 2 (A3 heredoc fallback, B3 control conflict), both mitigated by accepted amendments. + +#### Section 4: Data Flow & Interaction Edge Cases + +``` + A2 probe: codex exec "reply OK" -> exit/out -> [124? inconclusive] -> [model 400/404? MODEL_UNUSABLE] + -> [broken install? exit 2] -> [usage limit|insufficient_quota? quota_exhausted, cache] + -> [429|rate limit? rate_limited, no cache] -> else inconclusive + shadow: empty out (inconclusive), quota line with exit 0 (MODEL_OK), cache from another model (signature miss) + A3 post: text -> agent file-write -> mktemp path -> gh --body-file + shadow: empty text (skill skips post), huge text (file, no argv limit), write failure (unsent), hostile text (never parsed) + A4 read: journal -> size check (cap) -> read -> parse -> ancestry -> ready + shadow: size > cap (too_large), changing during read (retry), malformed (retry), foreign cwd (hard cause) + A6 loop: red line -> classify -> estimate (history? else unknown -> ask) -> N trials (isolated state) + -> rate -> fix -> re-measure -> gate once -> PR table + shadow: no single-case command (ask once), 3 rounds (named red), cap hit (partial count) +``` + +Async ordering: the A6 parallel trials share the stub's slot directory; `mkdir` claims are atomic, so no two trials take one slot (CEO-3). Parallel free reruns share nothing after CEO-1's per-run HOME, state root and port range. Interaction edge cases: double-run of /ship while a measurement is running is serialized by the existing eval lock (`gstack-detach --lock gstack-evals`); B2 TTY with no args now exits instead of hanging (CEO-17). Edge cases mapped: 14; unhandled before review: 4 (TTY hang, parallel slot race, no-history estimate, narrowed-loop cap), all handled by CEO-3/9/17/2. + +#### Section 5: Code Quality Review + +Findings: (1) A3's committed heredoc approach duplicates a transport the file-write rule makes unnecessary; CEO-18 replaces it. (2) A2's committed `_QUOTA_SIG` mixes two failure classes into one signature; CEO-14 splits it. (3) B3's original two keyword heuristics replaced by one context rule (CEO-20). (4) No new method in the plan branches more than 5 times except the probe's classifier, which is already a flat if-chain with one exit per outcome; acceptable. (5) Over-engineering removed: the A4 timing protocol and the heredoc fallback (CEO-2, CEO-12). Issues found: 5, all dispositioned. + +#### Section 6: Test Review + +``` + NEW THING TYPE HAPPY FAILURE EDGE + A1 idiom + guard unit/integ bash+zsh from /tmp BASH_SOURCE outside idiom lint nested source, auth probe + A2 quota / rate unit (stub) MODEL_OK quota exit1 cached; 429 uncached quota line exit 0; timeout + A2 classifier unit clean review quota/429 in stderr both lines present + A3 structural rule lint+unit file-write post non-allowlisted placeholder hostile payload, bad branch + A4 too_large unit (seam) at cap reads cap+1 too_large x3 paths identity still identity + A5 OSV CI run scan clean new advisory braces in bundle + A6 loop free + paid free test pins steps named red after 3 rounds no history, no cmd + A7 replay free replay matches Step 0E class no match -> loop - + B1 shell-init unit shapes detected negative controls generic pipes/redirects + B2 chain stdin integ (win) piped flow runs TTY/empty -> exit 1 - + B3 redaction unit TS/JSX not flagged YAML token flagged concat literal + B4 junction integ (win) playwright resolves stale junction replaced real node_modules kept + B5 docker unit (win) allowlisted bound user-writable rejected - + B6 router gen + paid new wording routing panel green skillOverrides check + B7 spawned consent gen exception present x3 - - + B8 hook parse integ fresh install ok upgrade with bad shim aborts TS entry bad + B9/B10/B11/B12 gen/unit per CEO-19/CEO-8 fallbacks gbrain < 0.60 +``` + +Test ambition: the 2am-Friday test is A1's class guard under zsh plus A3's hostile-payload test; the hostile-QA test is a branch named `x;curl evil` through the generated Greptile reply; the chaos test is B8's upgrade with an unparseable shim. Flakiness risks: A6/A7 paid trials (by design measured, not retried); B4 on Windows CI (real filesystem). Pyramid: mostly unit/generated-text tests, three integration tests, two paid cases. Prompt/LLM changes: per CLAUDE.md, edits to ship, review, router, preflight resolvers and skill-start require `bun run gen:skill-docs`, `test/parity-suite.test.ts`, the affected paid cases (outside-voice cases for A2; routing panel for B6; the A6 seeded case; A7 measurement; the PR gate). Gaps found: 6 before amendments (Windows tests unnamed, B7/B9/B10/B11 untested, boundary-test cost, hostile payloads, upgrade path), all closed by CEO-8/19/2/12/21. + +#### Section 7: Performance Review + +The only hot path is the publication hook's journal read, which runs only on phase-entry Read/Agent calls (`candidate()` returns early otherwise) and stays capped at 32 MiB. The A6 loop's cost is bounded by N, the per-trial ask threshold and the 10-minute free-rerun cap. The A2 quota cache removes a paid round trip per skill while quota is exhausted. No database, connection pool or N+1 concerns apply. Issues found: 0 beyond the A4 cap decision already made. + +#### Section 8: Observability & Debuggability Review + +Every new failure state prints a reason and a doc anchor (CEO-7), Codex's own line is kept verbatim (A2), and A6/A7 write per-case tables with every trial (CEO-23). Gaps: per-reason telemetry for `quota_exhausted`, `rate_limited`, `too_large` is not added (existing P3 TODO, X4). The `autoplan-guard.jsonl` analytics log already records unverified entries; `too_large` denials are not logged there. Gaps found: 2 (telemetry deferred by existing TODO; `too_large` not logged, accepted as is because the denial is user-visible and reproducible). + +#### Section 9: Deployment & Rollout Review + +No migrations. Rollout is the normal release: `./setup` / `/gstack-upgrade`. Risk window: users on old installs keep the old probe until they upgrade; B8's staged parse check protects the upgrade itself. Rollback: revert the item's commit and re-run setup; B4's junction is replaced or removed by setup on the next run. Post-deploy checks: the weekly OSV run (A5), the next periodic census (A7), and the Windows lane (B2/B4/B5/B12). Risks flagged: 3 (upgrade over a broken install, Windows-only behavior unverified live, periodic A7 outcome after merge). + +#### Section 10: Long-Term Trajectory Review + +Debt introduced: the CEO-12 placeholder allowlist (maintenance), the deferred bounded read (P1), the deferred executed-subcommand probe. Path dependency: keeping sourced helpers (CEO-28) keeps the two-shell guard alive until the subcommand migration. Reversibility: 4/5 (all items revertable by commit; B8's staged swap and B4's junction touch install layout but setup restores them). Ecosystem fit: matches the repo's reason-table, generated-skill and lint conventions. The 1-year question: a new engineer finds one idiom, one reason table, one free-text rule. Retrospective on cherry-picks: X1 (raise cap) was wrong and is now deferred; X2 (free-suite flakes) needed isolation and retry-off to be honest. What comes next: the bounded journal read, the executed-subcommand probe, per-reason telemetry and the argument-array posting helper. Debt items: 4. + +#### Section 11: Design & UX Review + +SKIPPED (no UI scope). + +#### NOT in scope + +Deferred (TODOS.md entries written by the implementation PR, CEO-6): +- B9 full: push early and run Greptile in parallel during /ship (#3020). Reason: new flow outside blast radius (P3). +- Bounded or incremental owned-journal read (P1, next wave). Reason: both voices; needs a design that keeps ancestry checks. +- Executed-subcommand Codex probe (CEO-28). Reason: taste T5; A1 already committed. +- Argument-array posting helper (CEO-12's stronger form). Reason: taste T8; new helper surface. +- B11 if the first live probe fails; B5 side note if it does not reproduce; A7 P1 if it stops red; any reverted Tier 2 item. + +Rejected (no TODO): X3 quota cache until reset time (fragile parsing, P5); X4 per-reason telemetry in this wave (duplicate of the existing P3 TODO, P4). + +Carried from the source plan: #3037 (closes now that #3033 merged), #3012, #3021, Dependabot #2923/#2988/#3043. + +#### What already exists + +See 0B: the codex probe contract and cache, `GATE_OUTCOMES` and its anchor test, `classifyOutsideReview`, the generated-bash lint, `HARD_CAUSE`, `omitDisabledSkills`, `scripts/test-paid-shards.ts --case`, `eval:pass-rates`, `test:ubicloud`, setup's staged render/swap and `gstack-settings-hook add-event`, the CSO git-binding pattern, `scripts/lib/windows-curation.ts`, and `windows-setup-e2e.yml`. The plan reuses each; it builds no parallel mechanism. + +#### Dream state delta + +After this wave: shell portability is guarded for sourced helpers, free text never enters a shell command in generated skills, every new failure has a reason and an anchor, and a red eval is measured before a gate rerun. Still short of the 12-month ideal: helpers still depend on the caller's shell (until the executed-subcommand migration), long sessions still hit a journal cliff (until the bounded read), and per-reason telemetry does not yet tell us which failures users hit most. + +#### Error & Rescue Registry / Failure Modes Registry + +``` + CODEPATH | FAILURE MODE | RESCUED? | TEST? | USER SEES? | LOGGED? + ----------------------------|-------------------------------|----------|-------|-----------------------------|-------- + codex-probe (A1) | zsh self-locate | Y | Y | correct model line | n/a + codex-probe (A2) | usage limit | Y | Y | verbatim line + anchor | cache file + codex-probe (A2) | 429 | Y | Y | verbatim, unverified | N (by design) + outside review (A2) | quota/429 mid-run | Y | Y | unavailable (reason) | review log + generated posts (A3) | hostile text / bad identifier | Y | Y | unsent with reason | N + journal read (A4) | > 32 MiB | Y | Y | too_large + fallback | N (gap, accepted) + /ship loop (A6) | 3 rounds red | Y | Y | named-red table | PR body + periodic case (A7) | stays red | Y | Y | named-red, #3038 open | P1 TODO + hook init (B1) | shell-built init | Y | Y | specific denial | N + browse chain (B2) | TTY / empty / Windows pipe | Y | Y | usage error / real run | N + setup (B4) | stale junction | Y | Y | replaced / reported | setup log + CSO (B5) | docker outside allowlist | Y | Y | not bound + anchor | N + setup (B8) | bad shim on upgrade | Y | Y | swap aborted + file:line | setup log + ship (B9) | no PR / no gh / GitLab | Y | Y | "not run ()" | ship report + sync-gbrain (B11) | old gbrain / probe error | Y | Y | unchanged offer | N +``` + +Rows: 15. CRITICAL GAPS (RESCUED=N and TEST=N and silent): 0. + +#### CEO Implementation Tasks + +- [ ] **T1 (P1, human: ~3h / CC: ~15min)** — codex probe — split quota vs rate limit and add `rate_limited` row + - Surfaced by: Spec review 2 / CEO-14, CEO-18 + - Files: bin/gstack-codex-probe, lib/gate-outcomes.ts, lib/outside-review-result.ts, docs/troubleshooting.md, test/codex-model-probe.test.ts, test/outside-review-result.test.ts + - Verify: bun test test/codex-model-probe.test.ts test/outside-review-result.test.ts test/troubleshooting-anchors.test.ts +- [ ] **T2 (P1, human: ~1d / CC: ~45min)** — generated skills — structural free-text rule, identifier grammars, no heredoc fallback, hostile-payload tests + - Surfaced by: Section 3 / CEO-12, both voices + - Files: review/greptile-triage.md, scripts/resolvers/preamble/generate-test-failure-triage.ts, ship/sections/tests.md, spec/sections/gate-and-file.md.tmpl, test/helpers/generated-bash-lint.ts, test/env-host-fence-conformance.test.ts + - Verify: bun run gen:skill-docs && bun test test/env-host-fence-conformance.test.ts +- [ ] **T3 (P1, human: ~2h / CC: ~15min)** — transcript reader — batch and narrowed-loop too_large with seam tests; P1 bounded-read TODO + - Surfaced by: Spec review 1-2, both voices / CEO-2 + - Files: lib/claude-public-transcript.ts, test (autoplan publication guard / transcript tests), TODOS.md + - Verify: bun test test/autoplan-publication-guard.test.ts +- [ ] **T4 (P1, human: ~3h / CC: ~20min)** — A1 guard — auth-probe call, nested sources, bash-only-construct lint + - Surfaced by: CEO-15, CEO-18 + - Files: test/regression-issue3024-zsh-sourced-helpers.test.ts, bin/gstack-codex-probe + - Verify: bun test test/regression-issue3024-zsh-sourced-helpers.test.ts (with zsh installed) +- [ ] **T5 (P1, human: ~2d / CC: ~1.5h)** — /ship — measure loop with CEO-1/3/9/23 rules and seeded paid case + - Surfaced by: 0G X2, X6, X12 / CEO-1, CEO-3, CEO-9, CEO-23 + - Files: ship/sections/tests.md.tmpl, scripts/ubicloud/test-free.sh, docs/TESTING_INTERNALS.md, test fixture project, test/helpers/touchfiles-data.ts + - Verify: free ship-template test; paid seeded case panel of 3 +- [ ] **T6 (P1, human: ~1d / CC: ~30min + paid)** — A7 — replay run 37272185151 first, then measure on main and on the wave head + - Surfaced by: 0A-8 / CEO-4, CEO-23 + - Files: test fixtures for the replay, test/skill-e2e-auto-decide-preserved.test.ts (if fixed) + - Verify: free replay test; 10 trials alone with scripts/test-paid-shards.ts --tier periodic --case auto-decide-preserved +- [ ] **T7 (P1, human: ~3h / CC: ~20min)** — setup — staged hook parse check before swap, upgrade test + - Surfaced by: Codex 5 / CEO-21, CEO-17 + - Files: setup, test (setup hook registration) + - Verify: bun test on the setup hook-parse test (fresh + upgrade) +- [ ] **T8 (P2, human: ~4h / CC: ~30min)** — Windows — tests registered in windows-curation, journey step in windows-setup-e2e, honest PR claims + - Surfaced by: Spec review 1, Codex 6 / CEO-8, CEO-24 + - Files: scripts/lib/windows-curation.ts, .github/workflows/windows-setup-e2e.yml, browse/test/cli-chain-stdin.test.ts + - Verify: windows-free-tests and windows-setup-e2e workflows green +- [ ] **T9 (P2, human: ~3h / CC: ~20min)** — Tier 2 — B1 shapes, B3 context rule, B7/B9/B10/B11 tests and fallbacks, B6 premise check and both texts + - Surfaced by: Spec review 2-3, Claude premises / CEO-16, 17, 19, 20, 25, 27 + - Files: autoplan/bin/phase-publication-hook.ts, lib/redact-patterns.ts, bin/gstack-skill-start, hosts/claude/hooks/spawned-directive.ts, hosts/claude/hooks/auq-error-fallback-hook.ts, SKILL.md.tmpl, CLAUDE.md, ship/SKILL.md.tmpl, browse/src/cli.ts, BROWSER.md, sync-gbrain/SKILL.md.tmpl + - Verify: focused free tests per item; routing panel for B6 +- [ ] **T10 (P2, human: ~1h / CC: ~10min)** — release hygiene — OSV acceptance scan, doc anchors test, watchdog regression test, contributor comments + - Surfaced by: CEO-7, CEO-18, CEO-22, CEO-26 + - Files: test/troubleshooting-anchors.test.ts, docs/troubleshooting.md, docs/autoplan-guard-troubleshooting.md, CHANGELOG.md + - Verify: osv-scanner run linked in PR; bun test test/troubleshooting-anchors.test.ts + +#### Completion Summary (CEO) + +``` + +====================================================================+ + | MEGA PLAN REVIEW — COMPLETION SUMMARY | + +====================================================================+ + | Mode selected | SELECTIVE EXPANSION | + | System Audit | #3033 merged mid-review; A1/A2/A3/A4/A5 and | + | | a watchdog fix landed on the branch; B6 half | + | | already shipped (C8); #3033 overlap larger | + | Step 0 | 10 premise checks; 7 cherry-picks; 3 spec | + | | iterations (6, 7, 6); 28 accepted amendments | + | Section 1 (Arch) | 3 issues found | + | Section 2 (Errors) | 18 error paths mapped, 0 GAPS (3 closed) | + | Section 3 (Security)| 8 issues found, 2 High severity (mitigated) | + | Section 4 (Data/UX) | 14 edge cases mapped, 0 unhandled (4 closed) | + | Section 5 (Quality) | 5 issues found | + | Section 6 (Tests) | Diagram produced, 0 gaps (6 closed) | + | Section 7 (Perf) | 0 issues found | + | Section 8 (Observ) | 2 gaps found (both accepted/deferred) | + | Section 9 (Deploy) | 3 risks flagged | + | Section 10 (Future) | Reversibility: 4/5, debt items: 4 | + | Section 11 (Design) | SKIPPED (no UI scope) | + +--------------------------------------------------------------------+ + | NOT in scope | written (8 deferred, 2 rejected) | + | What already exists | written | + | Dream state delta | written | + | Error/rescue registry| 18 rows, 0 CRITICAL GAPS | + | Failure modes | 15 total, 0 CRITICAL GAPS | + | TODOS.md updates | 8 items proposed (written by impl PR) | + | Scope proposals | 21 proposed, 17 accepted (EXP + SEL) | + | CEO plan | written | + | Outside voice | codex completed; native completed | + | Lake Score | N/A (no coverage-scored questions) | + | Diagrams produced | 4 (architecture, error map, data flows, | + | | test map) | + | Stale diagrams found | 0 | + | Unresolved decisions | 9 (UC1 + taste T1-T8, held for the gate) | + +====================================================================+ +``` + +Scope tally: X1-X21 = 21 proposals; accepted 17 (X2, X6-X21), deferred 2 (X1 after both voices, X5), skipped 2 (X3, X4). Stale diagram audit: the files this plan touches carry no ASCII diagrams that the changes invalidate (checked `bin/gstack-codex-probe` contract comment, `lib/gate-outcomes.ts` header, `scripts/gen-skill-docs.ts` generation diagram); the probe's contract comment needs its two new outcomes added (part of T1). + + +### Phase 2: Design review + +Skipped: no UI scope detected (0 view/rendering terms in the plan). This is not a completed review; design outside voice status: skipped. + +### Phase 2.5: DX review (DX POLISH) + +#### Step 0: DX investigation + +- Pre-review audit: `git log --oneline -15` and `git diff --stat origin/main...HEAD` (8 commits on the branch: A1, A3, A2, union-size records, watchdog, A4, A5 plus the #3033 merge; 26+ files). README audience: founders/CEOs, first-time Claude Code users, tech leads. `package.json`: "Claude Code skills + fast headless browser". Design doc: this plan. Brain context: unreachable (cold). Prior DX reviews: none in the review log. +- Product type (auto-detected, P6): **Claude Code Skill pack with helper CLIs** (SKILL.md, Claude Code, agent, `./setup`, `bin/gstack-*`). Primary type for scoring: Claude Code Skill; secondary: CLI tool. + +``` +TARGET DEVELOPER PERSONA +======================== +Who: A founder or tech lead on macOS (default shell zsh) who installed gstack with the README's + one-line clone + ./setup, uses Claude Code daily, and has Codex signed in for outside voices. +Context: Runs /autoplan, /review and /ship on real branches; hits these failures mid-flow, not in setup. +Tolerance: One failed outside voice is tolerated; a wrong cause (model error, "inconclusive") costs a + debugging session (issue #3051) and erodes trust in every later "clean" verdict. +Expects: Upgrading fixes it; any failure names the cause and the one command that fixes it. +``` + +Persona chosen by P6 from README "Who this is for" and the reporters of #3024/#3051/#3050 (macOS, Claude Code host, long sessions). Secondary personas noted for passes: Windows Git Bash users (B2/B4/B5/B12) and contributors (CEO-26). + +Developer empathy narrative (first person; observed facts from issues and code, predictions labeled): + +> I upgraded gstack last week and ran /autoplan on a plan. Every Codex pass said the model was unusable, so I spent twenty minutes in `~/.codex/config.toml` before someone told me it is my shell: zsh, the macOS default, makes the probe look for `/../scripts/resolve-codex-generation-model.ts` (observed, #3024). Two days later my Codex allowance ran out and gstack said "inconclusive; proceeding" and pointed me at model errors again; the real message with my reset time was swallowed (observed, #3051). In a long session /autoplan refused Phase 1 with "retry this phase-entry tool", and retrying never worked because my journal was 52 MiB (observed, #3050). When I replied to a Greptile comment, the post said "Fixed in ." because my commit hash was in backticks (observed, #3046). After this wave, I expect (predicted): one upgrade, then the outside voice just runs on zsh; when it cannot, it prints Codex's own line with the reset time and a docs link; a long session tells me its size and the limit and what to do instead. + +Competitive DX benchmark (web search; boundaries differ, so DX choices are compared, not times): + +| Tool | Start -> result | Time + evidence type | DX choice | Source | +|---|---|---|---|---| +| OpenAI codex-plugin-cc (Claude Code plugin) | install -> first `/codex:review` | 4 slash commands + `/codex:setup` readiness check; time not reported | One readiness command (`/codex:setup`) that says whether Codex is installed and signed in, and offers to install it | github.com/openai/codex-plugin-cc README | +| codex-plugin-cc (npm lane) | `npm install -g` -> `codex-claude doctor` | 3 commands; reported | A `doctor --probe-runtime` command | npmjs.com/package/codex-plugin-cc | +| gstack today | upgrade -> first skill with a working outside voice | Never on macOS zsh (observed, #3024); on bash about the README's 30 s setup plus the first skill run | Readiness is a `CODEX_MODE:` line inside each skill's preflight; no standalone readiness command | README, `bin/gstack-codex-probe` | +| gstack after this wave | `/gstack-upgrade` (or `./setup`) -> first /review on zsh showing `CODEX_MODE: ready` | Estimated 2-3 min (upgrade + one skill preflight) | Same preflight line; every failure state has a reason and an anchor | this plan | + +TTHW (clock: persona with gstack already installed; start = run the upgrade; useful result = the first skill run whose outside voice completes or names a specific fixable cause): current = never completes on macOS zsh (outside voice always reports `model_unusable`); target = **Competitive (2-5 min)**, auto-decided (P5: reachable with existing commands, no new vehicle). Champion (< 2 min) would need a standalone readiness command; listed as an out-of-scope opportunity. + +Magical moment (P5, lowest-effort vehicle within DX POLISH): the first preflight after upgrading prints `CODEX_MODE: ready` on zsh; and when Codex cannot run, the same line names the real cause with Codex's own message (`quota_exhausted` with the reset time). Vehicle: the existing preflight output, no new command. + +Mode: **DX POLISH** (autoplan override). + +#### Developer journey map + +``` +STAGE | DEVELOPER DOES | FRICTION POINTS | STATUS +----------------|--------------------------------------------------|---------------------------------------------------|--------- +1. Discover | Reads the issue reply / CHANGELOG for the fix | Tracker hygiene closes issues at merge only; | fixed (CEO-26 comments early; issue + | | contributors hear late | close template names upgrade command) +2. Install | Runs /gstack-upgrade or ./setup | A broken hook in the new tree could block every | fixed (CEO-21 staged parse check) + | | tool call after upgrade (B8) | +3. Hello World | Runs /review or /autoplan; reads CODEX_MODE line | zsh -> model_unusable (A1); quota -> inconclusive | fixed (A1, A2, CEO-14); verify on zsh + | | (A2); 429 -> wrongly cached as quota (branch code) | in CI (A1 guard) +4. Real Usage | /ship with paid evals, Greptile replies, /autoplan| Blind gate reruns (A6); backticks run as shell (A3);| fixed (A6/CEO-1/3/9/23, CEO-12, A4/CEO-2; + | in long sessions | journal > 32 MiB (A4); Greptile with no PR (B9) | B9/CEO-19); long sessions still capped +5. Debug | Reads the error line and its anchor | Messages lacked anchors for B1/B2/B4/B5/B8 | fixed (CEO-7) +6. Upgrade | Next upgrade | `skillOverrides` users still see old router text | partially (CEO-16: new installs only; + | | in their committed CLAUDE.md | release note for existing users) +``` + +First-time developer report (grounded in the issues and current code): + +``` +FIRST-TIME DEVELOPER REPORT +============================ +Persona: macOS zsh founder (above) +Attempting: first /autoplan after upgrading to this wave + +CONFUSION LOG: +T+0:00 Runs /gstack-upgrade. Setup prints its summary; a staged-tree parse check passes silently. +T+0:40 Runs /autoplan. Preflight prints CODEX_MODEL: ... and CODEX_MODE: ready (A1). Before the wave + it printed CODEX_MODEL: invalid (observed #3024). +T+1:30 Codex allowance exhausted mid-week: preflight prints CODEX_MODE: quota_exhausted with Codex's + line "try again at Oct 10th, 2026 2:55 AM" and docs/troubleshooting.md#codex-quota-exhausted. + Predicted confusion: whether the review still counts. Answered: coverage reads "unavailable". +T+2:00 Long session over 32 MiB: guard denies with "journal is 52 MiB, over the 32 MiB limit; start a + new session or run the reviews by hand" (A4). Predicted confusion: losing session context; + mitigated only by the P1 follow-up (CEO-2). +T+3:00 Succeeded (fresh session) or switched to individual reviews; no misleading cause on the way. +``` + +Confusion points through the Decision gate: (a) "does a quota state count as a pass?" is answered by A2's existing contract (missing coverage, never a pass); no new decision. (b) long-session context loss is a known residual owned by the CEO-2 P1 follow-up; no new decision. + +#### Dual voices (DX) + +- Voice input: `/home/user/.gstack/projects/garrytan-gstack/autoplan-dx-LXchGa/dx-implementation.md` (sha256 `f71662d4...0159096`). +- Claude SUBAGENT (DX — independent review): completed; `INPUT: dx f71662d40d5c19bf21f0d812bb7ea762b109fb895035d078bf10d9da40159096` (matches). Findings: C1 (critical) A1's fallbacks still guess a directory (Claude-only default path in the probe, `$(pwd)` in egress-lib, bare-name sourcing) and the two committed blocks differ, so CEO-15's byte lint fails on `f0531a2`; H1 rate-limit token spelled three ways, no `rate_limited` anchor; H2 no escape from the 15-minute quota cache, committed HINT still says "usage or rate limit" with no anchor; H3 `too_large` message lacks its anchor and the /context-save recovery; H4 "left unsent" is a dead end and the branch grammar rejects valid refs; H5 measure-loop defaults have no named controls, Ubicloud spend is implicit, A6 "10" vs CEO-9 "12"; H6 B5 refusal has no recovery message; M1 A1 vs CEO-28 reason conflict; M2 silent local zsh skip; M3 no post-upgrade verification command; M4 setup partial-success output; M5 B4 next step; M6 temp-file cleanup; M7 users keep old router text; M8 `GSTACK_DISABLE_GPU=off` double negative; M9 no user-facing release notes. Scores: getting started 7, ergonomics 6, errors 7, docs 7, escape hatches 4. +- Codex SAYS (DX — developer experience challenge) (gpt-6-astra): `OUTSIDE_STATUS: completed provider=codex host=claude`, exit 0. Full output: + +```text +The plan addresses real DX failures, but I would revise it before approval. The 2–5 minute journey remains unproven, and the upgrade guarantee misses an existing update path. + +1. **Critical — B8's preservation guarantee needs to cover live Git updates.** CEO-21 requires validation before swapping installations, but the auto-updater changes the live checkout before running setup (bin/gstack-session-update:198). Existing hooks already point into that checkout; refusing registration afterward cannot preserve their old contents. + **Fix:** Validate the candidate before changing the live checkout, covering manual and automatic upgrades. Test an upgrade launched by the previously released updater, with existing registrations and an invalid incoming hook; verify the old hooks remain callable throughout. + +2. **High — No acceptance check proves time to hello world.** For this signed-in macOS persona, the intended journey should be upgrade → one small skill invocation, with a restart only if necessary. A1's helper tests do not exercise that journey. The README's 31-second setup measurement (README.md:58) covers Linux installation, not a useful outside review. + **Fix:** Specify one copy-paste-complete example and time the actual macOS zsh journey from upgrade start to completed outside voice or actionable failure. Record user steps, restart requirements and elapsed time. Exercise both successful review and quota failure. + +3. **High — A6 still holds the urgent repairs hostage.** The earlier CEO concern remains unresolved: A6 is Tier 1, and CEO-11 explicitly makes its failures block the entire PR. Developers whose outside reviews currently fail must wait for a new diagnostic workflow and its paid evaluation. + **Fix:** Ship A1–A5 independently of A6. Give A6 its own release and acceptance criteria; A7's investigation can proceed separately using the existing runner. + +4. **High — A6's cost controls are ambiguous at the point users need predictability.** A6 promises a per-case spend cap, but CEO-9 defines approval using *per-trial* cost without specifying the aggregate cap or its enforcement. At $1.99 per trial, a 12-trial behavior measurement costs $23.88 without crossing that approval threshold; additional measurements multiply it. + **Fix:** Define and display a total diagnostic budget covering baseline, repair measurements and panel scoring. Make the limit enforceable across parallel workers, expose its configuration, and stop with the named-red report when exhausted. Document this user-facing behavior outside `TESTING_INTERNALS.md`. + +5. **Medium — A2 can keep rejecting reviews after the prescribed fix succeeds.** The quota cache (bin/gstack-codex-probe:260) lasts 15 minutes. Adding credits or reaching the provider's reset time need not change its signature, yet the recovery instructions (docs/troubleshooting.md:165) imply the next attempt should work. + **Fix:** Print the cache's expiry and provide a documented "retry now" operation that bypasses this negative cache once. Honor a reliably parsed earlier reset time. Test recovery without changing credentials or model configuration. + +6. **Medium — Recovery instructions need to work from the developer's project.** CEO-7 guarantees that repository-relative anchors exist, but `docs/troubleshooting.md#…` does not identify an accessible page from an unrelated application checkout. Likewise, tracker instructions asking for `./setup --status` assume the correct working directory. A4's "new session" fallback also needs to explain how to retain the plan; resume and compact append to the same journal (docs/autoplan-guard-troubleshooting.md:31). + **Fix:** Emit clickable documentation URLs and commands using the resolved installation path. Give A4 a short recovery recipe: preserve the plan, start a genuinely new session, and invoke `/autoplan` with that plan's path. Check these instructions from an unrelated working directory. + +This was a read-only review; no skill files were read and no tests were run. + +Recommendation: Revise before implementation because live-update protection, measurable first-use success, and predictable recovery remain incomplete, while A6 still delays the urgent repairs. +``` + +(The validator also printed `VERDICT: clean` / `FINDINGS: none` because the output carries no `[P0]`-style tags; the findings are real and integrated. Markdown links were flattened to `path:line` text here.) + +``` +DX DUAL VOICES — CONSENSUS TABLE: + Dimension Claude Codex Consensus + 1. Getting started < 5 min? short (7/10) unproven, no timed DISAGREE -> resolved: DX-8 timed journey + journey + 2. API/CLI naming guessable? no: tokens, controls no: cost controls CONFIRMED gap (DX-2, DX-7) + 3. Error messages actionable? no: 4 dead ends no: cache escape, CONFIRMED gap (DX-1, DX-3, DX-4, DX-5) + links, A4 recipe + 4. Docs findable & complete? no: release notes no: repo-relative CONFIRMED gap (DX-2, DX-9) + anchors + 5. Upgrade path safe? mostly (M4, M7) no: auto-update DISAGREE -> resolved by code evidence: DX-6 + bypasses CEO-21 + 6. Dev environment friction-free? no: unknown shell, not assessed N/A (single voice; Claude critical C1 flagged) + local zsh skip +CONFIRMED = native + outside agree; primary cannot replace outside. DISAGREE -> taste. +``` + +Dispositions: both disagreements were settled by evidence rather than taste (Codex's auto-update finding is verified at `bin/gstack-session-update` lines 185-200: fetch, then `git merge --ff-only` on the live checkout before setup; the timed journey is Pass 8's own requirement). Single-voice critical flagged: Claude C1 (A1 fallbacks guess a directory), verified at `bin/gstack-egress-lib.sh` (`$(pwd)` fallback in `f0531a2`); accepted as DX-1. Codex 3 (A6 holds the urgent repairs) repeats CEO's **UC1**; it stays a User Challenge, now raised by both models in two phases. Codex 5's "honor a parsed reset time" reopens CEO X3: kept rejected (P5) with DX-3's retry-now bypass instead; surfaced as **taste T9**. Codex 4 and Claude H5 converge on DX-7 (named controls plus a total budget cap). The ship-skill's user-facing table and the `TESTING_INTERNALS.md` table both carry it. + +#### Passes 1-8 + +**Pass 1: Getting Started — 4/10 -> 8/10.** For this persona the "hello world" is the first working outside voice after an upgrade. Today it never arrives on zsh; the plan's A1 makes it one upgrade plus one skill run. Gaps found: no measurement of the journey (DX-8), no copy-paste verification for reporters (DX-9). A 10 would be a standalone readiness command like codex-plugin-cc's `/codex:setup`; that is a new command, so in DX POLISH it is listed as an out-of-scope opportunity, not added. The magical moment vehicle (the existing preflight line printing `ready`, or the real cause) is in the plan via A1/A2/DX-2/DX-3. + +**Pass 2: API/CLI design — 5/10 -> 7/10.** Status tokens were inconsistent (`rate limited` vs `rate_limited`, `quota` vs `quota_exhausted`); DX-2 and CEO-18 settle one token per state. A6's knobs had defaults but no names; DX-7 names gstack config keys consistent with existing `gstack-config` keys (`codex_reviews`, `auto_upgrade`). Ubicloud spend becomes opt-in (`ship_rerun_backend`). No established API is renamed (`GSTACK_DISABLE_GPU=off` stays; DX-9 explains it). + +**Pass 3: Error messages — 5/10 -> 8/10.** Three traced error paths, before and after: +- zsh probe: before `CODEX_MODEL: invalid` / `Module not found "/../scripts/..."` (wrong cause); after `CODEX_MODEL: (exec; ...)`, or on an unknown shell `gstack: cannot locate gstack-codex-probe (shell: ) ... export GSTACK_ROOT=... ` (DX-1). +- quota: before `MODEL_PROBE_INCONCLUSIVE ... see the codex skill's Error Handling entry` (wrong cause, message dropped); after Codex's own line with the reset time, cache expiry, `GSTACK_CODEX_PROBE_RETRY=1`, and the URL (A2, DX-3). +- long session: before `Native parent evidence has not reached the journal yet. Retry this phase-entry tool` (wrong cause, impossible fix); after size, limit, the /context-save recipe and the URL (A4, DX-4). +Every refusal now carries problem, cause, fix and a link (Tier 1/Tier 3 formula). Residual: messages are prose, not structured codes, which matches the rest of gstack. + +**Pass 4: Documentation — 6/10 -> 8/10.** Anchors existed only for some states and were repo-relative; DX-2 makes them absolute URLs and CEO-7's test enforces them. Release notes now carry a "What changes for you" list (DX-9). Residual: no copy-paste example of the measure loop outside the ship skill and TESTING_INTERNALS (DX-7 puts the table in both). + +**Pass 5: Upgrade path — 5/10 -> 8/10.** The auto-updater moves the live checkout before setup, so an unparseable incoming hook would block every tool call before setup can refuse it; DX-6 checks the incoming revision before the fast-forward and holds the update like `BUN_HELD`. Manual `git pull` users get DX-5's message. Router wording for existing users is a release-note one-liner (they own their CLAUDE.md). Versioning stays a patch bump. + +**Pass 6: Developer environment — 5/10 -> 7/10.** zsh is now tested in CI with a loud local skip (DX-1, A1); Windows items are tested in the curated subset plus one e2e journey step (CEO-8, CEO-24). Residual: Windows Copilot interpreter choice and Docker operation are unverified live (stated in the PR), and sh/dash sourcing fails loudly rather than working. + +**Pass 7: Community — 6/10 -> 8/10.** Contributors whose PRs are rewritten hear when the commit lands, with credit (CEO-26); issue closes include the upgrade command and a verification check (DX-9). No issues beyond these. + +**Pass 8: DX measurement — 3/10 -> 6/10.** Nothing measured the journey; DX-8 records one timed macOS zsh run and a CI check. No telemetry is added (per-reason telemetry is the existing P3 TODO). A recurring friction audit is not proposed (it would be a new process needing its own decision). + +Claude Code Skill DX checklist: AskUserQuestion design (unchanged; A6 asks only on the per-trial or no-history threshold, one question), state storage (probe cache under the state root; DX-3 documents its reset), progressive consent (B7 keeps consent interactive), auto-upgrade (DX-6 extends the existing hold), skill composition (unchanged), error recovery (DX-4, DX-5 preserve work), session continuity (DX-4 uses /context-save), bounded autonomy (DX-7 budget cap; B7 publishing exception). All checked; no new design decision needed. (The Hall of Fame reference was read in full in one Read, 127 lines, rather than one section per pass.) + +#### DX Scorecard + +``` ++====================================================================+ +| DX PLAN REVIEW — SCORECARD | ++====================================================================+ +| Dimension | Score | Prior | Trend | +|----------------------|--------|--------|--------| +| Getting Started | 8/10 | 4/10 | +4 ↑ | +| API/CLI/SDK | 7/10 | 5/10 | +2 ↑ | +| Error Messages | 8/10 | 5/10 | +3 ↑ | +| Documentation | 8/10 | 6/10 | +2 ↑ | +| Upgrade Path | 8/10 | 5/10 | +3 ↑ | +| Dev Environment | 7/10 | 5/10 | +2 ↑ | +| Community | 8/10 | 6/10 | +2 ↑ | +| DX Measurement | 6/10 | 3/10 | +3 ↑ | ++--------------------------------------------------------------------+ +| TTHW | 2-5 min (target, unmeasured) | never on zsh | +| Competitive Rank | Competitive (target) | +| Magical Moment | designed via existing preflight line | +| Product Type | Claude Code Skill (+ CLI) | +| Mode | POLISH | +| Overall DX | 7/10 | 5/10 | +2 ↑ | ++====================================================================+ +| DX PRINCIPLE COVERAGE | +| Zero Friction | covered (one upgrade; DX-6 protects it) | +| Learn by Doing | covered (copy-paste verification, DX-9) | +| Fight Uncertainty | covered (problem + cause + fix + URL) | +| Opinionated + Escape Hatches | covered (DX-3, DX-7 defaults + keys) | +| Code in Context | covered (exact commands in refusals, DX-5) | +| Magical Moments | covered (preflight shows ready or real cause) | ++====================================================================+ +``` + +Pre-fix ("initial") scores are the plan as submitted to Phase 2.5 judged against the persona; post scores assume DX-1 to DX-9. DX Measurement stays below 8 (critical DX debt threshold is 6): flagged, with telemetry deferred to the existing TODO. + +#### DX Implementation Checklist + +``` +DX IMPLEMENTATION CHECKLIST +============================ +[ ] Time to hello world < 5 min (upgrade -> CODEX_MODE: ready on zsh), measured once on macOS (DX-8) +[x] Installation is one command (existing ./setup; upgrade via /gstack-upgrade) +[ ] First run produces meaningful output: preflight prints ready or the real cause (A1, A2, DX-2) +[ ] Magical moment delivered via the existing preflight line +[ ] Every error message has: problem + cause + fix + absolute docs URL (CEO-7, DX-2..DX-5) +[ ] Status tokens are one spelling per state (DX-2, CEO-18) +[ ] Every A6 parameter has a default and a named config key (DX-7) +[ ] Docs have copy-paste verification commands that work (DX-9) +[ ] Examples show the real failure states (quota with reset time, too_large recipe) +[ ] Upgrade path: auto-updater holds an update with an unparseable hook (DX-6); setup reports partial success (DX-5) +[ ] Release notes list "What changes for you" (DX-9) +[n/a] TypeScript types (no new public API) +[ ] Works in CI without special configuration (zsh in CI images, macOS CI probe check) +[n/a] Free tier / credit card (open source; Ubicloud spend opt-in via DX-7) +[ ] CHANGELOG entry maintained +[n/a] Docs search (GitHub-hosted docs) +[x] Community channel exists (GitHub issues; CEO-26 early contributor comments) +``` + +#### NOT in scope (DX) + +- Standalone readiness command (`gstack doctor` or a Codex row in `./setup --status`) to reach a Champion TTHW: a new command, out of scope in DX POLISH; TODOS.md entry written by the implementation PR. +- Parsing Codex's reset time to shorten the quota cache (Codex 5): kept rejected (P5, taste T9); DX-3's retry-now bypass covers recovery. +- Per-reason telemetry for the new states: existing P3 TODO. +- Renaming `GSTACK_DISABLE_GPU`: established contract; DX-9 clarifies wording instead. +- Rewriting users' committed CLAUDE.md routing text: never done; release-note one-liner instead. + +#### What already exists (DX) + +`docs/troubleshooting.md` anchors and `test/troubleshooting-anchors.test.ts`; the hook's absolute GUIDE URL pattern; `docs/autoplan-guard-troubleshooting.md` hard-denial table (already lists `too_large` in the working tree); `/context-save` and `/context-restore`; `gstack-config` keys; the auto-updater's `BUN_HELD` hold; `./setup --status`; the issue-close template in Tracker hygiene; `scripts/resolvers/runtime-root.ts`'s `GSTACK_ROOT`. + +#### DX Implementation Tasks + +- [ ] **T1 (P1, human: ~3h / CC: ~15min)** — sourced helpers — unify the self-locate block, `GSTACK_ROOT` fallback, loud failure, unknown-shell and bare-name tests + - Surfaced by: DX Claude C1 / DX-1 + - Files: bin/gstack-codex-probe, bin/gstack-egress-lib.sh, test/regression-issue3024-zsh-sourced-helpers.test.ts, docs/troubleshooting.md + - Verify: bun test test/regression-issue3024-zsh-sourced-helpers.test.ts under bash and zsh +- [ ] **T2 (P1, human: ~2h / CC: ~15min)** — codex probe — `rate_limited` token, retry-now bypass, cache expiry line, rewritten HINT + - Surfaced by: DX H1, H2, Codex 5 / DX-2, DX-3 + - Files: bin/gstack-codex-probe, lib/gate-outcomes.ts, docs/troubleshooting.md, test/codex-model-probe.test.ts + - Verify: bun test test/codex-model-probe.test.ts test/troubleshooting-anchors.test.ts +- [ ] **T3 (P1, human: ~4h / CC: ~25min)** — auto-updater — parse-check the incoming revision before fast-forward; hold like BUN_HELD + - Surfaced by: Codex DX 1 / DX-6 + - Files: bin/gstack-session-update, test (session-update fixture remote) + - Verify: bun test on the session-update hold test +- [ ] **T4 (P2, human: ~3h / CC: ~20min)** — messages — A4 recipe, refusals with exact commands, B5/B4 recovery lines, absolute URLs, temp cleanup, `git check-ref-format` + - Surfaced by: DX H3, H4, H6, M4, M5, M6, Codex 6 / DX-2, DX-4, DX-5 + - Files: autoplan/bin/phase-publication-hook.ts, lib/claude-public-transcript.ts, review/greptile-triage.md, scripts/resolvers/preamble/generate-test-failure-triage.ts, setup, lib/cso/process.ts, docs/autoplan-guard-troubleshooting.md + - Verify: focused free tests per message; anchor test +- [ ] **T5 (P1, human: ~3h / CC: ~20min)** — /ship — named measure-loop controls, total budget cap, opt-in Ubicloud, unmeasured skip + - Surfaced by: DX H5, Codex 4 / DX-7 + - Files: ship/sections/tests.md.tmpl, bin/gstack-config (key docs), docs/TESTING_INTERNALS.md + - Verify: free ship-template test pins the table, keys, budget stop and unmeasured label +- [ ] **T6 (P2, human: ~1h / CC: ~10min + Mac time)** — journey measurement and release notes — timed macOS zsh run, CI zsh probe check, issue-close verification commands, "What changes for you" + - Surfaced by: Codex DX 2, DX M3, M7, M8, M9 / DX-8, DX-9 + - Files: .github/workflows (macOS job), CHANGELOG.md, BROWSER.md + - Verify: PR body records the timed run (or says unmeasured); macOS CI job green + + +- **DX-1 (A1 locates itself or says why it cannot).** The two helpers share one self-locate block with one resolution order: `${BASH_SOURCE[0]}` under bash or `${(%):-%x}` under zsh, made absolute with `$PWD` when relative; else `$GSTACK_ROOT/bin` when `GSTACK_ROOT` is exported and holds the helper (the variable the runtime prelude in `scripts/resolvers/runtime-root.ts` already uses); else the helper fails loudly instead of guessing: `gstack: cannot locate gstack-codex-probe (shell: ). Source it from bash or zsh, or export GSTACK_ROOT=. https://github.com/garrytan/gstack/blob/main/docs/troubleshooting.md#sourced-helper-location`. The Claude-only default path in `bin/gstack-codex-probe` and the `$(pwd)` fallback in `bin/gstack-egress-lib.sh` (both in commit `f0531a2`) are removed. The class guard adds an unknown-shell case and a bare-name (`source gstack-codex-probe` via PATH) case. CEO-15's byte-for-byte lint applies after this rework; the two blocks committed in `f0531a2` differ today and must be unified first. Outside CI, a missing zsh prints `zsh not installed: install zsh to run the sourced-helper guard (CI requires it)` and skips; under `CI=true` it fails, as A1 already states. +- **DX-2 (one token per state, absolute doc links).** The rate-limit state is spelled `rate_limited` everywhere: probe output `CODEX_MODE: unverified (rate_limited)`, probe state, gate reason and docs anchor `codex-rate-limited`. Its troubleshooting row says: at probe time the review still runs; a mid-review 429 means that review failed, so coverage is missing. Every new user-facing message prints a full `https://github.com/garrytan/gstack/blob/main/docs/.md#` URL (the pattern `autoplan/bin/phase-publication-hook.ts` already uses), so the link works from any project; CEO-7's anchor test checks these URLs. +- **DX-3 (quota cache has a way out).** The `quota_exhausted` output prints Codex's own line, when the gstack cache expires, and the retry-now command: `GSTACK_CODEX_PROBE_RETRY=1` bypasses the negative cache for one probe (documented in the anchor row, along with deleting `/.codex-model-probe`). The probe HINT committed in `597e483` ("usage or rate limit", no anchor) is rewritten to match. A stub test covers recovery: cached `quota_exhausted`, then `GSTACK_CODEX_PROBE_RETRY=1` with a stub that now succeeds, gives `MODEL_OK` without changing model, config or credentials. +- **DX-4 (A4 recovery keeps the user's work).** The `too_large` denial reads: "This session's journal is MiB, over the 32 MiB limit /autoplan can verify. Run /context-save, start a new session (resume and compact keep writing this journal), run /context-restore, then /autoplan ; or run /plan-ceo-review, /plan-devex-review and /plan-eng-review individually." followed by its full anchor URL. The docs row says the cap has no user override and why (the guard must read the whole parent journal to verify ownership). +- **DX-5 (no dead-end refusals).** When a CEO-12 post cannot be sent (file write failed, or an identifier failed its grammar), the skill prints the cause with the actual value, the path of the body file it composed, and the exact safe command to post it by hand (for example `gh pr comment --body-file `). Branch names are validated with `git check-ref-format --branch` instead of a narrow regex, so valid refs with `+`, `@` or non-ASCII characters are accepted. After a successful post the block deletes its temp file. B5's refusal names the `docker.exe` path it found, says a user-writable directory is untrusted for a child that carries registry credentials, gives the fix (install Docker Desktop under Program Files, or run /cso static-only), states it is not overridable, and carries its anchor. CEO-21's setup output lists the hooks it registered and skipped, says "this is a gstack bug; report :", and on an upgrade says "your previous install is still active". B4, when it leaves a real `node_modules` in place and the Node/Playwright check then fails, prints the command that fixes it and its anchor. +- **DX-6 (upgrades are protected before the checkout changes).** `bin/gstack-session-update` fast-forwards the live checkout before setup runs (`git merge --ff-only` around line 198), and Claude Code runs hooks straight from that checkout, so CEO-21's staged check alone cannot protect auto-updates. The updater parse-checks every hook shim and TypeScript entry at the incoming revision (from `git archive ` into a temp directory) before the fast-forward, using the same check as CEO-21. A failure holds the update, the same way the existing `BUN_HELD` hold works, and records the reason and file:line in the update status. A test runs the updater against a fixture remote whose incoming revision has one unparseable shim, with existing registrations, and asserts the checkout did not move and the old hooks still run. A manual `git pull` followed by `./setup` cannot be checked before the pull; setup reports it with DX-5's message. +- **DX-7 (measure-loop controls are named and capped).** A6's controls are gstack config keys with the CEO-9 defaults: `ship_measure_rule_trials` (10), `ship_measure_behavior_panels` (4), `ship_measure_judge_outputs` (10), `ship_measure_ask_per_trial_usd` (2), `ship_measure_budget_usd` (25: a total cap per red case across baseline, repair rounds and panel scoring, enforced by the coordinator before each batch, including parallel workers), `ship_measure_max_rounds` (3) and `ship_rerun_backend` (`local` by default; `ubicloud` only when set, so Ubicloud spend is opt-in). /ship prints the per-kind table (trials, pass bar, panels, budget) before it runs; the same table is in `docs/TESTING_INTERNALS.md` and in the ship skill's user-facing text. When the budget is exhausted, /ship stops with the named-red report. A documented skip ("this red is infrastructure, do not measure") labels the case `unmeasured` in the PR body; it never counts as a pass. A6's "default 10" means the rule-kind default; behavior and judge cases use CEO-9's per-kind counts. +- **DX-8 (the target journey is measured).** The PR records one timed run of the persona's journey on a macOS machine with zsh as the login shell: start of `/gstack-upgrade` (or `./setup`) to the first skill preflight that shows `CODEX_MODE: ready`, and a second run with a stub quota response that shows `CODEX_MODE: quota_exhausted` with the reset line; it records steps, any restart needed and elapsed time against the 2-5 minute target. The implementer uses Garry's paired Mac when available; without a Mac the PR says the journey is unmeasured. A free CI check on the existing macOS runner sources the probe under zsh and runs `_gstack_codex_select_model exec` (no paid call). +- **DX-10 (DX amendments govern where they differ from CEO ones).** Where a DX amendment and a CEO amendment differ, the DX amendment governs: DX-7's opt-in `ship_rerun_backend` replaces CEO-1's automatic Ubicloud choice when `UBICLOUD_API_KEY` is set; DX-5's `git check-ref-format --branch` replaces CEO-12's example branch regex; DX-2's `rate_limited` spelling replaces CEO-14's `rate limited`. +- **DX-9 (users can verify and learn what changed).** Each issue-close comment (Tracker hygiene) includes a copy-paste check with its expected output; for #3024: `zsh -c 'source ~/.claude/skills/gstack/bin/gstack-codex-probe && _gstack_codex_select_model exec'` should print `CODEX_MODEL: (exec; source: ...)`, not `invalid`. The CHANGELOG entry has a "What changes for you" list: the new messages and their links, `quota_exhausted` skipping Codex for the rest of the run, setup and the auto-updater refusing unparseable hooks, the router wording (with a one-line command users can run to update their own CLAUDE.md section), the B7 consent rule, and the DX-7 controls. B10's BROWSER.md line states the effect plainly: "set `GSTACK_DISABLE_GPU=off` to enable WebGL (`off` is the only recognized value)". + + + +### Phase 3: Eng review (FULL_REVIEW, last) + +#### Step 0: Scope Challenge + +Target: this plan (`docs/designs/FIX_WAVE_2026_10_06.md`), checked against the branch at `659b197` (17 commits on top of origin/main `55f8151`; the parent implemented most items while the review ran). Design doc: this plan. Prior learnings: none. Brain: unreachable. + +**A. What already exists, per sub-problem (from code reads):** + +| Item | Landed on branch | Existing code reused | Rework owed (accepted amendments vs landed code) | +|---|---|---|---| +| A1 zsh self-locate | `f0531a2` | BASH_SOURCE sites, `gstack-state-root.sh` | The two blocks differ: probe falls back to `$HOME/.claude/skills/gstack/bin` (bin/gstack-codex-probe:40) and `$PWD` for bare names; egress-lib falls back to `$(pwd)` (bin/gstack-egress-lib.sh:52). DX-1, CEO-15. | +| A2 quota | `597e483` | probe contract, cache, `GATE_OUTCOMES` | `_QUOTA_SIG` still includes `rate limit|too many requests|429` (bin/gstack-codex-probe:287); HINT says "usage or rate limit" (:294). CEO-14, DX-2, DX-3. | +| A3 safe bodies | `a853e41` | generated-bash lint | heredoc mechanism, lint scope (CEO-12, DX-5). | +| A4 too_large | `ffde420` | `HARD_CAUSE`, guard doc | batch/narrowed paths + seam tests (CEO-2); message recipe (DX-4). | +| A5 OSV | `e77c532` | overrides, `.osv-scanner.toml` | Landed three ignores: braces (planned), plus sprintf-js and katex (not in the plan), each `ignoreUntil = 2026-11-30` with reasons but no tracking-issue reference the file header requires; acceptance scan (CEO-22). | +| A6 measure loop | not started | `scripts/test-paid-shards.ts --case ID --trials N` (line 760/830), `eval:pass-rates`, `test:ubicloud` | full item. | +| A7 periodic red | not started | captured run 37272185151 | full item (CEO-4). | +| B1 shell-init | `2328cd3` | hook diagnostic | CEO-17 negative controls. | +| B2 chain stdin | `cdc0fc7` | cli.ts | CEO-8 Windows curation entry, CEO-17 TTY case. | +| B3 redaction | `4441c8f` | `lib/redact-patterns.ts` | Landed rule is a "reads as a code name" heuristic (`readsAsCodeName`) gated on a `,;)` terminator or braces, not CEO-20's file/annotation context with prefix/entropy precedence. | +| B4 junction | not started | setup Node/Playwright check (setup ~2044) | full item. | +| B5 Docker | not started | `GSTACK_CSO_TRUSTED_GIT` pattern (lib/cso/process.ts:22) | full item. | +| B6 router | `e103e57` | `omitDisabledSkills` | Other hosts' generated SKILL.md still carry the old wording (`.agents/`, `.factory/`, `.kiro/`, `.opencode/`, `.slate/` skills/gstack/SKILL.md); `CLAUDE.md:532` and `test/helpers/autoplan-preconfigured-fixture.ts:7` unchanged (CEO-5, CEO-16); CEO-25 live check. | +| B7 spawned consent | `7bfe46b` | spawned-directive, auq hook | none found (static preamble untouched, test added). | +| B8 hook parse | `659b197` | setup registration | Gate checks a hand list in `$CANONICAL_GSTACK_ROOT` after the checkout already moved and exits 1 before any registration; CEO-21 and DX-6 require the staged/pre-fast-forward check and per-hook refusal. | +| B9 Greptile | `23520a9` | ship greptile section | `PR: unknown` still dispatches; no test (CEO-19). | +| B10 browse text | `5b06b7b` | cli.ts, BROWSER.md | DX-9 wording check. | +| B11, B12 | not started | sync-gbrain Step 3.5, Copilot glossary | full items. | + +**Minimum changes:** every remaining item is already minimal; the largest new surface is A6 (ship prose plus extending the existing `--trials` runner flag instead of a new runner). + +**Complexity check:** about 55 changed files and no new classes or services (estimate; generated SKILL.md outputs counted once per template). Over the 8-file threshold, so the B selectors run (auto-decided): no feature cuts are proposed (autoplan override: never reduce, P2); structure question auto-decided **Original arrangement** (one PR, one commit per item; a smaller arrangement would split the PR, which is UC1's territory and stays with Garry). Scope record: `feature answers: none (override); structure: A (Original arrangement, auto-decided P2/P6); accepted scope: CEO-1..28 + DX-1..10 + source plan; pending remedies: UC1, T1-T10`. + +**Search check:** no new architectural pattern. The pre-fast-forward hook check (DX-6) reuses the updater's existing `BUN_HELD` hold [Layer 1]; free-text-through-files is standard practice for `gh --body-file` [Layer 1]. + +**TODOS cross-reference:** closes TODOS "C8 router wording behind a routing panel" (CEO-5); touches "placeholder convention for generated bash" (A3 lint is its first enforced piece); "gate-outcome telemetry" stays open; "Codex follow-ups" stays open. + +**Distribution check:** no new binaries; generated SKILL.md for every host must be regenerated (`bun run gen:skill-docs --host all`), which the landed B6 commit did not do. + +Scope Challenge result: **scope accepted as-is** (MODE FULL_REVIEW). + +Scope Challenge findings: +- [P1] (confidence: 9/10) `.agents/skills/gstack/SKILL.md:243` (and `.factory`, `.kiro`, `.opencode`, `.slate`) — `**When in doubt, invoke the skill.** A false positive (invoking a skill that wasn't` still present after `e103e57` changed `SKILL.md.tmpl`; other hosts were not regenerated, so the skill-docs freshness check fails and non-Claude hosts keep the old router. +- [P1] (confidence: 9/10) `setup:3713-3738` (landed `659b197`) — `_hook_file="$CANONICAL_GSTACK_ROOT/$_hook"` ... `exit 1`: the gate runs on the live checkout after an update already moved it, and refuses every registration; conflicts with CEO-21/DX-6. +- [P2] (confidence: 8/10) `ship/sections/greptile.md.tmpl` (landed `23520a9`) — `` `PR: none` → skip the dispatch ... Otherwise dispatch`` dispatches on `PR: unknown ()`, including gh missing or a GitLab remote; CEO-19 requires a skip with the reason. +- [P2] (confidence: 7/10) `lib/redact-patterns.ts` `readsAsCodeName` (landed `4441c8f`) — heuristic exemption without CEO-20's file/annotation context or prefix/entropy precedence; letters-only secrets that "read as words" in a `,`-terminated YAML flow value would be exempt. +- [P3] (confidence: 8/10) `.osv-scanner.toml` (landed `e77c532`) — new ignores for sprintf-js and katex are outside the plan and name no tracking issue, which the file header requires ("Tracking issues are filed at ship time and named in each reason"). + +#### Dual voices (Eng) + +- Voice input: `/home/user/.gstack/projects/garrytan-gstack/autoplan-eng-ciD8GY/eng-implementation.md` (sha256 `190bb276...ceed3e`). +- Claude SUBAGENT (eng — independent review): completed; `INPUT: eng 190bb276602480889fe75a39bf1288b07075461f1453a761ab272a0c92ceed3e` (matches). 19 findings: E1 rework inventory stale (B8 hand list/late/exit 1; B2 empty input boots a daemon; B4 landed a different design); E2 CEO-21's staged swap does not exist for the Claude checkout; E3 A2's run-scoped skip has no carrier across fresh shells; E4 `GSTACK_ROOT` is set, not exported; E5 dash dies on later syntax before DX-1's message (probed: `Syntax error: redirection unexpected` at line 76); E6 quota matching reads the echoed prompt; E7 junction delete hazard; E8 B5 trusts env vars and claims an ACL property; E9 `readFileSync(0)` can throw; E10 B3 exemption can swallow a JWT; E11 A6 seeded case passes without the fix (slots never reset); E12 local parallel reruns create contention; E13 hand-listed hook sets drift; E14 Dockerfile.ci lacks zsh; E15 relative-path case untested; E16 file-write location prompts or is denied; fixed `/tmp/greptile_*.json` paths; E17 lint as ratchet; E18 budget has no owner script; E19 DX-6 ordering after `BUN_HELD`, anchor test should resolve locally. +- Codex SAYS (eng — architecture challenge) (gpt-6-astra): `OUTSIDE_STATUS: completed provider=codex host=claude`, `VERDICT: findings`, `FINDINGS: P1`, exit 0. Full output: + +```text +Reviewed code through `9618336`, including B4, which landed during this review. No skill files were read and no files were changed. + +1. **[P1] DX-5 makes branch validation unsafe for shell interpolation. Confidence: high.** + `git check-ref-format --branch` accepts `fix;id`, ``fix`id` ``, and `fix$(id)`; I verified this without creating refs. Git validity therefore cannot replace CEO-12's shell-safety constraint. The existing generator interpolates identifiers into commands at generate-test-failure-triage.ts:14, while the current lint only checks selected text arguments at generated-bash-lint.ts:35. + **Fix:** Transport identifiers as data before validating them—prefer argument arrays, or read them from files into quoted variables. Never substitute their bytes into shell source. Include shell metacharacters in the identifier tests, even when Git accepts them. + +2. **[P1] B8's proposed check misses broken imported hook code. Confidence: high.** + Even after moving validation before activation, `bun build --no-bundle` checks only the entry file. Hooks import substantial code: phase-publication-hook.ts:9 imports the snapshot implementation and transcript helpers. An unparseable or missing dependency passes the proposed entry-only gate at setup:3732, then triggers the shim's denial backstop at phase-publication-hook:5. Missing TypeScript entries are also silently skipped. The current fixture replaces entries with trivial modules, hiding this gap (setup-hook-syntax-gate.test.ts:30). + **Fix:** Validate required files and the complete local import graph without executing hooks. Test missing entries, broken imported modules, and upgrades with existing registrations. + +3. **[P1] B3's entropy amendment contradicts its intended exemption. Confidence: high.** + CEO-20 gives entropy precedence, but the values being exempted already exceed the detector's threshold: `SessionState,` measures 3.085 bits/character and `{turn.requestId` measures 3.374; the threshold is 3.0 at redact-patterns.ts:904. Conversely, the landed word-shape heuristic at redact-patterns.ts:602 suppresses `API_KEY=VelvetRiverOrbitSunset;` and `password: {RiverStoneAmberCedar}`; both returned zero `env.kv` findings in read-only probes. + **Fix:** Classify source expressions versus literals using actual context, then apply entropy to literals. Carry file context through the scanner where necessary; its current options lack it (redact-engine.ts:59). Add controls that demonstrate both sides simultaneously. + +4. **[P1] A6 cannot safely use the existing runner as specified. Confidence: high.** + `runCaseDiagnosis` preserves the behavior threshold of two while expanding the trial count, so `--trials 12` creates a **2/12** passing panel, not four panels with an 11/12 diagnostic target (paid-report.ts:82). `--case` accepts only E2E IDs, excluding standalone judges (test-paid-shards.ts:825). Repeated invocations also default to shared artifact directories, and evidence collection reads all finalized results there (test-paid-shards.ts:103). + **Fix:** Make A6 a separate runner feature, consistent with the CEO scope challenge. Define explicit diagnostic results, per-kind panel aggregation, judge selection, and unique baseline/repair/trial directories. Test these executable contracts with free fake runners; prose assertions and one rule-kind paid fixture are insufficient. + +5. **[P2] DX-7's "hard" spending cap has no enforceable cost bound. Confidence: high.** + Historical per-trial cost is an estimate. Reserving ten average-cost trials does not bound ten concurrent trials, especially when history is absent. The runner collects actual costs after execution (test-paid-shards.ts:128), and the diagnosis result discards those cost fields when constructing its returned verdict (paid-report.ts:108). + **Fix:** Define an adapter contract for bounded trial spend, reservation of all concurrent work—including judges—and reconciliation of actual costs. Where the backend cannot enforce a bound, describe the control as an estimated admission budget rather than a guaranteed $25 cap. + +6. **[P2] CEO-24 assumes a Windows installation step that does not exist. Confidence: high.** + The "Windows Setup E2E" workflow runs `bun run build` and checks checkout binaries; it never runs `./setup` (windows-setup-e2e.yml:72). Adding a journey "after setup" therefore does not establish an installed runtime root. Moreover, newly landed B4 uses a source-bundle fallback, not the plan's junction design (cli.ts:125). + **Fix:** Reconcile B4's chosen architecture first. Add an actual isolated host installation, then invoke its installed binary from an unrelated directory and assert both commands' output. Exercise stale installations and failed dependency resolution. + +7. **[P2] B5 fixes executable discovery while leaving deterministic Windows blockers downstream. Confidence: high.** + Finding `docker.exe` reaches code that rejects native drive-letter configuration paths (docker.ts:55), accepts only `unix:///` endpoints, and requires a filesystem Unix socket (docker.ts:104). A binding test cannot demonstrate usable native Windows Docker operation. + **Fix:** Explicitly scope B5 to discovery and preserve a clear static-only unsupported-transport outcome, or separately design and validate native endpoint support. Do not close the operational defect based solely on executable binding. + +8. **[P2] DX-1's loud self-location failure will be swallowed by its caller. Confidence: high.** + The generated preflight sources the helper with `2>/dev/null || true`, then suppresses the auth probe's errors and reports `not_authed` (constants.ts:138). Improving the helper's error message alone still leaves users being told to authenticate when installation discovery failed. + **Fix:** Handle source failure explicitly, retain its diagnostic, and stop before authentication probing. Test the complete generated preflight's output and branching, not only direct helper sourcing. + +9. **[P2] B2's empty-input handling still depends on successfully starting a browser. Confidence: high.** + Empty input becomes an empty command argument at cli.ts:2041, but the CLI calls `ensureServer()` before dispatching it (cli.ts:2062). On a broken Windows install, an empty pipe or argumentless terminal invocation reports a startup failure instead of the promised usage error; it may also launch an unnecessary daemon. + **Fix:** Reject empty input locally before server startup. Test both terminal and empty-pipe cases with no running daemon and unavailable browser dependencies. + +Recommendation: Revise the plan and separate A6 from urgent repairs because shell interpolation remains unsafe, hook validation misses executable dependencies, and the measurement workflow requires substantive runner changes. +``` + +(Markdown links in Codex's output were flattened to `path:line` text.) + +``` +ENG DUAL VOICES — CONSENSUS TABLE: + Dimension Claude Codex Consensus + 1. Architecture sound? yes, after E1/E2 revise (A6 runner, DISAGREE on verdict, agree on fixes -> ENG-1/2/3/12 (taste T13 on B4 design) + hook import graph) + 2. Test coverage sufficient? no (E11, E13-E15) no (2, 4, 6) CONFIRMED gap (ENG-3, ENG-5, ENG-12, ENG-13) + 3. Performance risks addressed? no (E12 contention) not raised N/A (single voice) -> ENG-12 parallelism cap + 4. Security threats covered? no (E6, E8, E10, E16) no (1, 3) CONFIRMED gap (ENG-6, ENG-8, ENG-9, ENG-11, ENG-14) + 5. Error paths handled? no (E5, E9) no (8, 9) CONFIRMED gap (ENG-5, ENG-10) + 6. Deployment risk manageable? no (E2, E13, E14) no (2, 6) CONFIRMED concern (ENG-2, ENG-3, ENG-7, ENG-9) +CONFIRMED = native + outside agree; primary cannot replace outside. DISAGREE -> taste. +``` + +Cross-model notes: both voices independently found the B8 gate gaps (Claude E1/E13, Codex 2), the B3 heuristic failures (Claude E10, Codex 3, with Codex's probes showing two literal secrets exempted), the B2 daemon boot on empty input (Claude E1/E9, Codex 9), the B4 design drift (Claude E1/E7, Codex 6) and the A6 runner gap (Claude E18, Codex 4/5). Codex again recommends splitting A6 out (UC1, now raised in all three phases). Codex 1 overturns DX-5's "check-ref-format replaces the grammar" (accepted as ENG-8). Claude E17's ratchet suggestion is declined: free-text placeholders get no baseline (taste T12). + +#### Section 1: Architecture + +``` + generated SKILL.md (all hosts; regenerate --host all) ./setup bin/gstack-session-update + | | | + | preflight (constants.ts) --source--> gstack-codex-probe | hook gate (derived list, | fetch -> BUN_HELD check + | | helper_unavailable (ENG-5) | | local-import bundle check) | -> HOOK check on + | v v | -> register passing hooks | git archive + | CODEX_MODE ready|quota_exhausted|rate_limited|... | (ENG-2/3) | -> ff-merge or HOLD (DX-6) + | | (errors read from Codex's ERROR lines, ENG-6) v v + | v Claude checkout = live hooks copy-based runtime roots + | codex exec --> outside-review-result.ts --> GATE_OUTCOMES (+quota_exhausted, +rate_limited) --> docs anchors + | + | posting blocks: mktemp path -> agent file-write -> gh --body-file (CEO-12, ENG-8, ENG-14) + | + '--> /ship tests step --> scripts/ship-measure.ts (NEW, ENG-12) --> scripts/test-paid-shards.ts --case/--trials + | per-kind panels, unique dirs, admission budget, table | (or project eval cmd) + '--> free reruns: shard file list, retry off, parallelism cap (CEO-1, ENG-12) + + autoplan hook --> readOwnedClaudePublicTranscript (32 MiB owned + batch caps, too_large) (A4, CEO-2) + browse cli --> chain: stdin/TTY/empty handled before ensureServer (ENG-10); Windows runtime root -> .source-path bundle, + version-skew guard (ENG-9) + lib/redact-engine.ts --(file path)--> redact-patterns env.kv: source-context vs literal (ENG-11) + lib/cso: docker.exe discovery (known-folder roots, realpath) -> unsupported-transport outcome (ENG-9) +``` + +Findings: +- [P1] (confidence: 9/10) `setup` hook gate (landed) checks live files and exits before registering anything — ENG-2/ENG-3. +- [P1] (confidence: 9/10) `scripts/lib/paid-report.ts` `runCaseDiagnosis` keeps the behavior threshold at 2 while expanding trials (Codex probe) — A6 needs its own runner, ENG-12. +- [P2] (confidence: 8/10) B4 landed `.source-path` design differs from the plan's junction — adopt landed design with a version-skew guard, ENG-9 (taste T13). +- [P2] (confidence: 8/10) `lib/cso/docker.ts` accepts only `unix:///` endpoints — B5 scoped to discovery, ENG-9. +Dispositions: all accepted via auto-decision (audit rows 59-63). + +#### Section 2: Code quality + +- [P1] (confidence: 9/10) Other hosts' generated SKILL.md are stale after `e103e57` (ENG-1). +- [P1] (confidence: 9/10) `lib/redact-patterns.ts` `readsAsCodeName` exempts `API_KEY=VelvetRiverOrbitSunset;` (Codex probe) — ENG-11. +- [P2] (confidence: 8/10) `bin/gstack-codex-probe:37-41` and `bin/gstack-egress-lib.sh:52-55` duplicate the self-locate block with different fallbacks — one shared block (DX-1, ENG-5). Shared-code rubric: two first-party callers with identical intent; the block is about 12 lines, duplicated by necessity (each file is sourced standalone), so it stays duplicated byte for byte under a lint rather than extracted into a third sourced file (which would itself need locating). +- [P2] (confidence: 8/10) `review/greptile-triage.md` fixed `/tmp/greptile_*.json` paths — ENG-14. +- [P3] (confidence: 7/10) hand-listed hook sets in setup and (planned) in the updater — derived list, ENG-3. +Stale diagrams: `bin/gstack-codex-probe`'s contract comment (lines ~164-180) must list `quota_exhausted` and `rate_limited`; no other touched ASCII diagram is invalidated. + +#### Section 3: Test review + +Framework: `bun test` (CLAUDE.md, `package.json`), free suite via `bun run test`, paid via `scripts/test-paid-shards.ts`. + +``` +CODE PATHS USER FLOWS +[+] bin/gstack-codex-probe (A1/A2) [+] Outside voice after upgrade (macOS zsh) + ├── self-locate: bash [★★ TESTED regression-issue3024] ├── [GAP] [→E2E-lite] timed journey (DX-8, manual + CI zsh check) + │ zsh [★★ TESTED]; dash [GAP] ENG-5; relative+cd [GAP] ENG-5 └── [GAP] full generated preflight, helper missing (ENG-5) + │ bare-name [GAP] DX-1; GSTACK_ROOT fallback [GAP] DX-1 + ├── model probe: quota [★★ TESTED codex-model-probe, needs split] [+] Long session /autoplan + │ rate_limited [GAP] CEO-14; precedence [GAP] ENG-6 ├── [★★ TESTED] too_large owned path (autoplan-publication-guard) + │ echoed-prompt negative [GAP] ENG-6; retry-now [GAP] DX-3 └── [GAP] batch + narrowed paths (CEO-2, seam) + │ fresh-shell skip [GAP] ENG-4 [+] Greptile reply / test-failure issue + └── classifyOutsideReview stderr [★★ TESTED, needs anchoring] ├── [GAP] hostile body via real generated text (CEO-12) +[+] setup hook gate (B8) ├── [GAP] metachar identifier refused (ENG-8) + ├── [★★ TESTED setup-hook-syntax-gate] broken shim └── [GAP] unsent -> manual command printed (DX-5) + ├── [GAP] broken local import, missing entry (ENG-3) [+] /ship red paid case + ├── [GAP] derived list == registrations (ENG-3) ├── [GAP] [→EVAL] seeded case, behavior panel (CEO-3, ENG-13) + └── [GAP] partial registration + exit != 0 (ENG-2) ├── [GAP] negative control unrelated "fix" (ENG-13) +[+] bin/gstack-session-update (DX-6) └── [GAP] ship-measure fake runners per kind, budget stop (ENG-12) + └── [GAP] incoming bad hook -> HOLD, checkout unmoved [+] Free flake rerun +[+] browse chain (B2) └── [GAP] retry off, isolated state, parallelism cap (CEO-1, ENG-12) + ├── [★★ TESTED cli-chain-stdin] piped flow [+] Router (B6) + └── [GAP] TTY / empty / EAGAIN before ensureServer (ENG-10) ├── [★★ TESTED gen-skill-docs] new wording, Claude host +[+] redact env.kv (B3) ├── [GAP] all hosts regenerated (freshness) (ENG-1) + ├── [★ TESTED redact-engine, positive TS case only] └── [→EVAL] routing panel (CEO-5) + └── [GAP] literal controls + JWT + file context (ENG-11) [+] Windows (B2/B4/B5/B12) +[+] Windows runtime root (B4) ├── [★★ TESTED config.test] source bundle resolution + └── [GAP] version-skew refusal; isolated install journey (ENG-9) └── [GAP] curated subset entries (CEO-8); unsupported transport (ENG-9) +[+] CSO docker discovery (B5) [GAP] known-folder roots, reparse, outcome (ENG-9) +[+] spawned consent (B7) [★★ TESTED spawned-consent-rule] +[+] OSV (A5) [★★ TESTED osv-config-wiring, deps-smoke]; acceptance scan [GAP] CEO-22 +[+] anchors (CEO-7) [★★ TESTED troubleshooting-anchors, gate rows only]; URL + local resolution [GAP] ENG-15 + +COVERAGE: 11/41 paths tested (27%) | Code paths: 9/29 | User flows: 2/12 +QUALITY: ★★★:0 ★★:10 ★:1 | GAPS: 30 (1 E2E-lite manual, 2 eval) +``` + +Every gap above maps to an accepted amendment (CEO-*, DX-*, ENG-*), so each is a required test, not a pending choice. LLM/eval scope (CLAUDE.md "Prompt/LLM changes" patterns: skill templates, resolvers, preamble): the PR gate's outside-voice cases (A2 preflight), the routing panel (B6), the new A6 seeded behavior case (periodic), A7's 10-trial measurement (diagnostic), and the full PR gate once. Tests made obsolete: the landed `test/codex-model-probe.test.ts` assertions that map 429 to quota (lines ~59, ~464) are rewritten, not deleted; the landed `test/setup-hook-syntax-gate.test.ts` fixture that replaces entries with trivial modules is extended with real imports. Test plan artifact: `/home/user/.gstack/projects/garrytan-gstack/user-garrytan-fix-wave-oct6-eng-review-test-plan-20261006-184711.md` (written, 46 lines). + +#### Section 4: Performance + +- [P2] (confidence: 8/10) CEO-1 local reruns: 10 parallel processes on the 4-core Capy machine (nproc = 4) at the shard's own concurrency would mostly measure contention — ENG-12 caps parallelism. +- The hook's journal read stays capped (32 MiB) and runs only on phase-entry calls. The probe's quota cache saves one Codex round trip per skill while exhausted. DX-6 adds one `git archive` plus a few `bun build` runs per update check (seconds, only when an update is incoming). No unbounded loops or caches added. + +#### Failure modes (Eng) + +``` + NEW PATH | REALISTIC FAILURE | TEST? | ERROR HANDLING | USER SEES + -------------------------------|-------------------------------------------------|--------|--------------------|--------------------------- + self-locate under dash | syntax error before message (today) | ENG-5 | return 1 early | clear message + anchor + preflight source failure | swallowed, reported as not_authed (today) | ENG-5 | helper_unavailable | clear message + quota match on echoed prompt | false quota_exhausted, Codex skipped 15 min | ENG-6 | anchored match | correct reason + hook import broken on upgrade | passes entry check, hook denies every call | ENG-3 | bundle check | refusal with file:line + auto-update with bad hook | checkout moved, hooks blocked | DX-6 | HOLD | update held + reason + A6 measurement | 2/12 panel threshold, shared artifact dirs | ENG-12 | ship-measure.ts | correct per-kind table + A6 seeded case | passes without the fix | ENG-13 | slot reset + neg. | case fails correctly + B3 literal secret | exempted by word-shape heuristic (today) | ENG-11 | context rule | secret redacted + B2 empty input | daemon boot / startup failure | ENG-10 | local usage error | usage + exit 1 + B4 version skew | runtime CLI vs checkout server mismatch | ENG-9 | refusal | clear message + B5 Windows Docker | discovery works, transport unsupported | ENG-9 | explicit outcome | static-only, anchor +``` + +Critical gaps (no test AND no handling AND silent): 0 after the accepted amendments. Before them, 3 were silent on the branch: the B3 literal exemption, the swallowed helper failure, and the echoed-prompt quota match. + +#### NOT in scope (Eng) + +- Argument-array posting helper (CEO-12's stronger form): deferred (taste T8). +- Native Windows Docker named-pipe transport for /cso: deferred to TODOS.md (ENG-9); B5 ships discovery plus an honest outcome. +- Lint ratchet with a shrinking baseline (Claude E17): declined; free-text placeholders get no baseline (taste T12). +- Bounded owned-journal read: deferred P1 (CEO-2). +- Standalone readiness command: deferred (DX). + +#### What already exists (Eng) + +Reused: `scripts/test-paid-shards.ts --case/--trials` and `scripts/lib/paid-report.ts` (wrapped by the new `scripts/ship-measure.ts`, not duplicated), `eval:pass-rates` cost history, `scripts/ubicloud/test-free.sh`, setup's `_hook_command_path` registration list, the updater's `BUN_HELD` hold, `GATE_OUTCOMES`, `test/troubleshooting-anchors.test.ts`, `test/setup-hook-syntax-gate.test.ts`, `browse/test/cli-chain-stdin.test.ts`, `browse/test/config.test.ts`, `test/spawned-consent-rule.test.ts`, `test/osv-config-wiring.test.ts`, `scripts/lib/windows-curation.ts`. New: `scripts/ship-measure.ts` (justified: the existing diagnosis path cannot aggregate per kind or isolate artifacts, Codex 4). + +#### Worktree parallelization strategy + +| Step | Modules touched | Depends on | +|---|---|---| +| S1 probe + preflight rework (A1/A2: DX-1..3, ENG-4..7) | bin/, scripts/resolvers/, lib/, docs/, test/ | — | +| S2 posting safety (A3: CEO-12, ENG-8, ENG-14) | review/, scripts/resolvers/preamble/, ship/sections/, spec/, test/helpers/ | — | +| S3 hook gate + updater (B8: ENG-2/3, DX-6) | setup, bin/gstack-session-update, test/ | — | +| S4 transcript + guard (A4: CEO-2, DX-4) | lib/, autoplan/bin/, docs/ | — | +| S5 browse + Windows (B2/B4/B5/B12: ENG-9/10, CEO-8/24) | browse/, lib/cso/, .github/workflows/, scripts/lib/ | — | +| S6 redaction (B3: ENG-11) | lib/redact-*.ts, test/ | — | +| S7 router regen + Tier 2 text (B6/B9/B10/B11) | SKILL.md.tmpl, CLAUDE.md, ship/, sync-gbrain/, all generated hosts | S1, S2 (shared generated outputs) | +| S8 A6 runner + ship prose (ENG-12/13, DX-7) | scripts/, ship/sections/, docs/TESTING_INTERNALS.md, test/ | S2 (ship/sections) | +| S9 A7 replay + measurement | test/fixtures, paid runs | S8 | + +Parallel lanes: Lane A: S1 → S7; Lane B: S2 → S8 → S9; Lane C: S3; Lane D: S4; Lane E: S5; Lane F: S6. Execution order: launch A-F; merge B's S2 and A's S1 before S7; S9 last. Conflict flags: generated SKILL.md files (S1, S2, S7) must be regenerated once at the end from merged templates; `ship/sections/tests.md.tmpl` is shared by S2 and S8. + +#### Eng Implementation Tasks + +- [ ] **T1 (P1, human: ~2h / CC: ~10min)** — router — regenerate every host and update CLAUDE.md + fixture + - Surfaced by: Scope Challenge — `.agents/skills/gstack/SKILL.md:243` still says "When in doubt" + - Files: SKILL.md.tmpl outputs for all hosts, CLAUDE.md, test/helpers/autoplan-preconfigured-fixture.ts + - Verify: bun run gen:skill-docs --host all && git diff --exit-code (freshness) && bun test test/gen-skill-docs.test.ts +- [ ] **T2 (P1, human: ~1d / CC: ~40min)** — setup + updater — derived hook list, local-import bundle check, partial registration, pre-fast-forward hold + - Surfaced by: Section 1 — setup hook gate (ENG-2, ENG-3, DX-6) + - Files: setup, bin/gstack-session-update, test/setup-hook-syntax-gate.test.ts + - Verify: bun test test/setup-hook-syntax-gate.test.ts (+ updater hold test) +- [ ] **T3 (P1, human: ~1d / CC: ~45min)** — /ship — `scripts/ship-measure.ts` with per-kind panels, unique dirs, admission budget, parallelism cap; seeded case fixes + - Surfaced by: Codex 4/5, Claude E11/E12/E18 (ENG-12, ENG-13) + - Files: scripts/ship-measure.ts, scripts/lib/paid-report.ts, ship/sections/tests.md.tmpl, test fixtures + - Verify: free fake-runner tests for each kind and the budget stop; seeded case negative control +- [ ] **T4 (P1, human: ~4h / CC: ~25min)** — redaction — context-based classification with file path through the engine; literal controls + - Surfaced by: Codex 3, Claude E10 (ENG-11) + - Files: lib/redact-patterns.ts, lib/redact-engine.ts, test/redact-engine.test.ts + - Verify: bun test test/redact-engine.test.ts +- [ ] **T5 (P1, human: ~3h / CC: ~20min)** — probe + preflight — dash early return, helper_unavailable mode, anchored quota/rate matching, fresh-shell skip test + - Surfaced by: Claude E3/E5/E6, Codex 8 (ENG-4, ENG-5, ENG-6) + - Files: bin/gstack-codex-probe, bin/gstack-egress-lib.sh, scripts/resolvers/constants.ts, lib/outside-review-result.ts, tests + - Verify: bun test test/codex-model-probe.test.ts test/outside-review-result.test.ts test/regression-issue3024-zsh-sourced-helpers.test.ts +- [ ] **T6 (P1, human: ~3h / CC: ~20min)** — posting — metachar-safe identifiers, data transport for remote identifiers, verified write location, mktemp for greptile JSON + - Surfaced by: Codex 1, Claude E16 (ENG-8, ENG-14) + - Files: review/greptile-triage.md, scripts/resolvers/preamble/generate-test-failure-triage.ts, test/helpers/generated-bash-lint.ts + - Verify: hostile-payload tests through generated text +- [ ] **T7 (P2, human: ~3h / CC: ~20min)** — browse/Windows — B2 local usage error, B4 version-skew guard, isolated install journey, B5 known-folder roots + unsupported-transport outcome + - Surfaced by: Claude E7-E9, Codex 6/7/9 (ENG-9, ENG-10) + - Files: browse/src/cli.ts, lib/cso/process.ts, lib/cso/docker.ts, .github/workflows/windows-setup-e2e.yml, scripts/lib/windows-curation.ts + - Verify: browse/test/cli-chain-stdin.test.ts, browse/test/config.test.ts, Windows lanes +- [ ] **T8 (P2, human: ~1h / CC: ~10min)** — CI + hygiene — zsh in Dockerfile.ci, anchor test local resolution, OSV tracking issues + - Surfaced by: Claude E14/E19, Scope Challenge P3 (ENG-1, ENG-7, ENG-15) + - Files: .github/docker/Dockerfile.ci, test/troubleshooting-anchors.test.ts, .osv-scanner.toml + - Verify: bun test test/troubleshooting-anchors.test.ts test/osv-config-wiring.test.ts + +#### TODOS.md updates (collected from every phase; not persisted) + +The parent's instruction for this review forbids editing repository files other than the plan, so these TODOS.md entries are recorded here for the implementation PR to write (CEO-6 already requires it), in the repo's TODOS format: +1. **B9 full: Greptile in parallel during /ship** (#3020) — What: push early so Greptile reviews while /ship runs its own passes, then merge findings. Why: removes the "no PR yet" gap. Effort L/M. P2. Depends on: B9 skip landed. +2. **Bounded or incremental owned-journal read** (P1, next wave) — acceptance: /autoplan completes with a >100 MiB journal; phase-entry latency and peak memory measured on macOS and Linux. Depends on: CEO-2 seams. +3. **Executed-subcommand Codex probe** (CEO-28) — removes the caller-shell dependency. Effort M. P2. +4. **Argument-array posting helper** (CEO-12 stronger form) — Effort M. P2. +5. **Native Windows Docker transport for /cso** (ENG-9) — named-pipe endpoint support in `lib/cso/docker.ts`. Effort M. P2. +6. **Standalone readiness command** (DX: `gstack doctor` or a Codex row in `./setup --status`) — Effort S/M. P3. +7. **Per-reason telemetry** — already in TODOS (gate-outcome telemetry, P3); no new entry. +8. Conditional: B11 if the first live probe fails (CEO-27); B5 side note if it does not reproduce (CEO-10); A7 P1 if it stops red (CEO-4); any Tier 2 item reverted (CEO-11). + +#### Completion summary (Eng) + +- Step 0: Scope Challenge — scope accepted as-is (5 findings: 2 P1, 2 P2, 1 P3) +- Architecture Review: 4 issues found +- Code Quality Review: 5 issues found +- Test Review: diagram produced, 30 gaps identified (all mapped to accepted amendments) +- Performance Review: 1 issue found +- NOT in scope: written +- What already exists: written +- TODOS.md updates: 6 items proposed (+2 conditional groups), not persisted per the parent's instruction; CEO-6 makes the implementation PR write them +- Failure modes: 0 critical gaps flagged (3 silent gaps on the branch closed by ENG-5, ENG-6, ENG-11) +- Unresolved decisions: 0 in this review (UC1 and taste T1-T13 held for the gate) +- Outside voice: codex (gpt-6-astra) completed; native subagent completed +- Parallelization: 6 lanes, 6 parallel / 3 sequential steps +- Lake Score: N/A (no coverage-scored questions answered) + + +- **ENG-1 (rework inventory covers every landed commit).** CEO-18's list is extended to every commit on the branch; each item ships only when its row says "matches" or its rework is done. B1 `2328cd3`: add CEO-17's negative controls. B2 `cdc0fc7`: see ENG-10. B3 `4441c8f`: see ENG-11. B6 `e103e57`: regenerate every host (`bun run gen:skill-docs --host all`; `.agents/`, `.factory/`, `.kiro/`, `.opencode/`, `.slate/` still carry the old wording), update `CLAUDE.md:532` and `test/helpers/autoplan-preconfigured-fixture.ts:7` (CEO-5, CEO-16), run CEO-25's live check. B7 `7bfe46b`: matches. B8 `659b197`: see ENG-2 and ENG-3. B9 `23520a9`: `PR: unknown` must skip with the reason instead of dispatching, plus CEO-19's test. B10 `5b06b7b`: DX-9 wording check. B4 `9618336` and B12 `3df850f`: see ENG-9 for B4; B12 matches (CEO-8's Windows curation entry still owed). A5 `e77c532`: the plan now records the two extra ignores it added (sprintf-js via roarr, katex in diagram-render) and their `ignoreUntil = 2026-11-30` (consistent with the file's other entries, replacing CEO-17's 2026-12-31); each of the three new ignores names a tracking issue filed at ship time, as the file header requires. +- **ENG-2 (honest scope for the setup hook gate).** On the Claude install, `CANONICAL_GSTACK_ROOT` (`~/.claude/skills/gstack`) is the git checkout the hooks run from, so by the time `./setup` runs the new files are already live; there is no staged swap there. CEO-21's "abort the swap, previous install stays active" applies only to copy-based runtime roots. On the Claude checkout the setup gate refuses to register the failing hooks, registers the rest, reports file and line, and exits non-zero; it never prints DX-5's "your previous install is still active". DX-6 (the updater's pre-fast-forward check) is the protection for auto-updates. The landed gate's early `exit 1` before any registration is replaced accordingly. +- **ENG-3 (the hook check covers what the hook runs).** The hook list is derived from the registration code (every `_hook_command_path` argument plus the phase-publication hook), not a hand list; a free test asserts the two stay equal. Each TypeScript entry is checked with a local-import bundle into a temp directory (`bun build --target=bun` with node_modules packages external), so a broken or missing local import fails the check; a missing entry file fails instead of being skipped. Tests: broken imported module, missing entry, and an upgrade with existing registrations (both for setup and for DX-6's updater check, which runs after the existing `BUN_HELD` check). +- **ENG-4 (A2's run-scoped skip has a carrier).** Skill blocks run in fresh shells, so "skip the remaining Codex calls in that run" is carried by the 15-minute negative cache; a test runs two consecutive fresh-shell preflight blocks against a quota stub and asserts the second makes no Codex call. +- **ENG-5 (self-locate works where it matters and fails loudly elsewhere).** DX-1's fallback accepts a set (not necessarily exported) `GSTACK_ROOT` that holds the helper. The shared block ends with `return 1 2>/dev/null || exit 1` after printing its message and before any bash/zsh-only syntax, so dash and sh reach the message instead of a syntax error. The generated preflight (`scripts/resolvers/constants.ts`, which today runs `source ... 2>/dev/null || true`) keeps the source step's diagnostic and reports a distinct `CODEX_MODE: helper_unavailable` with that line and its anchor, instead of falling through to `not_authed`. Class-guard cases: dash (message, no syntax error), relative-path source followed by `cd`, bare-name source via PATH, and the full generated preflight under each. +- **ENG-6 (quota and rate-limit matching reads Codex's error, not the echoed prompt).** Both the probe's signatures and `classifyOutsideReview`'s stderr matching read only Codex's own error lines (the trailing `ERROR:` / `stream error` lines), not the whole stderr, which echoes the session prompt. Tests: an echoed prompt that contains "usage limit" and `insufficient_quota` with a non-quota exit stays `execution_failed`; a 429 body that also carries `insufficient_quota` resolves to `quota_exhausted` (quota takes precedence over rate limit). +- **ENG-7 (Dockerfile.ci installs zsh).** `.github/docker/Dockerfile.ci` adds zsh, as A1 already states, so the A1 guard runs (and fails loudly when missing) inside the eval container too. +- **ENG-8 (identifiers are shell-safe, not only git-valid).** `git check-ref-format --branch` accepts `fix;id` and `` fix`id` ``, so DX-5's branch check also rejects shell metacharacters (`$`, backtick, `;`, `|`, `&`, `(`, `)`, `<`, `>`, quotes, `\`, whitespace) and refuses with DX-5's message; numeric ids must match `^[0-9]+$`. Identifiers that come from remote data (PR head branch names, file paths from a diff or PR) travel as data, read from the agent-written file into quoted variables, never substituted into shell source. Tests include metacharacter identifiers that git accepts. +- **ENG-9 (B4 uses the landed design, and B5 is honest about Windows Docker).** B4 keeps the landed runtime design (`9618336`: the Windows runtime root starts the server from the source checkout's bundle through `.source-path`) instead of the plan's `mklink /J` junction, which avoids a runtime-root delete following the junction into the checkout. It adds a version-skew guard: when the runtime root's CLI version and the checkout's server version differ, the CLI refuses with an anchor and the `./setup` fix. CEO-17's junction mechanics and CEO-8's junction test are replaced by a test that the runtime root resolves the checkout bundle and refuses on version skew; CEO-24's journey step becomes a real isolated install (the current `windows-setup-e2e.yml` runs `bun run build` but never `./setup`), then the installed `browse` from an unrelated directory piping a two-command `chain` flow. B5 resolves the allowlisted roots through the known-folder API (PowerShell `[Environment]::GetFolderPath`), not the process environment, resolves the candidate's real path before the prefix check, and rejects reparse points; its refusal says "outside the trusted install locations". Because `lib/cso/docker.ts` accepts only `unix:///` endpoints, B5 is scoped to discovery: with `docker.exe` bound, /cso reports a clear "native Windows Docker transport is not supported yet; static assessment only" outcome with an anchor, and native named-pipe transport goes to TODOS.md. #3028 is closed with that explanation, not as fixed operation. +- **ENG-10 (B2 never boots a daemon for empty input).** Empty piped input, a TTY with no arguments, and a `readFileSync(0)` error (`EAGAIN`, `EOF`) all produce the usage error and exit 1 before `ensureServer()`. Tests run with no daemon and with browser dependencies unavailable. +- **ENG-11 (B3 classifies by context; replaces CEO-20's precedence rule).** Identifiers like `SessionState,` exceed the 3.0 bits/char entropy threshold, so "entropy takes precedence" cannot hold. B3 instead passes the file path into the scanner (`lib/redact-engine.ts` options) and classifies a value as a source expression only in a TypeScript/JSX file after a `:` type annotation or inside a JSX `{...}` expression; entropy and known-prefix rules apply to everything classified as a literal. The landed word-shape heuristic (`readsAsCodeName`) is replaced. Controls that must flag: `API_KEY=VelvetRiverOrbitSunset;` in a `.env`/YAML file, `password: {RiverStoneAmberCedar}` in YAML, a JWT in a `.ts` file, a quoted literal, a brace-wrapped literal, a literal concatenated to a property read. Controls that must not flag: `session: SessionState,` and `key={turn.requestId + turn.role}` in `.ts`/`.tsx`. +- **ENG-12 (A6 runner owns the measurement; budget is an admission budget).** A6 ships as a runner feature, `scripts/ship-measure.ts`, which /ship calls and which owns: per-kind aggregation (rule: N trials; behavior: panels of 3, each at its own 2-of-3 threshold, not one 2-of-12 panel as `runCaseDiagnosis` would produce today; judge: outputs scored by 3-sample panels, with standalone judge ids accepted), unique artifact directories per baseline, repair round and trial, the diagnostic label, and the per-case table. DX-7's `ship_measure_budget_usd` is an estimated admission budget: before admitting a batch it reserves the estimated cost of every concurrent trial (judges included), reconciles actual costs after each batch, and stops when reserved plus spent would exceed the budget; the PR table shows estimated and actual spend. Local reruns (CEO-1) cap parallelism at max(1, floor(cores / shard concurrency)), queue the rest and record the parallelism used. Free tests drive `scripts/ship-measure.ts` with fake runners for every kind, the budget stop and the unmeasured skip. +- **ENG-13 (A6 seeded case cannot pass without the fix).** The stub fails a trial only while the list is unsorted and its slot number mod 10 is 2, 5 or 9; slots reset per measurement batch. The case asserts the closing measurement ran at least 10 trials against the sorted file, and a negative control (a "fix" that edits an unrelated line) re-measures at 7 of 10 and never reaches the gate command. +- **ENG-14 (A3 file location and fixed temp paths).** The implementation verifies, against the Claude Code version pinned in `.github/docker/Dockerfile.ci`, where the host's write tool can create the body file without a permission prompt in default mode and in headless or spawned sessions (candidates: a project-local `.gstack/tmp/` excluded through `.git/info/exclude`, or `$GSTACK_STATE_ROOT/tmp`), uses the first that works with mode 0600, and records the result in the PR. `review/greptile-triage.md`'s fixed `/tmp/greptile_line.json` and `/tmp/greptile_top.json` move to `mktemp` paths. +- **ENG-15 (anchor test resolves locally).** CEO-7's anchor test accepts both relative anchors and DX-2's `https://github.com/garrytan/gstack/blob/main/docs/...#anchor` URLs and resolves both against the docs in the working tree, so a new anchor does not fail CI before it reaches main. + + +### Phase 4: Pre-Gate Verification + +| Phase | Required outputs | Status | +|---|---|---| +| CEO | premise challenges (10); Sections 1-10 with findings or examined/no-findings (11 skipped, no UI); Error & Rescue map; Failure Modes registry; NOT in scope; What already exists; dream state delta; Completion Summary; consensus table | present | +| Design | skipped (no UI scope); not a completed review | skipped | +| DX | 8 dimension scores; journey map; empathy narrative; TTHW (never -> 2-5 min target); DX Implementation Checklist; consensus table | present | +| Eng | scope challenge from code reads; architecture diagram; codepath/user-flow test diagram; test plan on disk (`/home/user/.gstack/projects/garrytan-gstack/user-garrytan-fix-wave-oct6-eng-review-test-plan-20261006-184711.md`); NOT in scope; What already exists; failure modes with critical gaps (0); Completion summary; consensus table | present | + +Voices: every phase's native subagent and Codex (gpt-6-astra) voice completed with matching INPUT hashes; design skipped. Decision Audit Trail: 77 rows. Known incomplete items: the CEO spec-review loop ended at its 3-launch cap with score 6 (FAIL) and one open clarity issue (amendments override item text rather than being folded in); no `## GSTACK REVIEW REPORT`, review logs or TODOS.md writes were made (autoplan skip-lists the per-skill report, and the parent reserved logs and repository writes); the tasks aggregator dropped the CEO task list because the branch gained more than five commits during the review (its 5-commit window), so CEO tasks are carried from the plan's markdown section. + +Cross-phase themes (raised independently in 2+ phases): A6 coupled to the urgent repairs (CEO, DX, Eng) -> UC1; untrusted text and identifiers reaching the shell (CEO, DX, Eng) -> CEO-12, DX-5, ENG-8; hook safety across upgrades (CEO, DX, Eng) -> CEO-21, DX-6, ENG-2/3; the long-session journal cliff (CEO, DX) -> CEO-2, DX-4; Windows claims vs evidence (CEO, DX, Eng) -> CEO-24, ENG-9; measurement honesty (CEO, Eng) -> CEO-23, ENG-12/13; plan drift against a branch being implemented during review (CEO, spec review, Eng) -> CEO-18, ENG-1. + + +## Decision Audit Trail + +| # | Phase | Decision | Classification | Principle | Rationale | Rejected | +|---|-------|----------|----------------|-----------|-----------|----------| +| 1 | CEO | Mode SELECTIVE EXPANSION | Mechanical | override | autoplan fixes the mode | SCOPE REDUCTION (file-count rule) | +| 2 | CEO | Keep the whole wave in one PR (HOLD check 1) | Mechanical | P6 | user's standing one-PR preference | split per item | +| 3 | CEO | Keep B4/B5/B11 in scope (HOLD check 2) | Mechanical | P1 | never reduce a complete plan | defer them | +| 4 | CEO | Correct B6 text (C8 half already shipped) | Mechanical | P5 | factual correction from code read | leave stale text | +| 5 | CEO | Correct #3033 overlap list and base | Mechanical | P5 | `pr3033-files.txt`, `git log` | leave stale text | +| 6 | CEO | X1 raise A4 cap by measurement | Taste -> reversed | P2, then P5 | both voices: a bigger cap moves the cliff; now cap stays 32 MiB + P1 follow-up | measured raise | +| 7 | CEO | X2 free-suite flake measurement in A6 | Mechanical | P2 | same file, part of #3034 | omit | +| 8 | CEO | X3 cache quota until reset time | Mechanical | P5 | fragile parsing | accept | +| 9 | CEO | X4 per-reason telemetry | Mechanical | P4 | duplicate TODO | accept | +| 10 | CEO | X5 Greptile in parallel deferred | Mechanical | P3 | outside blast radius | include | +| 11 | CEO | X6 seeded stub paid case, periodic tier | Taste (T4) | P1 | keeps paid case near 10 minutes | real eval runner inside the case | +| 12 | CEO | X7 replay A7's captured run first | Mechanical | P1 | AGENTS.md: free replay before paid | paid trials first | +| 13 | CEO | CEO-5 update pinned-prose tests with B6 | Mechanical | P1 | prompt cleanups audit pinning tests | leave tests | +| 14 | CEO | CEO-6 TODOS.md entries for every deferral | Mechanical | P1 | Prime Directive 7 | chat-only deferrals | +| 15 | CEO | CEO-7 anchors for every new failure state | Mechanical | P1 | P3 promise | A2/A4 only | +| 16 | CEO | CEO-8 Windows tests named and curated | Mechanical | P1 | spec review 1 | unnamed | +| 17 | CEO | CEO-9 A6 defaults ($2/trial ask, 3 rounds, units per kind) | Taste (T2) | P5 | #3034 left values open | other values | +| 18 | CEO | CEO-10 B5 side note bounded to one fixture | Mechanical | P5 | open-ended investigation | investigate freely | +| 19 | CEO | CEO-11 Tier 2 revert rule | Mechanical | P6 | defines "ship if green" | hold PR on any Tier 2 red | +| 20 | CEO | CEO-12 structural free-text rule, no heredoc fallback | Mechanical | P1/P5 | both voices | heuristic lint + heredoc | +| 21 | CEO | CEO-13 B7 out of static preamble; cheap gates listed | Mechanical | P1 | consistency with #3012; AGENTS.md | edit preamble | +| 22 | CEO | CEO-14 split quota vs 429 | Mechanical | P1 | transient 429 must not drop coverage 15 min | 429 as quota | +| 23 | CEO | CEO-15 nested sources + bash-only lint + auth probe | Mechanical | P1 | spec review 2-3 | SKILL.md-only list | +| 24 | CEO | CEO-16 B6 changes router + injection + gstack CLAUDE.md | Taste (T3) | P1/P5 | same wording in two places; never rewrite user files | router only | +| 25 | CEO | CEO-17 Tier 2 mechanics named | Mechanical | P5 | implementable without questions | leave open | +| 26 | CEO | CEO-18 rework list for commits on the branch | Mechanical | P1 | committed code contradicts accepted plan | ignore drift | +| 27 | CEO | CEO-19 B7/B9/B10/B11 tests and fallbacks | Mechanical | P1 | spec review 3 | untested | +| 28 | CEO | CEO-20 B3 source-context exemption | Mechanical | P1 | original rule fails its own control | bare-identifier rule | +| 29 | CEO | CEO-21 staged hook parse check before swap | Mechanical | P1 | Codex 5: upgrade must not leave a broken hook live | fresh-install-only check | +| 30 | CEO | CEO-22 A5 acceptance scan | Mechanical | P1 | close #3040 on evidence | close on override bump | +| 31 | CEO | CEO-23 "fixed" needs a named cause + explained failures | Mechanical | P1 | both voices: 9/10 is not a rate | 9/10 closes | +| 32 | CEO | CEO-24 honest Windows claims + one e2e journey step | Taste (T6) | P1 | Codex 6 | claims only, no journey | +| 33 | CEO | CEO-25 live skillOverrides check before B6 | Mechanical | P1 | unproven premise | assume | +| 34 | CEO | CEO-26 comment on contributor PRs when commits land | Mechanical | P6 | contributor channel | comment only at merge | +| 35 | CEO | CEO-27 B11 decided on first probe | Mechanical | P3 | bounded scope | open investigation | +| 36 | CEO | CEO-28 keep sourcing, record reason, TODO subcommand | Taste (T5) | P5 | already committed; skills call functions across a block | convert to subcommands now | +| 37 | CEO | CEO-4/CEO-11 A7 may stop red after 3 rounds; wave continues | Taste (T1) | P6 | periodic case cannot hold the wave | PR waits for A7 green | +| 38 | CEO | Keep B6 in the wave | Taste (T7) | P1 | real user bug #3018; panel already required | defer B6 (Claude voice) | +| 39 | CEO | Argument-array posting helper deferred | Taste (T8) | P5 | new helper surface; structural rule covers P2 | build now (Codex voice) | +| 40 | CEO | Split A6 out of the wave / hotfix A1-A5 first | USER CHALLENGE (UC1) | n/a | both voices recommend; not auto-decided | (held for gate) | +| 41 | CEO | 0H document approval | Mechanical | P6 | documents reflect decisions | revise | +| 42 | DX | Product type Claude Code Skill (+ CLI) | Mechanical | P6 | README/package.json/plan terms | API/Library | +| 43 | DX | Persona: macOS zsh founder with Codex | Mechanical | P6 | README audience + reporters | Windows-first persona | +| 44 | DX | TTHW target Competitive (2-5 min) | Mechanical | P5 | reachable with existing commands | Champion (needs new command) | +| 45 | DX | Magical moment via existing preflight line | Mechanical | P5 | lowest-effort vehicle | standalone readiness command | +| 46 | DX | Mode DX POLISH | Mechanical | override | autoplan fixes the mode | EXPANSION/TRIAGE | +| 47 | DX | DX-1 unified self-locate with loud failure | Mechanical | P1 | Claude critical C1 verified in `f0531a2` | keep guessing fallbacks | +| 48 | DX | DX-2 one token per state + absolute URLs | Mechanical | P5 | both voices | mixed spellings, repo-relative anchors | +| 49 | DX | DX-3 quota retry-now bypass | Mechanical | P1 | both voices | no escape hatch | +| 50 | DX | DX-4 A4 recovery recipe with /context-save | Mechanical | P1 | both voices | "start a new session" only | +| 51 | DX | DX-5 refusals print cause, body path, exact command | Mechanical | P1 | no dead ends | "left unsent" | +| 52 | DX | DX-6 auto-updater checks incoming hooks before fast-forward | Mechanical | P1 | Codex critical verified in `bin/gstack-session-update` | staged setup check only | +| 53 | DX | DX-7 named controls + $25 total budget + opt-in Ubicloud | Taste (T10) | P5 | both voices; values are judgment calls | per-trial ask only | +| 54 | DX | DX-8 one timed macOS zsh journey + CI probe check | Mechanical | P1 | Pass 8 measures the approved clock | unmeasured target | +| 55 | DX | DX-9 verification commands + "What changes for you" | Mechanical | P1 | both voices | issue close without check | +| 56 | DX | Keep X3 rejected; no parsed reset time | Taste (T9) | P5 | fragile parsing; DX-3 covers recovery | parse reset time (Codex) | +| 57 | DX | Standalone readiness command deferred | Mechanical | P3 | new command outside DX POLISH | build now | +| 58 | DX | A6 decoupling (Codex DX 3) | USER CHALLENGE (UC1, repeated) | n/a | both voices again | (held for gate) | +| 59 | Eng | Scope accepted as-is; structure = Original arrangement | Mechanical | P2/P6 | override: never reduce; split is UC1's call | smaller arrangement | +| 60 | Eng | ENG-1 rework inventory for every landed commit | Mechanical | P1 | both voices; stale host outputs verified | A1-A5 only | +| 61 | Eng | ENG-2 honest setup-gate scope on the Claude checkout | Mechanical | P5 | no staged swap exists there (verified) | keep "previous install active" claim | +| 62 | Eng | ENG-3 derived hook list + local-import bundle check | Mechanical | P1 | both voices | hand list, entry-only parse | +| 63 | Eng | ENG-4 quota cache is the run-scoped skip | Mechanical | P5 | fresh shells per block | shell variable | +| 64 | Eng | ENG-5 dash early return + helper_unavailable mode | Mechanical | P1 | both voices; dash probe | message after syntax error | +| 65 | Eng | ENG-6 match Codex ERROR lines only | Mechanical | P1 | echoed prompt false positive | whole-stderr match | +| 66 | Eng | ENG-7 zsh in Dockerfile.ci | Mechanical | P1 | A1 text was wrong | correct text only | +| 67 | Eng | ENG-8 shell-safe identifiers + data transport | Mechanical | P1 | Codex probe: check-ref-format accepts `;` | git validity alone | +| 68 | Eng | ENG-9 keep landed B4 `.source-path` design + skew guard | Taste (T13) | P5 | avoids junction delete hazard; already landed | plan's junction | +| 69 | Eng | ENG-9 B5 scoped to discovery + unsupported-transport outcome | Mechanical | P1 | docker.ts accepts unix sockets only | claim operation fixed | +| 70 | Eng | ENG-10 B2 usage error before ensureServer | Mechanical | P1 | both voices | daemon boot | +| 71 | Eng | ENG-11 B3 context classification replaces CEO-20 precedence | Mechanical | P1 | both voices; Codex probes | word-shape heuristic | +| 72 | Eng | ENG-12 scripts/ship-measure.ts + admission budget + parallelism cap | Mechanical | P4/P1 | both voices: runner gap | prose-only loop | +| 73 | Eng | ENG-13 seeded case cannot pass without the fix | Mechanical | P1 | slots never reset | as written | +| 74 | Eng | ENG-14 verified write location + mktemp greptile JSON | Mechanical | P1 | permission dead end, fixed /tmp paths | assume /tmp | +| 75 | Eng | ENG-15 anchor test resolves locally | Mechanical | P5 | new anchors before main | resolve against main | +| 76 | Eng | Decline lint ratchet baseline | Taste (T12) | P1 | free text gets zero tolerance | ratchet (Claude E17) | +| 77 | Eng | A6 decoupling (Codex eng recommendation) | USER CHALLENGE (UC1, repeated) | n/a | raised in CEO, DX, Eng | (held for gate) |