* fix(auq): spawned trigger is objective — explicit declaration or STATUS echo, never inference (periodic-lane AUQ collapse)
The v1.76 spawned rule's parenthetical '(or your dispatch prompt marks this
session as spawned)' let the model INFER spawned status from a scripted-looking
prompt in a CI-looking session and silently auto-choose every review-phase
question: reviewCount=0 across the plan-review periodic E2Es (weekly run
33363624506, 9 of 14 failed shards; reproduced locally, zero AUQ fingerprints).
Env and hook paths were excluded by inspection: hermetic children echo
SESSION_KIND: interactive (CLAUDE_CODE_ENTRYPOINT=cli beats CI markers) and the
question-preference hook isn't installed there.
The trigger is now objective: the echoed SESSION_KIND: spawned STATUS line, or
an EXPLICIT dispatch-prompt declaration ("you are a SPAWNED subagent") —
declared, never inferred — with an absence-safe interactive fence: CI env vars,
scripted-looking or pasted prompts, and write-to-this-exact-file instructions
are NOT spawned markers. The prose channel stays because Task-tool subagents
inherit the parent env (no spawned prefix) — their dispatch prompt is the only
signal; #2733's env-prefix channel is untouched.
19 carve skeleton ceilings re-pinned with measured values (+~440 bytes/skill);
ship goldens refreshed for all three hosts; resolver pins extended with the
no-inference regression tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: mktemp failure aborts loudly at all three skill-content sites; failed upgrade swap restores the backup (#2679)
An empty $(mktemp) result silently disabled the redaction pass (redact-doc
resolver, ship pr-body) and made /gstack-upgrade's vendored path destructive:
clone lands at "/gstack", the swap mv fails, and rm -rf then deletes BOTH the
live install's backup and "". All three sites now guard the assignment with a
loud exit; the vendored block additionally restores the backup when the swap
fails (same failure class — backup deletion after a failed mv) and the GitLab
MR path sends the SCANNED file's bytes instead of re-rendering an unscanned
heredoc. bin/gstack-redact rejects an explicit empty --from-file path instead
of silently falling through to stdin.
Receipts: 6 of 8 new regression checks fail on a v1.77.0.0 scratch worktree.
Fixes #2679
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(auq): the interactive fence classifies the session — it never nudges ask-count
Burn-in run 1 of the periodic repro overshot the review band (reviewCount=8 >
CEILING=7) with the fence's 'when unsure, ask' tail: that phrasing is a quota
nudge, not a classification default. The fence now states it only classifies
the session and never changes how many questions the skill asks. Pin added.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ci): OSV suppression config actually loads — explicit global --config + expiring, reasoned ignores
The ignore file was inert from v1.65.0.0: OSV-Scanner only auto-discovers
configs named osv-scanner.toml (no leading dot) and applies them
per-directory, so the root config never covered lib/diagram-render/bun.lock
either way. The workflow now passes --config=.osv-scanner.toml globally.
Every IgnoredVulns entry carries a reason with an upgrade trigger and an
ignoreUntil expiry (~90 days) so suppressions must be re-justified. A wiring
test pins flag ↔ filename ↔ entry hygiene so the file can never silently go
inert again.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(deps): dependency wave — 105 OSV advisories → 3 reasoned suppressions, all lanes verified on the pinned scanner
Root: overrides pin ip-address 10.3.1 (defeats BOTH nested nodes — socks'
range pull and express-rate-limit's exact 10.1.0 pin, which a top-level bump
provably cannot reach) and sharp 0.35.0 (GHSA-f88m, HIGH; transformers still
pins ^0.34 upstream — smoke-tested round-trip); marked ^18.0.11; full in-range
lockfile refresh clears hono, fast-uri, protobufjs, qs, body-parser, nanoid,
uuid, immutable and friends.
lib/diagram-render (via its own build-script contract: exact pins edited,
fresh lock, dist rebuilt): mermaid 11.16.1, @excalidraw/excalidraw 0.18.1,
@excalidraw/mermaid-to-excalidraw 1.1.2 → 2.2.2 — the 1.x line exact-pinned
mermaid 10.9.x and dragged the entire duplicate mermaid-10 advisory chain
(dompurify 3.1.6, nanoid 3.3.3, lodash-es); the bundle shrinks 9.96 → 7.59 MB
with the duplicate mermaid gone. Nested exact pins that survived get scoped
overrides (nanoid 5.1.16, lodash-es 4.18.1).
Verification: clean-worktree frozen-lockfile installs (root + nested) + the
SAME osv-scanner release the action pins (v2.3.8) with the workflow's exact
scan-args → exit 0, 'No issues found'. Smoke tests cover the override
surfaces (sharp round-trip, ip-address lockfile assertion, marked parse);
socks + diagram-drift suites already pin the rest.
Supersedes #2695 (its own lockfile kept socks/ip-address@10.2.0; @anupamme's
report credited for the parallel diagnosis).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(gbrain-sync): stub pgrep so the pin case is hermetic
The only non-dry-run --code-only child hits #1734's PATH-resolved
autopilot probe. A live host daemon is a correct refuse; the test
cannot inject processRunning. Neutralize pgrep in the fixture bindir
instead of adding a production env hatch.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(gbrain-sync): blank inherited GBRAIN_HOME in the pin child
Lock paths are checked before pgrep. Spreading process.env let a runner
GBRAIN_HOME with a live lock refuse the case before the stub ran.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: point ship design-checklist at installed gstack/review path
The /ship Design Review step skipped the checklist because the generated path omitted the gstack/ install segment. Sync the generated skill doc and pin a regression assertion.
Co-authored-by: Cursor <cursoragent@cursor.com>
Wave-amended: goldens regenerated against the wave tree (author's golden commit 8e7a03ca superseded)
* fix(codex): a CLI that cannot execute no longer reports CODEX_MODE: ready
Follow-up to #2477. The model probe it added does a real round trip, but its
final branch is the `else` of a "model 400" grep, so it swallowed spawn ENOENT,
non-executable binaries and missing vendor payloads alongside genuine network
timeouts. All three are deterministic — retrying never helps — yet they landed
in the fail-open bucket and resolved to `ready`, so every Codex pass was
skipped in silence and the review reported itself complete.
Observed live: @openai/codex was on PATH with an empty
vendor/aarch64-apple-darwin/codex/ directory. gstack said `ready` for two
months while no Codex pass ran.
Three changes:
- `_gstack_codex_model_probe` classifies deterministic install failures (exit
126/127, or stderr matching ENOENT/ENOEXEC/EACCES/"cannot execute binary
file") as MODEL_UNUSABLE_INSTALL, exit 2, never cached — a reinstall is
picked up on the next probe. Exit 124 and genuine transients still fail open,
which is what #2477 intended.
- The preflight chain captures the probe's code instead of testing it for
truthiness, so exit 2 routes to a new `broken_install` mode whose remedy is
`npm install -g @openai/codex` rather than "check your model pin". A missing
binary and an unusable model are different problems with different fixes.
- `_gstack_codex_version_check` no longer reads a broken CLI as healthy. It ran
`codex --version 2>/dev/null | head -1`, which captures head's status, not
codex's — and 2>/dev/null discarded the one diagnostic available. It now
captures the real exit code and warns on non-zero. Empty-but-successful
output stays silent, per the existing "empty output → OK" case.
Tests: 6 added to test/codex-hardening.test.ts covering both broken-install
shapes, the exit-2 contract, no caching, the transient still failing open, the
model 400 still classifying as MODEL_UNUSABLE, and the version-check warning.
845 pass / 0 fail across all 8 suites touching the changed files.
Closes #2742
Wave-amended: autoplan hand-maintained preflight chain completed (tmpl+render); install-signature grep gated on failed spawn only; goldens regenerated against the wave tree (author's golden commit 5797d326 superseded); +2 tests
* feat(redact): add Groq, Tavily and Notion API key patterns
* fix(redact): stop reporting .env.local as an internal hostname
`internal.hostname` ends in `.local|.prod|.staging|…`, so `.env.local`
matches on `env.local` and a dotenv FILENAME is reported as a leaked
internal host.
The collision is not exotic. It fires on `--env-file=.env.local` in an npm
script, `.env.staging` in a README, `.env.prod` in a .gitignore — ordinary
lines on branches that leak nothing. Measured on one private repo, three of
four MEDIUM findings in a routine push were this, and the fourth was a
deleted localhost URL. That ratio is the real cost: a scanner that reports
package.json is one people learn to skim, and skimming is how the HIGH
finding it exists for gets missed.
The guard follows the `insideUuid` precedent and stays deliberately narrow —
it exempts only a span beginning `env.` immediately preceded by a dot, i.e.
the literal `.env.<suffix>` form. `api.corp.local`, `build-7.internal` and
`myenv.local` all still report.
The test pins both directions, and the negative controls are the point: an
exemption written as "any span ending .local" would pass the dotenv half
while quietly gutting the pattern for every real host. Verified red/green —
with the validate hook removed, exactly the 6 dotenv cases fail and all 9
real-host controls still pass.
* fix: don't flag git SSH remotes as pii.email
`pii.email` matches the `git@github.com` inside
`git@github.com:acme/widgets.git`. That is a transport user@host, not a
person's address, so any diff touching a clone URL -- a deploy config's
repo URL, a submodule entry, a README clone line -- draws a spurious
MEDIUM from the pre-push hook.
Suppressed by URL shape rather than by adding `git` to
EMAIL_ALLOW_LOCALPARTS. A bare `git@` allowlist entry would also
suppress a genuine address at a domain that merely begins with "git"
(git@gitmail.com), converting a false positive into a false negative --
the worse failure for a guardrail. Two shapes are accepted:
- `<user>@<host>:<path>.git` for ANY host, covering self-hosted
remotes, plus the equivalent ssh:// URL form.
- `git@<known-host>` for github.com, gitlab.com, bitbucket.org and
ssh.dev.azure.com, whose bare form appears in docs and in
`ssh -T git@github.com` connectivity checks with no path at all.
Matched exactly, so gitmail.com is unaffected.
emailAllowed now receives the normalized text and the span offset so it
can see that surrounding shape; it had only ever been passed the matched
span.
Tests pin both directions: the SSH remotes go quiet, and a real address
still fires -- including at a git host (alex@github.com) and at a
git-prefixed domain (git@gitmail.com).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(redact): install-prepush-hook refreshes a stale managed hook
The marker check returned before the only writer, so once a repo had the
hook, no later change to the wrapper could ever reach it. The `printf x`
fail-open fix (v1.64.0.0) has still not landed in any repo that received
the hook before it, and a wrapper naming a gstack that has since moved
stays pointed at a dead path for the same reason.
Compare the body against what this version generates: rewrite on drift,
stay a no-op when identical. The chained pre-push.local is untouched on
both paths.
The existing trailing-newline regression test cannot catch this — it
installs into a repo with no prior managed hook, the one case that was
never broken.
* fix(redact): install-prepush-hook refreshes a stale managed hook
The marker check returned before the only writer, so once a repo had the
hook, no later change to the wrapper could ever reach it. The `printf x`
fail-open fix (v1.64.0.0) has still not landed in any repo that received
the hook before it, and a wrapper naming a gstack that has since moved
stays pointed at a dead path for the same reason.
Compare the body against what this version generates: rewrite on drift,
stay a no-op when identical. The chained pre-push.local is untouched on
both paths.
The existing trailing-newline regression test cannot catch this — it
installs into a repo with no prior managed hook, the one case that was
never broken.
Wave-amended: spawnSync timeouts added to the new tests (v1.77 sync-spawn tripwire)
* refactor(redact): name the SSH-remote path lookahead constant
Wave polish on the #2734 absorption: the 512-char scp-path lookahead window
follows the UUID_CONTEXT_CHARS named-constant convention instead of a magic
number at the slice site.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(config): reject malformed cross_project_learnings at set
A typo was stored with exit 0, so the feature stayed off and the first-run prompt never returned. Reject like codex_reviews; do not coerce.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(gbrain-detect): classify gbrain >= 0.43 held-lock refusal as engine-locked
gbrain 0.43+ refuses a held PGLite lock with exit 1 and the message
"GBrain's local database is already open through `gbrain serve` (MCP,
PID N)" instead of the pre-0.43 exit 124 + "connect timed out" that
the #2194 branch matches. The message matches no known pattern, so the
classifier falls through to the defensive broken-config default — and
Step 1.5 of /setup-gbrain and /sync-gbrain then tell the user to move a
perfectly healthy config.json aside and re-init the engine.
Reproduced live on gbrain 0.43.0.0, 0.44.0.0 and 0.46.30.0: with a
serve holding the lock, gstack-gbrain-detect reports
gbrain_local_status=broken-config; after stopping the serve it reports
ok with the same untouched config.
Match on the stable substring "already open through", mirroring the
existing #2194 branch semantics: engine-locked for pglite, broken-db
otherwise. Adds a fake-gbrain behavior for the 0.43+ refusal plus two
cases (pglite -> engine-locked, postgres -> broken-db).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(memory-helpers): a slow gitleaks probe no longer disables secret scanning
`gitleaksAvailable()` cached every failure the same way, so a 2s timeout on
`gitleaks version` was recorded as "the binary is absent" for the rest of the
process. One busy moment and the whole ingest ran unscanned behind a single
stderr line — a fail-open outcome decided by machine load rather than by
anything about the machine's setup. The caller only acts on
`scanner === "gitleaks"`, so every later file was written with no scan and no
second warning.
The probe now classifies three outcomes. ENOENT (and a present-but-unusable
binary: bad exit, EACCES) stays cached — that is a fact about the box, and
re-probing it per file would be waste. A timeout gets one retry on a 10s
budget, and if that also expires nothing is cached: the file is reported
unscanned, the warning says so in those words, and the next file probes again.
Observed under the 7-way sharded free-test runner, where spawning a shell
script inside a temp bin dir took longer than the 2s budget.
Tests: the retry path, the no-cache-on-timeout path (the second call must
re-probe), and the cached-absent path. The fake gitleaks hangs for 30s rather
than racing a short sleep against a short budget, and the budgets are chosen so
load cannot flip an outcome: 30s where the retry MUST answer, 800ms where the
probe MUST expire. An earlier draft used 1s/5s and flaked under the same shard
runner this commit is about. The existing probe test pinned `detect` to
calls[1], which a retry breaks; it now asserts the order instead of the index.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0111Mq3JGwZDcstn5wYcbhSw
* fix(make-pdf): pdftotext version and flavor probe returns unknown on poppler
describeBinary reports version="unknown" flavor="unknown" for every poppler
install, so logDiagnostics prints nothing useful on the most common
implementation. Two independent causes:
1. poppler writes the -v banner to stderr and exits 0. execFileSync returns
stdout (empty) and does not throw on a zero exit, so the stderr fallback in
the catch block is unreachable. The in-code comment already notes poppler
exits 0, but only the throwing path reads stderr.
2. flavor is matched against the version line alone. poppler prints
"pdftotext version 26.06.0" on line 1 and names itself on line 2,
"Copyright ... The Poppler Developers", so even a working stderr read
yields "unknown".
Switch the probe to spawnSync, which returns both streams regardless of exit
status, match the version banner rather than assuming line 0, and derive the
flavor from the full output.
Measured on poppler 26.06.0 (Homebrew, macOS), same machine and binary:
before: { version: "unknown", flavor: "unknown" }
after: { version: "pdftotext version 26.06.0", flavor: "poppler" }
xpdf is unaffected: it exits non-zero and names itself on line 1, so it
resolved correctly before and still does.
Tests use shell shims reproducing each vendor's banner, stream and exit status,
since a real pdftotext cannot be assumed present in CI. Two of the four fail on
this commit's parent; the xpdf and no-banner cases pass there and are included
as regression guards rather than red-proofs.
* fix(open-gstack-browser): pre-flight cleanup never killed the stale daemon
Step 0 read the old pid with `grep -o '"pid":[0-9]*'` and Step 2 read the port
the same way. Neither can match. Every writer of that file in
browse/src/server.ts serializes with `JSON.stringify(state, null, 2)`, so the
bytes on disk are `"pid": 12060` — colon, space, digits.
The failure was silent in the worst way. `_OLD_PID` came back empty, the kill
never ran, browse.json was deleted anyway, and the next `connect` died with
"existing daemon has different config (proxy/headed mismatch)" — an error
pointing at proxy/headed flags rather than at the cleanup that no-opped. Caught
against a daemon left over from a reboot: the operator was told to check flags
they had never passed.
Both patterns now accept optional whitespace. The new tripwire does not match
strings — it RUNS the snippets the skill hands the agent, against a state file
written exactly the way the server writes one, and asserts pid and port come
back out. A third case pins the coupling to `JSON.stringify(state, null, 2)`,
so a switch to compact JSON surfaces as a failing expectation rather than as
silence.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0111Mq3JGwZDcstn5wYcbhSw
* test(make-pdf): clean up the pdftotext shim tmpdir after the suite
Wave polish on the #2690 absorption: the describe-scope mkdtemp left one
directory per run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(browse): honour CHROMIUM_PROFILE in cli profile-lock cleanup
cli.ts resolved the Chromium profile dir with a hardcoded
$HOME/.gstack/chromium-profile, while browser-manager launches the profile
returned by config.resolveChromiumProfile(), which honours CHROMIUM_PROFILE
and GSTACK_HOME.
killOrphanChromium() and cleanChromiumProfileLocks() are called with no
argument, so whenever CHROMIUM_PROFILE was set they cleaned locks for, and
killed Chromium on, the DEFAULT profile rather than the one being launched.
Starting a browser with a custom profile therefore evicted an unrelated
browser running on the default profile.
Delegating to resolveChromiumProfile() also picks up GSTACK_HOME and
os.homedir(), so the cleanup path now matches the launch path on Windows
where HOME is frequently unset.
* fix(auq): the interactive fence is quota-silent — it defers to the skill's own decision points
Burn-in calibration: run 1 (fence tail 'when unsure, ask') overshot the
plan-ceo review band at reviewCount=8; run 2 (tail mentioning 'HOW MANY
questions') undershot at 1. Any ask-count language in the fence anchors the
model in one direction or the other. The tail now says only: classify as
interactive, then follow the skill's own decision-point instructions exactly
as written. Pins updated to forbid count language in either direction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(browse): pin cli.ts profile-dir wiring to the canonical resolver
Wave-added coverage for the #2732 absorption: a 6-line fix with zero tests is
how the hardcoded path shipped in the first place. resolveChromiumProfile's
env behavior is already pinned in config.test.ts; this pins cli.ts's
delegation and forbids the hardcoded path from returning.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(browse): preserve return value for async IIFE expressions in js/eval (#2727)
Wave-amended: test moved to browse/test/ (browse unit-test convention); trailing-semicolon normalization kept — it is load-bearing for the expression wrapper
* fix: bin writers drop data on Windows paths with an apostrophe
Two independent Windows git-bash bugs in the bin writers, both silent
because callers invoke these scripts with 2>/dev/null and do not check
the exit status — a hard failure was indistinguishable from success.
Bug 1 — apostrophe in the checkout path breaks the bun -e program.
gstack-learnings-log, gstack-question-log and gstack-telemetry-log build
a bun -e program as a double-quoted shell string and interpolate
SCRIPT_DIR into a single-quoted JS import specifier. A path such as
C:/Users/Someone's PC/... closes the JS string literal early and Bun
fails to parse ("Expected ; but found s"). Every learning write and every
plan-tune question event no-oped; telemetry error redaction fell to its
fail-closed null path. The #1950 cygpath -m guard did not cover this —
cygpath normalises the drive form but does not remove the apostrophe.
Fixed by not interpolating the path at all: cd into the module root and
use a relative import specifier, which is immune to apostrophes, spaces,
backslashes and MSYS paths alike. The one remaining interpolated data
path in gstack-developer-profile (readFileSync of PROFILE_FILE) is passed
via the environment instead, matching do_log_session in the same file.
Bug 2 — gstack-developer-profile --derive fails on an MSYS-form
GSTACK_HOME. GSTACK_HOME defaults to $HOME/.gstack, which under git-bash
is /c/Users/..., and Bun on Windows cannot open that form (ENOENT). This
script carried no cygpath guard at all. Fixed by normalising GSTACK_HOME
once, before PROFILE_FILE / LEGACY_FILE / the events path are derived
from it, so all three pick up the normalised value.
Adds test/hostile-path-writers.test.ts, which runs the bins from a
directory whose name contains an apostrophe and asserts that rows are
ACTUALLY WRITTEN (not merely that the exit code is 0 — exit-code-only
checks are what masked bug 1). The apostrophe repro is OS-independent:
SCRIPT_DIR derives from the script's own location, so a copied checkout
under a hostile directory name reproduces bug 1 on Linux/macOS CI too.
Wave-amended: all four writers unified on the env-var import pattern the PR already used in gstack-developer-profile (no CWD-dependent module resolution)
Wave-amended: all four writers unified on the env-var import pattern the PR already used in gstack-developer-profile (apostrophe-safe without CWD-dependent module resolution); import-shape pin updated
* fix(memory-ingest): stop two silent transcript-ingest failures
Two independent bugs made transcript pages silently fail to reach the brain.
1. Frontmatter fence gluing. buildTranscriptPage() built the closing "---"
with no trailing newline, and session bodies always start with "## ", so
the rendered page ended "...---## User". gbrain's frontmatter matcher
(/^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/ in src/core/markdown.ts) requires
the closing "---" to end its own line, so it skipped the glued fence,
latched onto the next standalone "---" in the transcript body, parsed the
prose between as YAML, and dropped the page with "Invalid YAML frontmatter".
Transcripts with no later "---" fell back to body-only, silently losing
their frontmatter. Fix: emit the fence on its own line with a blank
separator, matching renderPageBody()'s artifact branch.
2. Slug collisions. Two source files can map to one path-derived slug (a
session resumed under the same id on one day, or two ids sharing a 12-char
prefix). writeStaged() names each file "${slug}.md", so the second
overwrote the first; gbrain collected N-1 of N staged files and the
reconciliation guard failed the whole batch every run. Fix:
disambiguateSlugs() keeps the first occurrence and gives each later collider
a stable "-<sha8(source_path)>" suffix (deterministic, and slug + page_slug
move together so writeStaged, the failure mapping, and state recording agree).
Exports buildTranscriptPage, renderPageBody, and disambiguateSlugs for tests.
Adds regression tests for both failures.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wave-amended: contributor's local-workaround docblock note removed; issue refs retargeted #2653 (closed by its author) -> #2724 (the live 887-staged-to-0-ingested report)
* fix: keep feature markers in GStack state
* fix: align feature marker seeding with GStack state
Wave-amended: seeding relocation re-applied to the composite action (v1.77 moved CI seeding out of the inline workflow steps the original commit edited); wiring tripwire re-pointed accordingly; stale marker comment updated
* chore(upgrade): migrate feature-discovery markers to GSTACK_HOME
Follow-through on the #2748 absorption: existing installs answered the
continuous-checkpoint and model-overlay prompts with markers beside the
install; v1.78 reads them from GSTACK_HOME. Copy them once so nobody gets
re-prompted. Idempotent, non-fatal.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(land-and-deploy): check fork branch in head repo
Wave-amended: gh leaves .headRepository.nameWithOwner empty (verified live against gh 2.83) — owner/name now composed from headRepositoryOwner.login + headRepository.name so reconciliation is not a permanent no-op; fork branches get report-not-delete (maintainers lack fork push rights); pins updated
* test: spawn timeouts on the #2748 marker tests (v1.77 sync-spawn tripwire)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: spawn timeouts on absorbed-PR tests (v1.77 sync-spawn tripwire)
The absorbed community tests (#2748, #2676, #2714, #2720) were authored
before the v1.77 tripwire required a timeout on every sync spawn in the test
trees.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(gbrain): a slow --version probe classifies as timeout, never no-cli (#2716)
resolveGbrainBin's bare catch collapsed 'gbrain missing' and 'gbrain present
but the 2s --version budget expired' into the same null — freshClassify then
said no-cli, which the --is-ok whitelist from #1964 does NOT forgive, so a
bun-shim install on a loaded POSIX box silently lost every brain-aware block.
The probe now returns a discriminated result (cached per-process, same
lifetime the old null had) using the same killed/SIGTERM/ETIMEDOUT
discrimination the sources-list probe below already uses; timeout routes to
the forgiven 'timeout' status. GSTACK_GBRAIN_VERSION_PROBE_TIMEOUT_MS test
override added (same precedent as the sources-probe override).
Receipt: the slow-but-present sibling test fails on a v1.77.0.0 scratch
worktree (classifies no-cli there).
Fixes #2716
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(codex): close the consult-mode fence, report turn.failed as a failure, capture exit codes portably (#2671, #2669)
Three defects in the codex skill sections:
- The resumed-session bash block never closed its fence; every fenced region
after it inverted (prose rendered as code, the synthesis-recommendation tail
rendered inert). A repo-wide fence-pairing test now scans every generated
SKILL.md and sections/*.md with a CommonMark-faithful state machine (an
info-string opener inside a fence is literal content — nested template
examples in document-generate/make-pdf stay legal; a file ending inside a
fence fails).
- The JSONL parsers had no turn.failed branch: a turn that STATED its failure
was reported as 'possible mid-stream disconnect'. Challenge and consult now
print the event's error and run a three-way completeness check (failed-with-
reason / silent-disconnect / ok); consult previously had no completeness
check at all.
- ${PIPESTATUS[0]} is empty under zsh, so hang detection never fired and
every clean run printed a spurious '[codex exit ]'. All three capture sites
use ${PIPESTATUS[0]:-${pipestatus[1]}}, pinned statically and EXECUTED
under real bash and zsh in the new test. Expect a step-change in
codex_timeout telemetry — the counter starts firing for zsh users.
Receipt: the portability pin fails on a v1.77.0.0 scratch worktree; the fence
fix is structural (17 → 18 fence lines, tail no longer inside a block).
Fixes #2671
Fixes #2669
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: outside-voice fallback is labeled honestly — same model family, not cross-model (#2735)
When Codex is unavailable, the plan-review outside voice falls back to a
Claude subagent and the copy sold it as 'cross-model coverage' with 'genuine
independence'. Fresh context is real; cross-model validation is not — a user
weighing 'both reviewers agree' deserves to know both reviewers share a model
family. Six canonical strings fixed at the resolver source (constants.ts
not_installed/not_authed, review.ts outside-voice bullet + three dispatch
paragraphs); ~10 generated docs and the ship goldens regenerated. Printing
the resolved fallback model at dispatch time is descoped as a functional
change (follow-up in the wave dispositions).
Fixes #2735
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(relink): skill_prefix patches the gbrain render too — the file the host actually serves (#2738)
gstack-relink linked SKILL.md from RENDER_DIR when a gbrain render was active
but ran gstack-patch-names only on INSTALL_DIR, so the served frontmatter kept
the unprefixed name and skill_prefix=true silently no-oped for every
brain-aware skill. The render tree (user-owned, untracked) is now patched too;
gstack-patch-names is idempotent so repeat relinks never double-prefix. The
gen-skill-docs note that pointed users at relink now describes what relink
actually covers.
Receipt: the new test fails on a v1.77.0.0 scratch worktree (served render
keeps 'name: qa').
Fixes #2738
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(render): section refs point at the FINAL render dir, never the tmp swap dir (#2692)
gen-skill-docs bakes its --out-dir into rendered CONTENT (rewriteSectionBase),
and both swap-in callers (setup, gstack-config gbrain-refresh) render into
claude.tmp.<pid> before the #2569 atomic rename — so every rendered skill
carried ~9 dead section Read paths that pointed at a directory the swap had
just deleted. New --link-root flag names the final serving dir (defaults to
--out-dir for direct-render callers: bin/dev-setup, dev-skill.ts, mkdtemp
tests — full caller audit in the wave notes); the rewrite now uses a
replacement callback so a $-bearing configured path can't expand as $& in a
replacement string. The swap logic itself stays byte-identical. Tests pin the
generator contract (tmp out-dir files reference the final dir, $-bearing
path included) and both callers' wiring.
Fixes #2692
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(setup): persistent timeline Stop hook opt-out — timeline_stop_hook config gate (#2677)
--no-team is a one-shot teardown, so every later bare ./setup (including the
ones /gstack-upgrade runs) re-registered the timeline Stop hook with no way
to say 'never'. New gate mirrors the plan_tune_hooks pattern: flag
(--timeline-stop-hook/--no-timeline-stop-hook) > env
(GSTACK_TIMELINE_STOP_HOOK) > saved config (timeline_stop_hook) > default
yes. An explicit flag persists to config so the decision survives upgrades;
an explicit 'no' also removes a live registration (reconciliation), so the
opt-out works against installs registered by an older setup. --no-team
semantics unchanged (NO_TEAM_MODE is never initialized from config). Full
gstack-config surface: DEFAULTS entry, header docs, list/defaults
enumeration, warn-and-default validation.
Fixes #2677
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(browse): tame the macOS headless GPU spin + reap the lock-less headless Chromium on stop (#2709)
Two defects in one report. On macOS 26 / Apple Silicon the headless-shell GPU
process pegs ~800% CPU indefinitely after real page work and --disable-gpu
alone is not enough; the reporter validated that adding
--disable-software-rasterizer/--disable-gpu-compositing/--disable-gpu-watchdog
drops it to 0.0% with screenshots still working. The flag block is a pure
platform-parameterized function (unit-tested on any host), darwin-gated,
headless-only (buildGStackLaunchArgs feeds the headed/GBrowser paths where
GPU-off is wrong), with a GSTACK_DISABLE_GPU=off escape.
Separately: the headless launch has no userDataDir, so it never writes the
SingletonLock that killOrphanChromium walks — 'browse stop' reported success
while the orphan kept spinning. The daemon now records the launched child's
pid + wall-clock start time in the state file (the xvfbPid/xvfbStartTime
contract), and stop paths reap a survivor only after verifying BOTH the
recorded start time and a Chromium-looking cmdline — a recycled PID, even one
running a different legitimate Chromium, is never killed (identity tests
include the coreutils-shebang trap that defeats argv0 renames).
macOS efficacy is per the reporter's validation; live re-verification on
Apple silicon is tracked in TODOS.md.
Refs #2709
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(wtree): a failed touch falls through to the HEAD seed instead of reopening the racy window (#2687)
The v1.74 racy-git fix carries the real index's mtime onto the temp copy —
but its 'touch -r … || true' meant a FAILED touch silently kept the copy's
fresh stamp, marking every entry non-racy and reopening the exact same-size-
rewrite hole. A failed touch now discards the copy and seeds from read-tree
HEAD (slower; every entry re-hashed; fingerprint stays honest).
Verification for #2687 itself: the reporter's same-size-rewrite repro run 20
iterations against this tree — 0 misses (the underlying race was fixed by
v1.74's b1485d88 with its own regression test; this wave verifies and closes,
it does not claim that fix). Receipt: the stubbed-touch test fails on a
v1.77.0.0 scratch worktree.
Fixes #2687
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: rewrite gate pin follows the LINK_ROOT rename (#2692)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: v1.78 fix-wave deferrals filed in TODOS.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* v1.78.0.0 release metadata: VERSION, package.json translation, CHANGELOG wave entry, agents digest
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(osv): ignore ledger names its filed tracking issues (#2753, #2754)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(redact): large reports survive the pipe — exitCode instead of process.exit; inert test payload
The wave's PR quality gate failed closed: gate-secret-scan.mjs pipes the
diff's added lines into gstack-redact and parses the JSON report, but
process.exit() discards stdout still buffered in the pipe — this wave's
646-finding report (202 KB) is the first big enough to arrive truncated
(~145 KB) at node's collector, so JSON.parse failed and the gate read
'no report' as HIGH. The report and auto-redact body paths now set
process.exitCode and let the runtime drain stdout; exit-code contract
unchanged (verified 0/2/3 end-to-end). Also: the C1 test's stdin payload no
longer uses a provider-prefix credential shape (the gate correctly flagged
it; the content was never read on the error path under test).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(memory-ingest): fence regression test survives Windows tmpdirs
Wave polish on the #2699 absorption: the hand-built JSONL interpolated the
raw tmpdir into a JSON string — on Windows (D:\a\...) that's an invalid
escape, the user line was silently dropped, and the body started at
'## Assistant' (Windows Free Tests red). JSON.stringify the path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(auq): the interactive fence ends at classification — all behavioral tails removed
The pinned-container periodic lane proved the collapse dead (reviewCount 0 →
7/8/5 across the AUQ suite) but flagged the fence's remaining behavioral
clause: 'never adds, removes, or batches the skill's decision points' broke
the paired-finding control (5 > 4 — it suppressed the batching that fixture
expects), and the band overshot its ceiling (8 > 7). Every behavioral tail
tried so far skewed counts somewhere ('when unsure, ask' → 8; 'HOW MANY
questions' → 1; 'never batches' → paired control red). The fence now ends at
'When unsure, default to interactive.' — classification only, zero behavior
words. Pins forbid every tried-and-failed phrasing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(auq): the spawned trigger is the STATUS echo, nothing else — prose channel removed from the eager path
Two pinned-container periodic rounds showed that ANY dispatch-prompt
declaration channel in rule 1 keeps question counts unstable (round 1, fence
with behavioral clause: paired control 5>4, band 8>7; round 2, bare fence:
intermittent 0s return, paired control breaks both directions). The stable
regime CI was calibrated against had no spawned prose in the eager path at
all. Rule 1 now keys on exactly one machine-verifiable thing: the preamble's
own SESSION_KIND: spawned STATUS echo. No text from a dispatch prompt, file,
or page can flip a session to auto-choose (the strongest anti-injection
form). Subagents that missed the env marker are caught at FAILURE time by
the AUQ hooks' spawned escape (explicit declaration, never inference) — a
channel that never enters an interactive session's eager reasoning.
This reverses the wave's earlier explicit-declaration middle ground (and
adopts the outside voice's twice-made echo-only argument) on the new
evidence. #2733 protected: skill-e2e-docsync-spawned (gate) passes 1/1 on
this prose — the ship Step-18 dispatch forces the env prefix, so the echo
fires there.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: periodic-lane stabilization residual filed (#2756)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(browse): chromium reap works off-Linux and on every stale-state path
readPidCmdline fell back to '' on darwin (no /proc), so the identity gate
never matched and reapRecordedChromium was inert on the platform #2709's
GPU-spin reap actually targets — it now falls back to ps -o command=.
readPidStartTime no longer throws when ps is missing (Windows): a launch
must never die to a reap-bookkeeping probe. Three stale-state cleanup
paths (dead-daemon stop, startServer stale cleanup, headed-connect) now
reap the recorded chromium BEFORE unlinking the state file instead of
orphaning it, and the stop-path wait polls (100ms steps, 1s cap) instead
of sleeping a fixed 500ms. Wiring pinned: server-state pid/start-time
write, all five cli.ts reap call sites, headless-only GPU-flag push.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(browse): chained IIFE + second statement no longer misclassified as one expression
isSingleParenOrIifeExpression accepted any tail after the initial group's
close as long as trailing chars looked chain-ish, so
`(async()=>{await 1})().then(x=>x); console.log('done')` classified as a
single expression and the expression wrapper emitted a SyntaxError. The
tail is now consumed as a strict member/call/index/optional-chain walk to
END of input via a shared string/escape-aware findBalancedClose scanner;
anything else (';', operators) demotes to the block wrapper. Negative +
positive tests added.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(browse): move headlessGpuArgs below the import block
The #2709 helper landed between two import statements; imports now stay
contiguous. No behavior change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(codex-probe): timed-out probe (124) keeps its fail-open contract
Exit 124 reached the string-signature branch before the timeout fail-open,
so a slow probe whose partial output happened to quote 'permission denied'
classified as MODEL_UNUSABLE_INSTALL — a deterministic-broken verdict from
a transient condition. 124 is now excluded from the signature branch, and
the detect/display greps share one hoisted _BROKEN_SIG regex (they had
already drifted: display dropped 'not executable').
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(codex): JSONL parser initializes its state vars in both modes
challenge-mode initialized turn_completed_count but tested turn_failed via
'in dir()'; consult-mode initialized neither and rebuilt the counter with
a dir() conditional per event. Both parsers now init turn_completed_count
and turn_failed up front and use plain checks — same semantics, no
module-globals introspection.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ship): PR/MR create aborts on a missing or empty scanned body file
Both the gh and glab send blocks now guard [ -s "$PR_BODY_FILE" ] and the
prose restates that the variable comes from the scan block — bash blocks
run in separate shells, and an unset/empty path would previously send an
empty body (gh) or cat's error output (glab) instead of the scanned bytes.
Codex/factory ship goldens regenerated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(upgrade): abort when a stale .bak already exists at the install path
A leftover $INSTALL_DIR.bak from a crashed upgrade would make the mv nest
the live install inside it, and the failure-restore arm would 'restore'
the stale backup — possibly deleting the only good copy. The upgrade now
refuses to start and tells the user to inspect/salvage the backup.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(redact-doc): mktemp-failure message names what it refuses to send
'refusing to send unscanned <noun>' read as if 'unscanned' modified a
missing word for sink nouns like 'the spec body'; now 'refusing to send
<noun> unscanned'. Generated spec section refreshed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(setup): typo'd timeline-stop-hook value warns instead of persisting
--timeline-stop-hook=noo silently normalized to yes AND wrote yes to
config — a persisted decision the user never made. Unrecognized values now
warn (naming the source), apply the default for this run only, and skip
the config write. The opt-out log line names the actual decision source
(flag/env/config) and no longer claims a removal that may not have
happened.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(memory-helpers): slow-probe warning no longer suppresses the absent warning
One shared _gitleaksWarned flag served two different messages: a 'machine
under load, retrying next file' warning early in a run permanently
silenced the later 'gitleaks not in PATH; secret scanning disabled'
warning — the user never learned scanning was off for good. Split into
per-message flags.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(lib): shared isExecTimeout helper; export GbrainBinProbe
The killed/SIGTERM/ETIMEDOUT discrimination was hand-rolled at three sites
(gbrain version probe, engine classifier, gitleaks probe) and free to
drift; it now lives once in lib/gbrain-exec.ts. GbrainBinProbe is exported
(it's the return type of exported probeGbrainBin) and the cache carries a
rationale comment: caching a timeout for process lifetime is deliberate —
the memo dedupes the ~3 probes of one short-lived preamble process.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(gen-skill-docs): extract parsePathFlag; fix rewriteSectionBase docstring
--out-dir and --link-root shared near-identical inline parsing; one helper
now owns it. The rewriteSectionBase docstring said 'no-op when --out-dir
is unset' but the gate is the link root (which --link-root can set
independently) — it now describes the real behavior.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(memory-ingest): reunite preparePages with its docblock; pin disambiguateSlugs wiring
The #2724 disambiguateSlugs block was inserted between preparePages'
docblock and the function, orphaning the secret-scanning policy doc onto
the wrong symbol. Reordered. A call-site pin now asserts the prepare→stage
flow actually invokes disambiguateSlugs, so a refactor can't drop the call
while every unit test stays green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(hostile-path): per-run mkdtemp root; telemetry-log redaction coverage
The suite used a FIXED tmpdir name, so concurrent runs (sharded runner,
sibling worktrees) tore down each other's trees mid-flight — now a
per-run mkdtemp root with the apostrophe dir inside. gstack-telemetry-log
was the one bin named in the suite header with no test: it now must append
a real row under the hostile path with the credential span redacted
(<REDACTED-github.pat>) and the rest of the message preserved.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(config): signal-killed spawns map to -1, not exit 0
Both cfg() helpers defaulted a null spawn status to 0 — a child killed by
signal would read as success and mask real failures.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(upgrade): v1.78.0.0 feature-marker migration suite
The only migration without a dedicated test. Covers copy-when-absent
(script must mkdir GSTACK_HOME itself), destination-wins (never
overwrites), clean no-op, and two-run idempotence — asserting file
existence and contents, not just exit codes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(auq): pin the explicit-declaration-only spawned escape sentence
SPAWNED_ESCAPE_SENTENCE's tightened wording had no pin: positive pins on
the explicit-declaration clause, negative pins on the retired v1.76 loose
parentheticals ('e.g. your dispatch prompt says', 'marks this session as
spawned'), and a drift guard that both hook directives embed the constant
verbatim.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(gbrain): invalid version-probe timeout env falls back to the default
GSTACK_GBRAIN_VERSION_PROBE_TIMEOUT_MS set to 'abc', '-1', or '0' must use
the default budget — exercised behaviorally through probeGbrainBin with a
fresh PATH per case (the memo keys on PATH).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(codex): execute the JSONL parser under real python3; fence scanner tracks opener length
The parser's turn.completed/turn.failed/disconnect semantics were pinned
by shape only — now the python block is extracted from both RENDERED
sections and run against synthetic event streams (tokens line, FAILED +
not-a-disconnect, silence -> disconnect warning, SESSION_ID echo), with
byte-equivalence safety pins on the bash double-quote extraction. The
fence scanner also gains CommonMark opener-length tracking: a 4-backtick
fence wrapping a 3-backtick example no longer false-positives, with a
self-test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(redact): large report survives a slow piped consumer
Pins the >145KB truncation regression (process.exit before the pipe
drained): 900 MEDIUM findings -> 259KB JSON report through a sleep-first
POSIX consumer that holds the 64KiB kernel buffer full at child exit;
asserts complete parseable JSON with matching counts and exit 2, plus an
--auto-redact mirror (700 redactions, final sentinel byte arrives).
Harness proven red against a copy of the bin with process.exit restored.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(dev-setup): update LINK_ROOT source pin to the parsePathFlag shape
Companion to the gen-skill-docs parsePathFlag extraction: the pin still
asserts the same invariant (LINK_ROOT defaults to OUT_DIR, so an in-place
render stays a byte-exact no-op) against the new expression.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(changelog): fix-batch properties folded into the v1.78.0.0 entry
Stale-backup refusal + empty-scanned-body guard on the mktemp bullet, the
redact pipe-truncation fix as its own item (a v1.77 bug), and test counts
refreshed to the post-fix-batch suite (8,660).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(upgrade): migration test resolves bash through the parent PATH
A hardcoded /usr/bin:/bin child PATH breaks spawn('bash') on the Windows
curated lane (spawn resolves against the CHILD env's PATH; no bash.exe
lives there). Hermeticity is carried by HOME/GSTACK_* overrides, not PATH.
Found by the cycle-2 review pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(memory-helpers): per-run cooldown bounds the slow-gitleaks probe cost
Retrying a slow probe per FILE (#2715's slow!=absent split) re-paid up to
probe+retry (12s default) per file — an 887-file ingest on a loaded box
spent hours re-asking the same slow question. After 3 consecutive slow
answers the run stops probing and warns once that remaining files go
unscanned; the availability cache is still never written, so the next
process probes fresh. Slow/absent discrimination is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(memory-ingest): slug assignments persist across runs via the state consult
First-occurrence-keeps-bare was walk-order-dependent ACROSS runs: a source
that got the suffixed slug once could take the bare slug the next run (its
collider aged out or was skipped as unchanged), leaving gbrain holding the
same transcript under two slugs — and a NEW collider could claim a bare
slug that state shows belongs to an unchanged source, silently overwriting
that page. disambiguateSlugs now consults state.sessions: a recorded slug
stays owned by its source_path, re-ingested sources keep their slug
verbatim, fresh assignments never take another source's slug, and legacy
duplicate records (pre-#2724 overwrites) resolve first-owner-wins and
self-heal on the next state write. Stateless behavior is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(changelog): D2/D3 properties folded into the absorbed-PR bullets
Gitleaks per-run probe cooldown on the #2715 credit; cross-run slug
persistence on the #2699/#2724 credit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: update project documentation for v1.78.0.0
README.md: the persistent timeline Stop hook opt-out (#2677) — flag,
env var, and config key with resolution order. BROWSER.md: browse stop
against a dead daemon now reaps the recorded headless Chromium child,
identity-verified (#2709). CLAUDE.md + CONTRIBUTING.md: free-suite test
count ~7,000 → ~8,700 (8,660 as of this wave).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: cross-model doc review fixes for v1.78.0.0
CONTRIBUTING.md: the day-to-day example now edits the .tmpl (SKILL.md
is generated); the OSV row states the explicit --config load and the
reasoned, expiring ignore contract. BROWSER.md: stop row mentions the
identity-checked Chromium reap; env table gains CHROMIUM_PROFILE and
GSTACK_DISABLE_GPU rows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(changelog): headline claims what the receipts show
"Both red weekly lanes are green again" overclaimed: OSV is verifiably
green (pinned scanner, frozen install, branch dispatch), but the periodic
lane keeps its pre-wave churn (#2756) — what this wave proves is that the
v1.76 regression that silenced plan reviews is dead. Flagged by the
cross-model doc review; headline now leads with the user-visible outcome.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: y$un_ <forrest.sun527@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Lockyer <135391289+Lockyer228@users.noreply.github.com>
Co-authored-by: Udhdhav kheni <udhavkheni12@gmail.com>
Co-authored-by: schienbiz <274676847+schienbiz@users.noreply.github.com>
Co-authored-by: David Park <show@davidani.com>
Co-authored-by: alopes50 <alex@alexlopes.com>
Co-authored-by: Peter van Leeuwen <petervanleeuwen@SB-petervanleeuwen.local>
Co-authored-by: Denis Zjukow <denis.zjukow@gmail.com>
Co-authored-by: Paul Snyman <5826275+snymanpaul@users.noreply.github.com>
Co-authored-by: Adam Badar <badaradam10@gmail.com>
Co-authored-by: loulanyue <260355617@qq.com>
Co-authored-by: Shreshth Kapoor <shreshth@osiflow.com>
Co-authored-by: Ryan Ayers <rayers@dividia.net>
Co-authored-by: Simon Altit <simon.altit@gmail.com>
Co-authored-by: ptt <1928627998@qq.com>
64 KiB
Review Sections (11 sections, after scope and mode are agreed)
Anti-skip rule: Never condense, abbreviate, or skip any review section (1-11) regardless of plan type (strategy, spec, code, infra). Every section in this skill exists for a reason. "This is a strategy doc so implementation sections don't apply" is always wrong — implementation details are where strategy breaks down. If a section genuinely has zero findings, say "No issues found" and move on — but you must evaluate it.
Anti-shortcut clause: The plan file is the OUTPUT of the interactive review, not a substitute for it. Writing every finding into one plan write and calling ExitPlanMode without firing AskUserQuestion is the precise failure mode of the May 2026 transcript bug — the model explored, found issues, and dumped them into a deliverable rather than walking the user through them. If you have ANY non-trivial finding in any review section, the path from finding to ExitPlanMode goes THROUGH AskUserQuestion. Zero findings in every section is the only path to ExitPlanMode that bypasses AskUserQuestion. If you find yourself wanting to write a plan with findings before asking, stop and call AskUserQuestion now — that's the bug, recognize it.
Section 1: Architecture Review
Evaluate and diagram:
- Overall system design and component boundaries. Draw the dependency graph.
- Data flow — all four paths. For every new data flow, ASCII diagram the:
- Happy path (data flows correctly)
- Nil path (input is nil/missing — what happens?)
- Empty path (input is present but empty/zero-length — what happens?)
- Error path (upstream call fails — what happens?)
- State machines. ASCII diagram for every new stateful object. Include impossible/invalid transitions and what prevents them.
- Coupling concerns. Which components are now coupled that weren't before? Is that coupling justified? Draw the before/after dependency graph.
- Scaling characteristics. What breaks first under 10x load? Under 100x?
- Single points of failure. Map them.
- Security architecture. Auth boundaries, data access patterns, API surfaces. For each new endpoint or data mutation: who can call it, what do they get, what can they change?
- Production failure scenarios. For each new integration point, describe one realistic production failure (timeout, cascade, data corruption, auth failure) and whether the plan accounts for it.
- Rollback posture. If this ships and immediately breaks, what's the rollback procedure? Git revert? Feature flag? DB migration rollback? How long?
EXPANSION and SELECTIVE EXPANSION additions:
- What would make this architecture beautiful? Not just correct — elegant. Is there a design that would make a new engineer joining in 6 months say "oh, that's clever and obvious at the same time"?
- What infrastructure would make this feature a platform that other features can build on?
SELECTIVE EXPANSION: If any accepted cherry-picks from Step 0D affect the architecture, evaluate their architectural fit here. Flag any that create coupling concerns or don't integrate cleanly — this is a chance to revisit the decision with new information.
Required ASCII diagram: full system architecture showing new components and their relationships to existing ones. STOP. AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds. Reminder: Do NOT make any code changes. Review only.
Section 2: Error & Rescue Map
This is the section that catches silent failures. It is not optional. For every new method, service, or codepath that can fail, fill in this table:
METHOD/CODEPATH | WHAT CAN GO WRONG | EXCEPTION CLASS
-------------------------|-----------------------------|-----------------
ExampleService#call | API timeout | TimeoutError
| API returns 429 | RateLimitError
| API returns malformed JSON | JSONParseError
| DB connection pool exhausted| ConnectionPoolExhausted
| Record not found | RecordNotFound
-------------------------|-----------------------------|-----------------
EXCEPTION CLASS | RESCUED? | RESCUE ACTION | USER SEES
-----------------------------|-----------|------------------------|------------------
TimeoutError | Y | Retry 2x, then raise | "Service temporarily unavailable"
RateLimitError | Y | Backoff + retry | Nothing (transparent)
JSONParseError | N ← GAP | — | 500 error ← BAD
ConnectionPoolExhausted | N ← GAP | — | 500 error ← BAD
RecordNotFound | Y | Return nil, log warning | "Not found" message
Rules for this section:
- Catch-all error handling (
rescue StandardError,catch (Exception e),except Exception) is ALWAYS a smell. Name the specific exceptions. - Catching an error with only a generic log message is insufficient. Log the full context: what was being attempted, with what arguments, for what user/request.
- Every rescued error must either: retry with backoff, degrade gracefully with a user-visible message, or re-raise with added context. "Swallow and continue" is almost never acceptable.
- For each GAP (unrescued error that should be rescued): specify the rescue action and what the user should see.
- For LLM/AI service calls specifically: what happens when the response is malformed? When it's empty? When it hallucinates invalid JSON? When the model returns a refusal? Each of these is a distinct failure mode. STOP. AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds. Reminder: Do NOT make any code changes. Review only.
Section 3: Security & Threat Model
Security is not a sub-bullet of architecture. It gets its own section. Evaluate:
- Attack surface expansion. What new attack vectors does this plan introduce? New endpoints, new params, new file paths, new background jobs?
- Input validation. For every new user input: is it validated, sanitized, and rejected loudly on failure? What happens with: nil, empty string, string when integer expected, string exceeding max length, unicode edge cases, HTML/script injection attempts?
- Authorization. For every new data access: is it scoped to the right user/role? Is there a direct object reference vulnerability? Can user A access user B's data by manipulating IDs?
- Secrets and credentials. New secrets? In env vars, not hardcoded? Rotatable?
- Dependency risk. New gems/npm packages? Security track record?
- Data classification. PII, payment data, credentials? Handling consistent with existing patterns?
- Injection vectors. SQL, command, template, LLM prompt injection — check all.
- Audit logging. For sensitive operations: is there an audit trail?
For each finding: threat, likelihood (High/Med/Low), impact (High/Med/Low), and whether the plan mitigates it. STOP. AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds. Reminder: Do NOT make any code changes. Review only.
Section 4: Data Flow & Interaction Edge Cases
This section traces data through the system and interactions through the UI with adversarial thoroughness.
Data Flow Tracing: For every new data flow, produce an ASCII diagram showing:
INPUT ──▶ VALIDATION ──▶ TRANSFORM ──▶ PERSIST ──▶ OUTPUT
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
[nil?] [invalid?] [exception?] [conflict?] [stale?]
[empty?] [too long?] [timeout?] [dup key?] [partial?]
[wrong [wrong type?] [OOM?] [locked?] [encoding?]
type?]
For each node: what happens on each shadow path? Is it tested?
Interaction Edge Cases: For every new user-visible interaction, evaluate:
INTERACTION | EDGE CASE | HANDLED? | HOW?
---------------------|------------------------|----------|--------
Form submission | Double-click submit | ? |
| Submit with stale CSRF | ? |
| Submit during deploy | ? |
Async operation | User navigates away | ? |
| Operation times out | ? |
| Retry while in-flight | ? |
List/table view | Zero results | ? |
| 10,000 results | ? |
| Results change mid-page| ? |
Background job | Job fails after 3 of | ? |
| 10 items processed | |
| Job runs twice (dup) | ? |
| Queue backs up 2 hours | ? |
Flag any unhandled edge case as a gap. For each gap, specify the fix. STOP. AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds. Reminder: Do NOT make any code changes. Review only.
Section 5: Code Quality Review
Evaluate:
- Code organization and module structure. Does new code fit existing patterns? If it deviates, is there a reason?
- DRY violations. Be aggressive. If the same logic exists elsewhere, flag it and reference the file and line.
- Naming quality. Are new classes, methods, and variables named for what they do, not how they do it?
- Error handling patterns. (Cross-reference with Section 2 — this section reviews the patterns; Section 2 maps the specifics.)
- Missing edge cases. List explicitly: "What happens when X is nil?" "When the API returns 429?" etc.
- Over-engineering check. Any new abstraction solving a problem that doesn't exist yet?
- Under-engineering check. Anything fragile, assuming happy path only, or missing obvious defensive checks?
- Cyclomatic complexity. Flag any new method that branches more than 5 times. Propose a refactor. STOP. AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds. Reminder: Do NOT make any code changes. Review only.
Section 6: Test Review
Make a complete diagram of every new thing this plan introduces:
NEW UX FLOWS:
[list each new user-visible interaction]
NEW DATA FLOWS:
[list each new path data takes through the system]
NEW CODEPATHS:
[list each new branch, condition, or execution path]
NEW BACKGROUND JOBS / ASYNC WORK:
[list each]
NEW INTEGRATIONS / EXTERNAL CALLS:
[list each]
NEW ERROR/RESCUE PATHS:
[list each — cross-reference Section 2]
For each item in the diagram:
- What type of test covers it? (Unit / Integration / System / E2E)
- Does a test for it exist in the plan? If not, write the test spec header.
- What is the happy path test?
- What is the failure path test? (Be specific — which failure?)
- What is the edge case test? (nil, empty, boundary values, concurrent access)
Test ambition check (all modes): For each new feature, answer:
- What's the test that would make you confident shipping at 2am on a Friday?
- What's the test a hostile QA engineer would write to break this?
- What's the chaos test?
Test pyramid check: Many unit, fewer integration, few E2E? Or inverted? Flakiness risk: Flag any test depending on time, randomness, external services, or ordering. Load/stress test requirements: For any new codepath called frequently or processing significant data.
For LLM/prompt changes: Check CLAUDE.md for the "Prompt/LLM changes" file patterns. If this plan touches ANY of those patterns, state which eval suites must be run, which cases should be added, and what baselines to compare against. STOP. AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds. Reminder: Do NOT make any code changes. Review only.
Section 7: Performance Review
Evaluate:
- N+1 queries. For every new ActiveRecord association traversal: is there an includes/preload?
- Memory usage. For every new data structure: what's the maximum size in production?
- Database indexes. For every new query: is there an index?
- Caching opportunities. For every expensive computation or external call: should it be cached?
- Background job sizing. For every new job: worst-case payload, runtime, retry behavior?
- Slow paths. Top 3 slowest new codepaths and estimated p99 latency.
- Connection pool pressure. New DB connections, Redis connections, HTTP connections? STOP. AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds. Reminder: Do NOT make any code changes. Review only.
Section 8: Observability & Debuggability Review
New systems break. This section ensures you can see why. Evaluate:
- Logging. For every new codepath: structured log lines at entry, exit, and each significant branch?
- Metrics. For every new feature: what metric tells you it's working? What tells you it's broken?
- Tracing. For new cross-service or cross-job flows: trace IDs propagated?
- Alerting. What new alerts should exist?
- Dashboards. What new dashboard panels do you want on day 1?
- Debuggability. If a bug is reported 3 weeks post-ship, can you reconstruct what happened from logs alone?
- Admin tooling. New operational tasks that need admin UI or rake tasks?
- Runbooks. For each new failure mode: what's the operational response?
EXPANSION and SELECTIVE EXPANSION addition:
- What observability would make this feature a joy to operate? (For SELECTIVE EXPANSION, include observability for any accepted cherry-picks.) STOP. AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds. Reminder: Do NOT make any code changes. Review only.
Section 9: Deployment & Rollout Review
Evaluate:
- Migration safety. For every new DB migration: backward-compatible? Zero-downtime? Table locks?
- Feature flags. Should any part be behind a feature flag?
- Rollout order. Correct sequence: migrate first, deploy second?
- Rollback plan. Explicit step-by-step.
- Deploy-time risk window. Old code and new code running simultaneously — what breaks?
- Environment parity. Tested in staging?
- Post-deploy verification checklist. First 5 minutes? First hour?
- Smoke tests. What automated checks should run immediately post-deploy?
EXPANSION and SELECTIVE EXPANSION addition:
- What deploy infrastructure would make shipping this feature routine? (For SELECTIVE EXPANSION, assess whether accepted cherry-picks change the deployment risk profile.) STOP. AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds. Reminder: Do NOT make any code changes. Review only.
Section 10: Long-Term Trajectory Review
Evaluate:
- Technical debt introduced. Code debt, operational debt, testing debt, documentation debt.
- Path dependency. Does this make future changes harder?
- Knowledge concentration. Documentation sufficient for a new engineer?
- Reversibility. Rate 1-5: 1 = one-way door, 5 = easily reversible.
- Ecosystem fit. Aligns with Rails/JS ecosystem direction?
- The 1-year question. Read this plan as a new engineer in 12 months — obvious?
EXPANSION and SELECTIVE EXPANSION additions:
- What comes after this ships? Phase 2? Phase 3? Does the architecture support that trajectory?
- Platform potential. Does this create capabilities other features can leverage?
- (SELECTIVE EXPANSION only) Retrospective: Were the right cherry-picks accepted? Did any rejected expansions turn out to be load-bearing for the accepted ones? STOP. AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds. Reminder: Do NOT make any code changes. Review only.
Section 11: Design & UX Review (skip if no UI scope detected)
The CEO calling in the designer. Not a pixel-level audit — that's /plan-design-review and /design-review. This is ensuring the plan has design intentionality.
Evaluate:
- Information architecture — what does the user see first, second, third?
- Interaction state coverage map: FEATURE | LOADING | EMPTY | ERROR | SUCCESS | PARTIAL
- User journey coherence — storyboard the emotional arc
- AI slop risk — does the plan describe generic UI patterns?
- DESIGN.md alignment — does the plan match the stated design system?
- Responsive intention — is mobile mentioned or afterthought?
- Accessibility basics — keyboard nav, screen readers, contrast, touch targets
EXPANSION and SELECTIVE EXPANSION additions:
- What would make this UI feel inevitable?
- What 30-minute UI touches would make users think "oh nice, they thought of that"?
Required ASCII diagram: user flow showing screens/states and transitions.
If this plan has significant UI scope, recommend: "Consider running /plan-design-review for a deep design review of this plan before implementation." STOP. AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds. Reminder: Do NOT make any code changes. Review only.
Outside Voice — Independent Plan Challenge (default-on)
After all review sections are complete, run an independent second opinion from a
different AI system automatically — it is a standard part of plan review, not an
opt-in. Two models agreeing on a plan is stronger signal than one model's thorough
review. The user turns this off only by asking explicitly
(gstack-config set codex_reviews disabled).
Preflight — decide whether and how the outside voice runs:
# Codex preflight: one block (functions sourced here don't persist to later blocks).
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || echo off)
_CODEX_CFG=$(~/.claude/skills/gstack/bin/gstack-config get codex_reviews 2>/dev/null || echo enabled)
source ~/.claude/skills/gstack/bin/gstack-codex-probe 2>/dev/null || true
if [ "$_CODEX_CFG" = "disabled" ]; then
_CODEX_MODE="disabled"
# Running-under-Codex presence probe (#2519): a live Codex session exports
# CODEX_THREAD_ID / CODEX_SANDBOX into every shell it spawns (verified
# against a live `codex exec 'env | grep -i codex'` capture, codex 0.147.0).
# Nested codex spawns from inside a Codex host multiply token burn
# (observed: one /review = 15M tokens). GSTACK_FORCE_CODEX_REVIEW=1 forces
# the nested passes anyway.
elif [ "${GSTACK_FORCE_CODEX_REVIEW:-0}" != "1" ] && { [ -n "${CODEX_THREAD_ID:-}" ] || [ -n "${CODEX_SANDBOX:-}" ]; }; then
_CODEX_MODE="under_codex"
elif ! command -v codex >/dev/null 2>&1; then
_CODEX_MODE="not_installed"; _gstack_codex_log_event "codex_cli_missing" 2>/dev/null || true
elif ! _gstack_codex_auth_probe >/dev/null 2>&1; then
_CODEX_MODE="not_authed"; _gstack_codex_log_event "codex_auth_failed" 2>/dev/null || true
else
# Capture the probe's code: 2 means the CLI cannot execute at all, which is a
# different problem (and a different fix) from a model the account can't use.
_gstack_codex_model_probe; _CODEX_MP=$?
if [ "$_CODEX_MP" -eq 2 ]; then
_CODEX_MODE="broken_install"
elif [ "$_CODEX_MP" -ne 0 ]; then
_CODEX_MODE="model_unusable"
else
_CODEX_MODE="ready"; _gstack_codex_version_check 2>/dev/null || true
fi
fi
echo "CODEX_MODE: $_CODEX_MODE"
Branch on the echoed CODEX_MODE:
disabled— the user turned Codex reviews off (codex_reviews=disabled). Skip this section entirely; do NOT fall back to a Claude subagent — disabled means no extra review step. Print: "Codex review skipped (codex_reviews disabled). Re-enable:gstack-config set codex_reviews enabled."not_installed— Codex CLI absent. Print: "Codex not installed — falling back to a Claude subagent (fresh context, but the SAME model family — not an outside model). Install Codex for an actual outside-model read:npm install -g @openai/codex." Fall back to the Claude subagent path.under_codex— this session is already running INSIDE a Codex host, so spawning codex again is the same model reviewing itself at multiplied token cost (#2519). Print exactly one line: "[running under Codex — nested codex passes skipped; set GSTACK_FORCE_CODEX_REVIEW=1 to force]" and skip the codex invocations below; run the section's free in-host pass instead if it defines one.not_authed— installed but no credentials. Print: "Codex installed but not authenticated — falling back to a Claude subagent (same model family, not an outside model). Runcodex loginor set$CODEX_API_KEY." Fall back to the Claude subagent path.broken_install— the CLI is on PATH but cannot execute (spawn ENOENT, non-executable binary, missing vendor payload). Print: "Codex is installed but its binary cannot run — Codex passes skipped. Reinstall:npm install -g @openai/codex." Relay the probe's HINT lines and fall back to the Claude subagent path. This state exists because a missing binary used to land in the model probe's fail-open bucket and reportready, so every Codex pass was skipped silently (#2742).model_unusable— authed but the account cannot use its configured model (#2477: HTTP 400 on every call, usually a stalemodel =pin in~/.codex/config.toml). Relay the probe's HINT lines, tell the user the one-line fix (update the pin;[notice.model_migrations]names the replacement), and fall back to the Claude subagent path. The ~10s round trip is cached for 1h; timeouts fail open toready.ready— run the Codex pass below.
When the mode is ready, not_installed, or not_authed, print one line so the off-switch
stays discoverable: "Running the outside voice automatically (standard step). Disable: gstack-config set codex_reviews disabled."
Construct the plan review prompt (for ready, not_installed, and not_authed — skip only on disabled).
Read the plan file being reviewed (the file the user pointed this review at, or the branch
diff scope). If a CEO plan document was written in Step 0D-POST, read that too — it contains
the scope decisions and vision.
Construct this prompt (substitute the actual plan content — if plan content exceeds 30KB, truncate to the first 30KB and note "Plan truncated for size"). Always start with the filesystem boundary instruction:
"IMPORTANT: Do NOT read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or agents/. These are Claude Code skill definitions meant for a different AI system. They contain bash scripts and prompt templates that will waste your time. Ignore them completely. Do NOT modify agents/openai.yaml. Stay focused on the repository code only.\n\nYou are a brutally honest technical reviewer examining a development plan that has already been through a multi-section review. Your job is NOT to repeat that review. Instead, find what it missed. Look for: logical gaps and unstated assumptions that survived the review scrutiny, overcomplexity (is there a fundamentally simpler approach the review was too deep in the weeds to see?), feasibility risks the review took for granted, missing dependencies or sequencing issues, and strategic miscalibration (is this the right thing to build at all?). Be direct. Be terse. No compliments. Just the problems.
THE PLAN: "
If CODEX_MODE: ready — run Codex:
TMPERR_PV=$(mktemp /tmp/codex-planreview-XXXXXXXX)
_REPO_ROOT=$(git rev-parse --show-toplevel) || { echo "ERROR: not in a git repo" >&2; exit 1; }
codex exec "<prompt>" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' -c 'web_search="cached"' < /dev/null 2>"$TMPERR_PV"
Use a 5-minute timeout (timeout: 300000). After the command completes, read stderr:
cat "$TMPERR_PV"
Present the full output verbatim:
CODEX SAYS (plan review — outside voice):
════════════════════════════════════════════════════════════
<full codex output, verbatim — do not truncate or summarize>
════════════════════════════════════════════════════════════
Error handling: All errors are non-blocking — the outside voice is informational.
- Auth failure (stderr contains "auth", "login", "unauthorized"): "Codex auth failed. Run `codex login` to authenticate." Fall back to the Claude subagent below.
- Timeout: "Codex timed out after 5 minutes." Fall back to the Claude subagent below.
- Empty response: "Codex returned no response." Fall back to the Claude subagent below.
If CODEX_MODE: not_installed or not_authed (or Codex errored at runtime):
Dispatch via the Agent tool. The subagent has fresh context and no conversation bias — but it is the SAME model family, not an outside model; weigh its agreement accordingly. Bound it the same way as Codex: cap the dispatch at a 5-minute timeout so "never blocking" is also "never hanging."
Subagent prompt: same plan review prompt as above.
Present findings under an OUTSIDE VOICE (Claude subagent): header.
If the subagent fails or times out: "Outside voice unavailable. Continuing to outputs."
(On CODEX_MODE: disabled you already skipped this section per the preflight — do not reach here.)
Cross-model tension:
After presenting the outside voice findings, note any points where the outside voice disagrees with the review findings from earlier sections. Flag these as:
CROSS-MODEL TENSION:
[Topic]: Review said X. Outside voice says Y. [Present both perspectives neutrally.
State what context you might be missing that would change the answer.]
User Sovereignty: Do NOT auto-incorporate outside voice recommendations into the plan. Present each tension point to the user. The user decides. Cross-model agreement is a strong signal — present it as such — but it is NOT permission to act. You may state which argument you find more compelling, but you MUST NOT apply the change without explicit user approval.
For each substantive tension point, use AskUserQuestion:
"Cross-model disagreement on [topic]. The review found [X] but the outside voice argues [Y]. [One sentence on what context you might be missing.]"
RECOMMENDATION: Choose [A or B] because [one-line reason explaining which argument is more compelling and why]. Completeness: A=X/10, B=Y/10.
Options:
- A) Accept the outside voice's recommendation (I'll apply this change)
- B) Keep the current approach (reject the outside voice)
- C) Investigate further before deciding
- D) Add to TODOS.md for later
Wait for the user's response. Do NOT default to accepting because you agree with the outside voice. If the user chooses B, the current approach stands — do not re-argue.
If no tension points exist, note: "No cross-model tension — both reviewers agree."
Persist the result:
~/.claude/skills/gstack/bin/gstack-review-log '{"skill":"codex-plan-review","timestamp":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'","status":"STATUS","source":"SOURCE","commit":"'"$(git rev-parse --short HEAD)"'"}'
Substitute: STATUS = "clean" if no findings, "issues_found" if findings exist. SOURCE = "codex" if Codex ran, "claude" if subagent ran.
Cleanup: Run rm -f "$TMPERR_PV" after processing (if Codex was used).
Outside Voice Integration Rule
Outside voice findings are INFORMATIONAL until the user explicitly approves each one. Do NOT incorporate outside voice recommendations into the plan without presenting each finding via AskUserQuestion and getting explicit approval. This applies even when you agree with the outside voice. Cross-model consensus is a strong signal — present it as such — but the user makes the decision.
Post-Implementation Design Audit (if UI scope detected)
After implementation, run /design-review on the live site to catch visual issues that can only be evaluated with rendered output.
CRITICAL RULE — How to ask questions
Follow the AskUserQuestion format from the Preamble above. Additional rules for plan reviews:
- One issue = one AskUserQuestion call. Never combine multiple issues into one question.
- Describe the problem concretely, with file and line references.
- Present 2-3 options, including "do nothing" where reasonable.
- For each option: effort, risk, and maintenance burden in one line.
- Map the reasoning to my engineering preferences above. One sentence connecting your recommendation to a specific preference.
- Label with issue NUMBER + option LETTER (e.g., "3A", "3B").
- Zero findings: if a section has zero findings, state "No issues, moving on" and proceed. Otherwise, use AskUserQuestion for each finding — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan.
Required Outputs
"NOT in scope" section
List work considered and explicitly deferred, with one-line rationale each.
"What already exists" section
List existing code/flows that partially solve sub-problems and whether the plan reuses them.
"Dream state delta" section
Where this plan leaves us relative to the 12-month ideal.
Error & Rescue Registry (from Section 2)
Complete table of every method that can fail, every exception class, rescued status, rescue action, user impact.
Failure Modes Registry
CODEPATH | FAILURE MODE | RESCUED? | TEST? | USER SEES? | LOGGED?
---------|----------------|----------|-------|----------------|--------
Any row with RESCUED=N, TEST=N, USER SEES=Silent → CRITICAL GAP.
TODOS.md updates
Present each potential TODO as its own individual AskUserQuestion. Never batch TODOs — one per question. Never silently skip this step. Follow the format in ~/.claude/skills/gstack/review/TODOS-format.md.
For each TODO, describe:
- What: One-line description of the work.
- Why: The concrete problem it solves or value it unlocks.
- Pros: What you gain by doing this work.
- Cons: Cost, complexity, or risks of doing it.
- Context: Enough detail that someone picking this up in 3 months understands the motivation, the current state, and where to start.
- Effort estimate: S/M/L/XL (human team) → with CC+gstack: S→S, M→S, L→M, XL→L
- Priority: P1/P2/P3
- Depends on / blocked by: Any prerequisites or ordering constraints.
Then present options: A) Add to TODOS.md B) Skip — not valuable enough C) Build it now in this PR instead of deferring.
Scope Expansion Decisions (EXPANSION and SELECTIVE EXPANSION only)
For EXPANSION and SELECTIVE EXPANSION modes: expansion opportunities and delight items were surfaced and decided in Step 0D (opt-in/cherry-pick ceremony). The decisions are persisted in the CEO plan document. Reference the CEO plan for the full record. Do not re-surface them here — list the accepted expansions for completeness:
- Accepted: {list items added to scope}
- Deferred: {list items sent to TODOS.md}
- Skipped: {list items rejected}
Diagrams (mandatory, produce all that apply)
- System architecture
- Data flow (including shadow paths)
- State machine
- Error flow
- Deployment sequence
- Rollback flowchart
Stale Diagram Audit
List every ASCII diagram in files this plan touches. Still accurate?
Implementation Tasks
Before closing this review, synthesize the findings above into a flat list of
build-actionable tasks. Each task derives from a specific finding — no padding.
Emit the markdown section AND write a JSONL artifact that /autoplan can
aggregate across phases.
Markdown section (always emit)
## Implementation Tasks
Synthesized from this review's findings. Each task derives from a specific
finding above. Run with Claude Code or Codex; checkbox as you ship.
- [ ] **T1 (P1, human: ~2h / CC: ~15min)** — <component> — <imperative title>
- Surfaced by: <section name> — <specific finding text or line reference>
- Files: <paths to touch>
- Verify: <test command or manual check>
- [ ] **T2 (P2, human: ~30min / CC: ~5min)** — ...
Rules:
- P1 blocks ship; P2 should land same branch; P3 is a follow-up TODO.
- If a finding produced no actionable task, do not invent one.
- If a section had zero findings, emit
_No new tasks from <section>._ - Effort uses the AI-compression table from CLAUDE.md.
JSONL artifact (always write, even if zero tasks)
/autoplan reads this file to aggregate across phases. Build each line with
jq -nc so titles and source findings containing quotes, newlines, or
backslashes serialize cleanly — never use hand-rolled echo / printf.
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
TASKS_DIR="${HOME}/.gstack/projects/${SLUG:-unknown}"
mkdir -p "$TASKS_DIR"
TASKS_FILE="$TASKS_DIR/tasks-ceo-review-$(date +%Y%m%d-%H%M%S).jsonl"
COMMIT=$(git rev-parse HEAD 2>/dev/null || echo unknown)
BRANCH=$(git branch --show-current 2>/dev/null || echo unknown)
RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)-$$"
# Repeat ONE jq invocation per task identified during this review.
# Substitute the placeholders inline with shell variables you set per task:
# TASK_ID (T1, T2, ...), PRIORITY (P1/P2/P3), COMPONENT, TITLE,
# SOURCE_FINDING, EFFORT_HUMAN, EFFORT_CC, FILES_JSON (a JSON array literal
# like '["browse/src/sanitize.ts","browse/src/server.ts"]').
jq -nc \
--arg phase 'ceo-review' \
--arg run_id "$RUN_ID" \
--arg branch "$BRANCH" \
--arg commit "$COMMIT" \
--arg id "$TASK_ID" \
--arg priority "$PRIORITY" \
--arg component "$COMPONENT" \
--arg effort_human "$EFFORT_HUMAN" \
--arg effort_cc "$EFFORT_CC" \
--arg title "$TITLE" \
--arg source_finding "$SOURCE_FINDING" \
--argjson files "$FILES_JSON" \
'{phase:$phase, run_id:$run_id, branch:$branch, commit:$commit, id:$id, priority:$priority, component:$component, files:$files, effort_human:$effort_human, effort_cc:$effort_cc, title:$title, source_finding:$source_finding}' \
>> "$TASKS_FILE"
If jq is not installed, fall back to skipping the JSONL write and warn
the user to install jq for autoplan aggregation. Never hand-roll JSONL.
If zero tasks were identified in this review, still touch the JSONL file
(: > "$TASKS_FILE") so the aggregator sees that the phase produced output
this run (an empty file means "ran, no findings" — distinct from "didn't run").
Completion Summary
+====================================================================+
| MEGA PLAN REVIEW — COMPLETION SUMMARY |
+====================================================================+
| Mode selected | EXPANSION / SELECTIVE / HOLD / REDUCTION |
| System Audit | [key findings] |
| Step 0 | [mode + key decisions] |
| Section 1 (Arch) | ___ issues found |
| Section 2 (Errors) | ___ error paths mapped, ___ GAPS |
| Section 3 (Security)| ___ issues found, ___ High severity |
| Section 4 (Data/UX) | ___ edge cases mapped, ___ unhandled |
| Section 5 (Quality) | ___ issues found |
| Section 6 (Tests) | Diagram produced, ___ gaps |
| Section 7 (Perf) | ___ issues found |
| Section 8 (Observ) | ___ gaps found |
| Section 9 (Deploy) | ___ risks flagged |
| Section 10 (Future) | Reversibility: _/5, debt items: ___ |
| Section 11 (Design) | ___ issues / SKIPPED (no UI scope) |
+--------------------------------------------------------------------+
| NOT in scope | written (___ items) |
| What already exists | written |
| Dream state delta | written |
| Error/rescue registry| ___ methods, ___ CRITICAL GAPS |
| Failure modes | ___ total, ___ CRITICAL GAPS |
| TODOS.md updates | ___ items proposed |
| Scope proposals | ___ proposed, ___ accepted (EXP + SEL) |
| CEO plan | written / skipped (HOLD/REDUCTION) |
| Outside voice | ran (codex/claude) / skipped |
| Lake Score | X/Y recommendations chose complete option |
| Diagrams produced | ___ (list types) |
| Stale diagrams found | ___ |
| Unresolved decisions | ___ (listed below) |
+====================================================================+
Unresolved Decisions
If any AskUserQuestion goes unanswered, note it here. Never silently default.
Handoff Note Cleanup
After producing the Completion Summary, clean up any handoff notes for this branch — the review is complete and the context is no longer needed.
setopt +o nomatch 2>/dev/null || true # zsh compat
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
rm -f ~/.gstack/projects/$SLUG/*-$BRANCH-ceo-handoff-*.md 2>/dev/null || true
Review Log
After producing the Completion Summary above, persist the review result.
PLAN MODE EXCEPTION — ALWAYS RUN: This command writes review metadata to
~/.gstack/ (user config directory, not project files). The skill preamble
already writes to ~/.gstack/sessions/ and ~/.gstack/analytics/ — this is
the same pattern. The review dashboard depends on this data. Skipping this
command breaks the review readiness dashboard in /ship.
~/.claude/skills/gstack/bin/gstack-review-log '{"skill":"plan-ceo-review","timestamp":"TIMESTAMP","status":"STATUS","unresolved":N,"critical_gaps":N,"mode":"MODE","scope_proposed":N,"scope_accepted":N,"scope_deferred":N,"commit":"COMMIT"}'
~/.claude/skills/gstack/bin/gstack-decision-log '{"decision":"CEO review (MODE): SCOPE_SUMMARY","rationale":"VERDICT","scope":"branch","source":"skill","confidence":8}' 2>/dev/null || true
The second command records the accepted scope as a durable cross-session decision so the next session sees what was settled (and why) without re-litigating it. It writes to ~/.gstack/ (same pattern as review-log), is non-interactive, and is best-effort (|| true — never blocks the review). Substitute SCOPE_SUMMARY (e.g. "accepted 4 of 6 proposals" for expansion, or "held scope" / "cut 3 items" for HOLD/REDUCTION) and VERDICT (the one-line verdict from the summary).
Before running this command, substitute the placeholder values from the Completion Summary you just produced:
- TIMESTAMP: current ISO 8601 datetime (e.g., 2026-03-16T14:30:00)
- STATUS: "clean" if 0 unresolved decisions AND 0 critical gaps; otherwise "issues_open"
- unresolved: number from "Unresolved decisions" in the summary
- critical_gaps: number from "Failure modes: ___ CRITICAL GAPS" in the summary
- MODE: the mode the user selected (SCOPE_EXPANSION / SELECTIVE_EXPANSION / HOLD_SCOPE / SCOPE_REDUCTION)
- scope_proposed: number from "Scope proposals: ___ proposed" in the summary (0 for HOLD/REDUCTION)
- scope_accepted: number from "Scope proposals: ___ accepted" in the summary (0 for HOLD/REDUCTION)
- scope_deferred: number of items deferred to TODOS.md from scope decisions (0 for HOLD/REDUCTION)
- COMMIT: output of
git rev-parse --short HEAD
Review Readiness Dashboard
After completing the review, read the review log and config to display the dashboard.
~/.claude/skills/gstack/bin/gstack-review-read
Parse the output. Find the most recent entry for each skill (plan-ceo-review, plan-eng-review, review, plan-design-review, design-review-lite, adversarial-review, codex-review, codex-plan-review). Ignore entries with timestamps older than 7 days. For the Eng Review row, show whichever is more recent between review (diff-scoped pre-landing review) and plan-eng-review (plan-stage architecture review). Append "(DIFF)" or "(PLAN)" to the status to distinguish. For the Adversarial row, show whichever is more recent between adversarial-review (new auto-scaled) and codex-review (legacy). For Design Review, show whichever is more recent between plan-design-review (full visual audit) and design-review-lite (code-level check). Append "(FULL)" or "(LITE)" to the status to distinguish. For the Outside Voice row, show the most recent codex-plan-review entry — this captures outside voices from both /plan-ceo-review and /plan-eng-review.
Source attribution: If the most recent entry for a skill has a `"via"` field, append it to the status label in parentheses. Examples: plan-eng-review with via:"autoplan" shows as "CLEAR (PLAN via /autoplan)". review with via:"ship" shows as "CLEAR (DIFF via /ship)". Entries without a via field show as "CLEAR (PLAN)" or "CLEAR (DIFF)" as before.
Note: autoplan-voices and design-outside-voices entries are audit-trail-only (forensic data for cross-model consensus analysis). They do not appear in the dashboard and are not checked by any consumer.
Display:
+====================================================================+
| REVIEW READINESS DASHBOARD |
+====================================================================+
| Review | Runs | Last Run | Status | Required |
|-----------------|------|---------------------|-----------|----------|
| Eng Review | 1 | 2026-03-16 15:00 | CLEAR | YES |
| CEO Review | 0 | — | — | no |
| Design Review | 0 | — | — | no |
| Adversarial | 0 | — | — | no |
| Outside Voice | 0 | — | — | no |
+--------------------------------------------------------------------+
| VERDICT: CLEARED — Eng Review passed |
+====================================================================+
Review tiers:
- Eng Review (required by default): The only review that gates shipping. Covers architecture, code quality, tests, performance. Can be disabled globally with `gstack-config set skip_eng_review true` (the "don't bother me" setting).
- CEO Review (optional): Use your judgment. Recommend it for big product/business changes, new user-facing features, or scope decisions. Skip for bug fixes, refactors, infra, and cleanup.
- Design Review (optional): Use your judgment. Recommend it for UI/UX changes. Skip for backend-only, infra, or prompt-only changes.
- Adversarial Review (automatic): Always-on for every review. Every diff gets both Claude adversarial subagent and Codex adversarial challenge. Large diffs (200+ lines) additionally get Codex structured review with P1 gate. No configuration needed.
- Outside Voice (optional): Independent plan review from a different AI model when Codex is available (falls back to a same-family Claude subagent otherwise — fresh context, not cross-model). Offered after all review sections complete in /plan-ceo-review and /plan-eng-review. Never gates shipping.
Verdict logic:
- CLEARED: Eng Review has >= 1 entry within 7 days from either `review` or `plan-eng-review` with status "clean" (or `skip_eng_review` is `true`)
- NOT CLEARED: Eng Review missing, stale (>7 days), or has open issues
- CEO, Design, and Codex reviews are shown for context but never block shipping
- If `skip_eng_review` config is `true`, Eng Review shows "SKIPPED (global)" and verdict is CLEARED
Staleness detection: After displaying the dashboard, check if any existing reviews may be stale:
- Content-first rule (diff-scoped rows only: `review`, `adversarial-review`, `codex-review`, ship-stage entries). Parse the `---WTREE---` and `---DIRTY---` sections from the bash output. If an entry has a `wtree` field AND it equals the current `---WTREE---` value, the review is CURRENT — identical content, regardless of commit count, rebase, amend, or whether it was committed yet (wtree equality alone proves identical content; that is the keystone property). Skip the commit-count heuristic for that entry and show no staleness note.
- Plan-tier rows (plan-ceo-review, plan-eng-review, plan-design-review) grade a plan file, not the repo tree — never apply the wtree rule to them; they keep the 7-day freshness logic. If such an entry carries a `plan_sha256` field, you MAY compare it against the current plan file's sha256 and note "plan changed since review" on mismatch.
- Fallback (no `wtree` on the entry, or wtree mismatch): parse the `---HEAD---` section to get the current HEAD commit hash. For each review entry that has a `commit` field: compare it against the current HEAD. If different, count elapsed commits: `git rev-list --count STORED_COMMIT..HEAD`. If that command FAILS (the stored commit was rebased away), grade UNKNOWN and treat as stale — do not error. Display: "Note: {skill} review from {date} may be stale — {N} commits since review"
- For entries without a `commit` field (legacy entries): display "Note: {skill} review from {date} has no commit tracking — consider re-running for accurate staleness detection"
- If all reviews grade CURRENT (wtree match or HEAD match), do not display any staleness notes
Plan File Review Report
After displaying the Review Readiness Dashboard in conversation output, also update the plan file itself so review status is visible to anyone reading the plan.
Detect the plan file
- Check if there is an active plan file in this conversation (the host provides plan file paths in system messages — look for plan file references in the conversation context).
- If not found, skip this section silently — not every review runs in plan mode.
Generate the report
Read the review log output you already have from the Review Readiness Dashboard step above. Parse each JSONL entry. Each skill logs different fields:
- plan-ceo-review: `status`, `unresolved`, `critical_gaps`, `mode`, `scope_proposed`, `scope_accepted`, `scope_deferred`, `commit` → Findings: "{scope_proposed} proposals, {scope_accepted} accepted, {scope_deferred} deferred" → If scope fields are 0 or missing (HOLD/REDUCTION mode): "mode: {mode}, {critical_gaps} critical gaps"
- plan-eng-review: `status`, `unresolved`, `critical_gaps`, `issues_found`, `mode`, `commit` → Findings: "{issues_found} issues, {critical_gaps} critical gaps"
- plan-design-review: `status`, `initial_score`, `overall_score`, `unresolved`, `decisions_made`, `commit` → Findings: "score: {initial_score}/10 → {overall_score}/10, {decisions_made} decisions"
- plan-devex-review: `status`, `initial_score`, `overall_score`, `product_type`, `tthw_current`, `tthw_target`, `mode`, `persona`, `competitive_tier`, `unresolved`, `commit` → Findings: "score: {initial_score}/10 → {overall_score}/10, TTHW: {tthw_current} → {tthw_target}"
- devex-review: `status`, `overall_score`, `product_type`, `tthw_measured`, `dimensions_tested`, `dimensions_inferred`, `boomerang`, `commit` → Findings: "score: {overall_score}/10, TTHW: {tthw_measured}, {dimensions_tested} tested/{dimensions_inferred} inferred"
- codex-review: `status`, `gate`, `findings`, `findings_fixed` → Findings: "{findings} findings, {findings_fixed}/{findings} fixed"
All fields needed for the Findings column are now present in the JSONL entries. For the review you just completed, you may use richer details from your own Completion Summary. For prior reviews, use the JSONL fields directly — they contain all required data.
Produce this markdown table:
```markdown
GSTACK REVIEW REPORT
| Review | Trigger | Why | Runs | Status | Findings |
|---|---|---|---|---|---|
| CEO Review | `/plan-ceo-review` | Scope & strategy | {runs} | {status} | {findings} |
| Codex Review | `/codex review` | Independent 2nd opinion | {runs} | {status} | {findings} |
| Eng Review | `/plan-eng-review` | Architecture & tests (required) | {runs} | {status} | {findings} |
| Design Review | `/plan-design-review` | UI/UX gaps | {runs} | {status} | {findings} |
| DX Review | `/plan-devex-review` | Developer experience gaps | {runs} | {status} | {findings} |
| ``` |
Below the table, add these lines. CODEX and CROSS-MODEL are optional (omit when empty); VERDICT is always present:
- CODEX: (only if codex-review ran) — one-line summary of codex fixes
- CROSS-MODEL: (only if both Claude and Codex reviews exist) — overlap analysis
- VERDICT: list reviews that are CLEAR (e.g., "CEO + ENG CLEARED — ready to implement"). If Eng Review is not CLEAR and not skipped globally, append "eng review required".
Unresolved-decisions status (MANDATORY — never omitted; the report's final non-whitespace line). After VERDICT, end the report (content under the `## GSTACK REVIEW REPORT` heading — a bold label, never a new `## ` heading; exempt from the "omit when empty" rule) with exactly one: the exact unbolded line `NO UNRESOLVED DECISIONS` (a bolded one does NOT count), OR a `UNRESOLVED DECISIONS:` header + one bullet per open item (last bullet = final line; add `+ N unresolved from prior reviews` only when N > 0). This avoids double-counting: list THIS review's open items from context; for prior reviews sum `unresolved` over the latest fresh row per skill (dashboard 7-day window) after you DROP the current skill's row; emit the sentinel only when both are zero.
Write to the plan file
PLAN MODE EXCEPTION — ALWAYS RUN: This writes to the plan file, which is the one file you are allowed to edit in plan mode. The plan file review report is part of the plan's living status.
The report must always be the LAST section of the plan file — never mid-file. Use a single delete-then-append flow:
- Read the plan file (Read tool) to see its full current content. Search the read output for a `## GSTACK REVIEW REPORT` heading anywhere in the file.
- If found, use the Edit tool to DELETE the entire existing section. Match from `## GSTACK REVIEW REPORT` through either the next `## ` heading or end of file, whichever comes first. Replace with the empty string. This applies regardless of where the section currently lives — mid-file deletion is intentional, not a special case. If the Edit fails (e.g., concurrent edit changed the content), re-read the plan file and retry once.
- After the delete (or skipped, if no section existed), append the new `## GSTACK REVIEW REPORT` section at the END of the file. Use the Edit tool to match the file's current last paragraph and add the section after it, or use Write to re-emit the whole file with the section at the end.
- Verify with the Read tool that `## GSTACK REVIEW REPORT` is the last `## ` heading in the file before continuing. If it isn't, repeat steps 2-3 once.
Do NOT replace the section in place. The "replace mid-file" path is what allowed prior versions to leave the report mid-file when an older report already lived there — the user then sees a plan whose review report is not at the bottom and (correctly) rejects it.
Next Steps — Review Chaining
After displaying the Review Readiness Dashboard, recommend the next review(s) based on what this CEO review discovered. Read the dashboard output to see which reviews have already been run and whether they are stale.
Recommend /plan-eng-review if eng review is not skipped globally — check the dashboard output for skip_eng_review. If it is true, eng review is opted out — do not recommend it. Otherwise, eng review is the required shipping gate. If this CEO review expanded scope, changed architectural direction, or accepted scope expansions, emphasize that a fresh eng review is needed. If an eng review already exists in the dashboard but the commit hash shows it predates this CEO review, note that it may be stale and should be re-run.
Recommend /plan-design-review if UI scope was detected — specifically if Section 11 (Design & UX Review) was NOT skipped, or if accepted scope expansions included UI-facing features. If an existing design review is stale (commit hash drift), note that. In SCOPE REDUCTION mode, skip this recommendation — design review is unlikely relevant for scope cuts.
If both are needed, recommend eng review first (required gate), then design review.
Use AskUserQuestion to present the next step. Include only applicable options:
- A) Run /plan-eng-review next (required gate)
- B) Run /plan-design-review next (only if UI scope detected)
- C) Skip — I'll handle reviews manually
docs/designs Promotion (EXPANSION and SELECTIVE EXPANSION only)
At the end of the review, if the vision produced a compelling feature direction, offer to promote the CEO plan to the project repo. AskUserQuestion:
"The vision from this review produced {N} accepted scope expansions. Want to promote it to a design doc in the repo?"
- A) Promote to
docs/designs/{FEATURE}.md(committed to repo, visible to the team) - B) Keep in
~/.gstack/projects/only (local, personal reference) - C) Skip
If promoted, copy the CEO plan content to docs/designs/{FEATURE}.md (create the directory if needed) and update the status field in the original CEO plan from ACTIVE to PROMOTED.
Formatting Rules
- NUMBER issues (1, 2, 3...) and LETTERS for options (A, B, C...).
- Label with NUMBER + LETTER (e.g., "3A", "3B").
- One sentence max per option.
- After each section, pause and wait for feedback.
- Use CRITICAL GAP / WARNING / OK for scannability.
Capture Learnings
If you discovered a non-obvious pattern, pitfall, or architectural insight during this session, log it for future sessions:
~/.claude/skills/gstack/bin/gstack-learnings-log '{"skill":"plan-ceo-review","type":"TYPE","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":N,"source":"SOURCE","files":["path/to/relevant/file"]}'
Types: pattern (reusable approach), pitfall (what NOT to do), preference
(user stated), architecture (structural decision), tool (library/framework insight),
operational (project environment/CLI/workflow knowledge).
Sources: observed (you found this in the code), user-stated (user told you),
inferred (AI deduction), cross-model (both Claude and Codex agree).
Confidence: 1-10. Be honest. An observed pattern you verified in the code is 8-9. An inference you're not sure about is 4-5. A user preference they explicitly stated is 10.
files: Include the specific file paths this learning references. This enables staleness detection: if those files are later deleted, the learning can be flagged.
Only log genuine discoveries. Don't log obvious things. Don't log things the user already knows. A good test: would this insight save time in a future session? If yes, log it.
Brain Calibration Write-Back (Phase 2 / gated)
When the skill makes a typed prediction worth tracking (scope decision,
TTHW target, architectural bet, wedge commitment), it MAY write a
kind=bet take to the brain so a calibration profile builds over time.
Gated on two things:
- Brain trust policy for the active endpoint is
personal(check via~/.claude/skills/gstack/bin/gstack-config get brain_trust_policy@<endpoint-hash>). Shared brains skip write-back to avoid polluting team calibration. - Feature flag
BRAIN_CALIBRATION_WRITEBACKis set (today: false; flips to true when upstream gbrain v0.42+ shipstakes_addMCP op).
When both gates pass, the write-back path uses mcp__gbrain__takes_add
to record a take with weight 0.8 (per SKILL_CALIBRATION_WEIGHTS).
If the MCP op is unavailable, fall back to mcp__gbrain__put_page with
a gstack:takes fence block (documented but uglier path).
Mandatory take frontmatter shape:
kind: bet
holder: <user identity from whoami>
claim: <one-line prediction the skill is making>
weight: 0.8
since_date: <today's date>
expected_resolution: <date in 1-3 months depending on skill>
source_skill: plan-ceo-review
After write, invalidate the affected digests so the next preflight reflects the new state:
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" 2>/dev/null || true
~/.claude/skills/gstack/bin/gstack-brain-cache invalidate product --project "$SLUG" 2>/dev/null || true
~/.claude/skills/gstack/bin/gstack-brain-cache invalidate goals --project "$SLUG" 2>/dev/null || true
~/.claude/skills/gstack/bin/gstack-brain-cache invalidate competitive-intel --project "$SLUG" 2>/dev/null || true
Brain Cache Background Refresh
After the skill's work completes (and telemetry has logged), kick a background refresh of any cache digest that's getting close to its TTL. This is non-blocking — the user doesn't wait. Next invocation benefits from the warm cache.
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" 2>/dev/null || true
(~/.claude/skills/gstack/bin/gstack-brain-cache refresh --project "$SLUG" 2>/dev/null &) || true
Mode Quick Reference
┌────────────────────────────────────────────────────────────────────────────────┐
│ MODE COMPARISON │
├─────────────┬──────────────┬──────────────┬──────────────┬────────────────────┤
│ │ EXPANSION │ SELECTIVE │ HOLD SCOPE │ REDUCTION │
├─────────────┼──────────────┼──────────────┼──────────────┼────────────────────┤
│ Scope │ Push UP │ Hold + offer │ Maintain │ Push DOWN │
│ │ (opt-in) │ │ │ │
│ Recommend │ Enthusiastic │ Neutral │ N/A │ N/A │
│ posture │ │ │ │ │
│ 10x check │ Mandatory │ Surface as │ Optional │ Skip │
│ │ │ cherry-pick │ │ │
│ Platonic │ Yes │ No │ No │ No │
│ ideal │ │ │ │ │
│ Delight │ Opt-in │ Cherry-pick │ Note if seen │ Skip │
│ opps │ ceremony │ ceremony │ │ │
│ Complexity │ "Is it big │ "Is it right │ "Is it too │ "Is it the bare │
│ question │ enough?" │ + what else │ complex?" │ minimum?" │
│ │ │ is tempting"│ │ │
│ Taste │ Yes │ Yes │ No │ No │
│ calibration │ │ │ │ │
│ Temporal │ Full (hr 1-6)│ Full (hr 1-6)│ Key decisions│ Skip │
│ interrogate │ │ │ only │ │
│ Observ. │ "Joy to │ "Joy to │ "Can we │ "Can we see if │
│ standard │ operate" │ operate" │ debug it?" │ it's broken?" │
│ Deploy │ Infra as │ Safe deploy │ Safe deploy │ Simplest possible │
│ standard │ feature scope│ + cherry-pick│ + rollback │ deploy │
│ │ │ risk check │ │ │
│ Error map │ Full + chaos │ Full + chaos │ Full │ Critical paths │
│ │ scenarios │ for accepted │ │ only │
│ CEO plan │ Written │ Written │ Skipped │ Skipped │
│ Phase 2/3 │ Map accepted │ Map accepted │ Note it │ Skip │
│ planning │ │ cherry-picks │ │ │
│ Design │ "Inevitable" │ If UI scope │ If UI scope │ Skip │
│ (Sec 11) │ UI review │ detected │ detected │ │
└─────────────┴──────────────┴──────────────┴──────────────┴────────────────────┘