mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-01 06:24:41 +01:00
* docs(designs): gstack field wave plan (2026-10-12) and the shared simulated Capy host fixture Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(doctor): --min-revision, a launch Bun found off PATH, planning-only pins SKIP gstack-doctor --check gains --min-revision <v>|unknown: below it the revision row FAILs with REVISION_BELOW_MINIMUM naming the Capy wrapper or bin/gstack-upgrade; unknown says so and never claims the minimum. The runtime and pins rows name a supported Bun found in $BUN_INSTALL/bin (not on PATH) with the export/GSTACK_LAUNCH fix (gstack_bun_launch_dir, one helper in bin/gstack-bun-version.sh). --for a planning-only workflow (autoplan, the plan reviews, office-hours) reports a repo test-pin mismatch as SKIP pins (planning-only; ...). New result codes UPGRADE_REFUSED, UPGRADE_FETCH_FAILED, REVISION_BELOW_MINIMUM, CAPY_WRAPPER_STALE, INSTALL_UNVERIFIED with troubleshooting anchors; REVISION_UNMET's fix names bin/gstack-upgrade. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(install): install verdict and install admission at skill start bin/gstack-install-verdict.sh (Bash) and lib/install-verdict.ts (TypeScript) are the one reader of the install verdict gstack-capy-install writes to <state root>/install-verdicts/<sha256(checkout realpath)>.json (checkout, HEAD, workflow and required components, planning-only pins, trailer). The source checkout and capy/support.env resolve through gstack_source_root / sourceRoot, which follow an installed root's .source-path. gstack-skill-start, after SESSION_KIND: an unattended session (any host) refuses on a PATH Bun below the supported minimum; with GSTACK_CAPY=1 an unattended or explicit spawned session also refuses without a passing verdict for this checkout, HEAD and workflow (INSTALL_UNVERIFIED). A refusal prints the INSTALL: row, an install-refused instruction block bound to its SESSION_ID, GSTACK_RESULT ... status=refused run=- and exits 3. Ambient spawned sessions warn; GSTACK_ALLOW_UNSUPPORTED_RUNTIME=1 admits and says so. capy/support.env declares GSTACK_MIN_REVISION and GSTACK_CAPY_MIN_WRAPPER. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(capy-install): launch PATH, Bun reuse and CI pins, setup skip on HEAD, verdict, --check The GSTACK_LAUNCH line carries PATH=<dir>:$PATH when the certified Bun is not first on PATH, plus GSTACK_CAPY=1 and GSTACK_INSTALL_VERDICT, and GSTACK_LAUNCH_PATH: <dir>; the doctor runs with that PATH. A supported Bun in $BUN_INSTALL/bin is reused instead of downloaded again. Project pins are read with bin/gstack-runtime-pins.ts (CI lanes included): when every exact pin is gstack's verified version the archive is installed alongside a different PATH Bun; any other exact pin is RUNTIME_PIN_CONFLICT. Both ./setup runs are skipped when this HEAD was set up with the same hosts, model and browser flag and the check passes (never after a fast-forward); a failed setup ends with gstack: fail setup and is retried next run. --check certifies only; GSTACK_CAPY_LOCK_HELD=1 accepts the bootstrapper's held lock; result anchors come from the generated components table; REV_NOTE names bin/gstack-upgrade. Intentional pin change: test/gstack-capy-install.test.ts --revision trailer now names <root>/bin/gstack-upgrade (the old text named a command that never existed). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(upgrade): bin/gstack-upgrade, the unattended fast-forward every fix line names --check fetches and prints current|behind <installed> -> <remote> (<n> commits); the default fast-forwards to the remote default branch tip and reinstalls (bin/gstack-capy-install on a Capy install, else ./setup --refresh-registered), completing a pending reinstall on the next run. It refuses (UPGRADE_REFUSED, exit 3, nothing changed) a dirty tracked byte, a diverged commit, a pinned checkout, the incoming Bun floor and a held .setup-lock; never stashes or resets; an unreachable remote is UPGRADE_FETCH_FAILED. Fetches are receipted (fail-open). The unattended skill-end UPGRADE_AVAILABLE line is followed by UPGRADE_COMMAND naming it. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(capy): vendored Capy skill, frozen bootstrapper and full wrapper bin/gstack-capy-session capy/skill/gstack/ is the Drive skill's source of truth. scripts/install.sh is a frozen bootstrapper: it prints gstack-capy-skill: wrapper <n> (<path>), clones or fetches under .gstack-capy-install.lock, reads the remote capy/support.env (FETCH_HEAD), fast-forwards a clean unpinned checkout below GSTACK_MIN_REVISION (refusing dirty, diverged or pinned checkouts with the exact commands; GSTACK_CAPY_AUTO_UPGRADE=0 disables), and execs the checkout's own bin/gstack-capy-session with GSTACK_CAPY_LOCK_HELD=1 so the held lock is never taken twice. Offline it says the remote minimum is unknown. A Drive copy older than GSTACK_CAPY_MIN_WRAPPER prints an owner action (CAPY_WRAPPER_STALE). SKILL.md: check at session start, install lazily before the first workflow, the GSTACK_LAUNCH line, the trailer rule, the subagent first-report line, gstack-pregate sealed before any push, and the autoplan/DRIVER.md pointer. The context-budget fixture gains the skill's entry. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: Capy install layers, lazy install, snapshot bake, forward-only recovery, raising the minimum docs/capy-install.md describes the three layers (Drive bootstrapper, full wrapper, installer), the lazy install, the GSTACK_LAUNCH line, install admission, baking gstack into a Capy snapshot, and that recovery after an interrupted fast-forward completes or refuses the pending install stages (checking out an older commit is not a rollback). docs/CHANGELOG_STYLE.md (the release doc; there is no docs/RELEASING.md) says when GSTACK_MIN_REVISION may be raised. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * refactor(capy): the stale-copy owner action moves from the frozen bootstrapper to bin/gstack-capy-session The checkout-owned full wrapper reads GSTACK_CAPY_MIN_WRAPPER through gstack_support_env_get(gstack_source_root) and compares it with the bootstrapper revision it was handed, so the rule can change without recopying the Drive skill. The bootstrapper keeps only what a stale checkout cannot do: clone, fetch, read the remote minimum, fast-forward, exec. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * v1.91.96.0: honest install and self-upgrade on Capy; supported minimum 1.91.96.0 Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: drop the helper-only Capy host fixture test (test-of-test ratchet); lane tests exercise the helper Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: bound the new sync spawns in the install-verdict and wrapper tests (spawnSync tripwire) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * ci(paid-pr-profile): the vendored Capy skill is free-only; free-only files are not prompts needing paid coverage Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore(harness): non-behavioral HARNESS_VERSION decision for the PR-profile free-only rule Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(doctor): lane A runtime and pins cases are platform-neutral (bash paths; a lane-free .bun-version pin) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(designs): field wave plan with amendments and the gate rev 2-3 review record (ARTIFACT_VALID, 3 gate revisions) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(capy-install): follow a project's exact Bun pin above the minimum when gstack has its verified archive (adds Bun 1.4.3) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: the installer follows a verified exact project Bun pin (capy-install, troubleshooting, changelog) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(capy-install): the install reason names the project's pin and gstack's real minimum Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2545 lines
112 KiB
Markdown
2545 lines
112 KiB
Markdown
# Troubleshooting gstack messages
|
||
|
||
Find the message you saw, then follow its fix. Every check gstack runs ends in
|
||
one of three states:
|
||
|
||
- **ran**: the check executed. It may still have found problems.
|
||
- **not run**: gstack chose not to run it (you turned it off, or nothing applied).
|
||
- **unavailable**: gstack tried and could not run it. This is missing coverage,
|
||
never a pass.
|
||
|
||
A message that is not `ran` has this shape:
|
||
|
||
```
|
||
<check>: not run (<reason>). Fix: <command>
|
||
<check> unavailable: <reason> (<detail>). No review ran; this is missing coverage, not a pass. Fix: <command>
|
||
<check>: ran, verdict unverified (<reason>). Fix: read the output above
|
||
```
|
||
|
||
Search this page for the words after `unavailable:` or `not run (`. Each
|
||
section has a stable link anchor; the reason codes and anchors come from
|
||
`lib/gate-outcomes.ts`, and a free test fails if a code has no section here.
|
||
|
||
<a id="gstack-doctor"></a>
|
||
### Check readiness before a skill runs
|
||
|
||
Run `~/.claude/skills/gstack/bin/gstack-doctor` (other hosts: `./setup --status`
|
||
in your gstack checkout prints the doctor's absolute path). It prints one row
|
||
each for the install, state root, Bun, hooks, Codex, the cached Codex model
|
||
check, artifacts sync, the browse bundle, the other compiled binaries, the
|
||
/cso native helper, Claude Code, the largest session journal and recent
|
||
/autoplan guard codes. Each row is `ok`, `warn`,
|
||
`not configured` or `fail` with its fix; only `fail` exits non-zero. It makes
|
||
no paid call: the Codex rows report the cached model check and its age, and
|
||
`--live` runs that check once. Paste its output into bug reports.
|
||
|
||
---
|
||
|
||
## Model policy
|
||
|
||
The [model-policy guide](model-policy.md) explains the two tiers, all six
|
||
settings, precedence and the read-only inspection command. A resolved selection
|
||
is not proof that the requested provider or model can run.
|
||
|
||
<a id="model-policy-config"></a>
|
||
### Model policy configuration is invalid or unreadable
|
||
|
||
**Meaning.** The new policy path could not read a coherent configuration or a
|
||
configured value is invalid. An unreadable file is not treated as an absent
|
||
setting: doing that could silently select the premium default.
|
||
|
||
**Fix.** Repair the file or key named by the error. `plan_review_tier` accepts
|
||
`frontier`, `smart` or `host`; `implementation_tier` accepts `frontier` or
|
||
`smart`. Use `gstack-config unset <key>` to restore a default, not an empty
|
||
value. A rejected `set` leaves the old value intact. Correct ownership/read
|
||
permissions rather than deleting an unrelated state directory.
|
||
For a malformed line such as `plan_review_tier = smart`, edit the reported file
|
||
and correct or remove that line first: `unset` only removes canonical `key:`
|
||
records and cannot repair a misspelled delimiter or quoted key.
|
||
|
||
**Expected result.** `gstack-models list` or the corresponding `resolve`
|
||
command explains the winning source. No outside model call ran on this error.
|
||
|
||
<a id="model-policy-provider"></a>
|
||
### A custom provider needs an explicit model or host mode
|
||
|
||
**Meaning.** Native settings or environment flags route the reviewer to a
|
||
custom endpoint or partner platform. A public API catalog identifier is not
|
||
necessarily a valid deployment identifier there. Malformed native provider
|
||
configuration also prevents gstack from assuming the public endpoint.
|
||
|
||
**Fix.** Supply a compatible model for that request, set the provider's gstack
|
||
environment override or tier-model setting, or choose
|
||
`gstack-config set plan_review_tier host`. Keep credentials in the provider's
|
||
normal authentication mechanism, never in a model setting. If the error names
|
||
a malformed native configuration, fix that source first.
|
||
|
||
**Expected result.** Inspection reports an explicit selection or honest
|
||
host-managed delegation. It does not certify account entitlement or send a
|
||
public default to an inferred custom endpoint.
|
||
|
||
<a id="model-policy-selection"></a>
|
||
### The selected review model could not be used
|
||
|
||
**Meaning.** The requested model was rejected or the provider could not
|
||
complete the review. A model may be nonexistent, unavailable to the account,
|
||
retired, or behind a misconfigured endpoint; preserve the provider's evidence
|
||
rather than treating all of these as the same diagnosis. No successful outside
|
||
review is implied, and gstack does not substitute a different model.
|
||
|
||
**Fix.** Change the source named in the error. For example, an environment
|
||
override beats a tier setting, so editing the tier cannot replace it. Unset or
|
||
correct that override, or explicitly name a supported model for the request.
|
||
Follow the existing auth, quota, sandbox or timeout repair when that is the
|
||
reported failure. Missing usage identity remains unknown, even if inspection
|
||
showed a requested ID.
|
||
|
||
**Expected result.** The next invocation announces its model and source,
|
||
checks that same selection and reports the actual provider result. A cached
|
||
probe or a native fallback is not a completed outside review.
|
||
|
||
<a id="model-policy-freshness"></a>
|
||
### Model-catalog freshness is unknown, stale or needs an update
|
||
|
||
**Meaning.** Official evidence changed, a required source could not be
|
||
checked, or the retained evidence does not match this catalog and parser.
|
||
`unknown/source-unavailable` is not current. A later source failure must not
|
||
erase an earlier retirement finding or close its maintenance issue.
|
||
|
||
**Fix.** Read the failed source and evidence details in the freshness workflow
|
||
report and tracking issue. Repair the source parser or publication access when
|
||
needed, then run the workflow manually against the default branch. If a model
|
||
was superseded or deprecated, follow the [maintainer checklist](model-policy.md#maintainer-update-checklist)
|
||
to qualify a replacement or record a supported mitigation. Do not clear the
|
||
issue just because a new release has not been adopted yet.
|
||
|
||
**Expected result.** Complete source checks produce evidence bound to the
|
||
current catalog, while unresolved lifecycle findings remain visible. Personal
|
||
pins and unrelated PRs are unchanged. Check the workflow's last run date too:
|
||
a scheduled job that never runs cannot report its own absence.
|
||
|
||
#### Repairing `recoveryRequired` state
|
||
|
||
This flag deliberately keeps the issue open until a maintainer repairs the
|
||
record. Another successful fetch alone cannot certify corrupted history.
|
||
|
||
1. Save the current issue body, including its retained recovery archive, and
|
||
download the latest trustworthy workflow report. Use its `report.state` as
|
||
the starting state; do not invent run IDs, catalog hashes or check dates.
|
||
2. Reconcile its `lifecycle` array with the issue's independent lifecycle block
|
||
and any retained archive. Preserve known retirements, deadlines and human
|
||
dispositions. A model currently in the catalog must not keep an old
|
||
`resolvedByCatalog` flag from a previous replacement.
|
||
3. In the issue editor, restore valid JSON between the `state:begin` and
|
||
`state:end` marker lines. Put that same lifecycle array between
|
||
`lifecycle:begin` and `lifecycle:end`. Restore the standalone ownership and
|
||
region markers, and leave human text outside the owned region intact.
|
||
4. Set `recoveryRequired` to `false` only after that reconciliation. If prior
|
||
successful evidence cannot be verified, set `lastSuccess` to `null` rather
|
||
than making up a fresh success. If lifecycle history cannot be recovered,
|
||
leave the flag set and the issue open for investigation.
|
||
5. Run **Model-policy freshness** against the default branch. Only complete,
|
||
current evidence with no unresolved findings can close the issue. A parsing
|
||
or source error leaves it open; do not clear history to manufacture a pass.
|
||
|
||
## Outside reviews (Codex and Claude Code)
|
||
|
||
Outside reviews send your diff, plan or question to a second AI provider. Their
|
||
verdict comes from `lib/outside-review-result.ts`, which prints
|
||
`VERDICT: clean|findings|unverified|unavailable`. A `findings` verdict with a
|
||
P0 or P1 finding blocks exactly like a native P0/P1. `unverified` and
|
||
`unavailable` are missing coverage: /ship and /review continue, show the gap in
|
||
the readiness dashboard and the PR body, and never count it as a pass.
|
||
|
||
<a id="outside-review-verdict"></a>
|
||
### How the verdict is read from a review
|
||
|
||
- **Severity tags.** `[P0]`-`[P3]`, Codex's native `P1:` labels, and a P0-P3
|
||
label at the start of a line after an optional heading, bullet, number or
|
||
bold: `**P1 — x**`, `**P1** — x`, `1. P2 — x`, `P2 —`, `### P1`, or a table
|
||
cell `| P1 |`. P0 and P1 block; P2 and P3 are advisory. A P0-P3 token in that
|
||
label position that none of these forms reads (`## P1/P2 findings`) makes the
|
||
review [`unverified`](#outside-review-unparsed-severity), never `clean`. Code
|
||
fences and inline code are quotes, not labels.
|
||
- **Severity words in label position.** `Severity: High`, `Priority: low`, a
|
||
line that starts with `High:`, `High —`, `[High]` or `**High**` (after an
|
||
optional heading, bullet or number), a bold `**High**` anywhere, or a table
|
||
cell `| high |`. Critical and High block like P0 and P1; Medium and Low are
|
||
advisory like P2 and P3. Words inside prose do not count: "high-level",
|
||
"low-risk", "a medium-term follow-up" and "no critical findings" are not
|
||
findings.
|
||
- **Every review gate.** These rules apply to every gate that summarises a
|
||
review, including the `execution` gate behind `/codex` consult and challenge.
|
||
A reply whose last `Verdict:` line says `not ready` but carries no readable
|
||
severity prints `FINDINGS: unspecified` and exits 3 like a blocking finding.
|
||
- **No findings.** A review with no tag and no label is `clean` only when it
|
||
says so explicitly (`NO_FINDINGS`, "no issues", "did not find any bugs").
|
||
Otherwise it is `unverified` (see below), never `clean`. A `/codex` consult
|
||
answer with no severity labels at all is not a review summary and stays
|
||
`clean`.
|
||
- **Design proposals** (the design-direction voices in /design-consultation
|
||
and /office-hours) are read with the `proposal` gate: a completed proposal
|
||
needs only its `Recommendation: ... because ...` line.
|
||
|
||
Outside-review prompts ask the reviewer to label each finding Critical, High,
|
||
Medium or Low, so most reviews land on `findings` or `clean`.
|
||
|
||
<a id="sourced-helper-location"></a>
|
||
### `gstack: cannot load gstack-codex-probe` / `gstack: cannot locate <helper> (shell: ...)` / `CODEX_MODE: helper_unavailable`
|
||
|
||
**Meaning.** Skills run `gstack-codex-probe` as a command, one subcommand per
|
||
check, so the shell your agent uses does not matter. `cannot load
|
||
gstack-codex-probe` and `CODEX_MODE: helper_unavailable` mean the probe file is
|
||
missing or not executable in your install.
|
||
|
||
`cannot locate <helper> (shell: ...)` comes from a helper that is still loaded
|
||
into your shell with `source` (`gstack-egress-lib.sh`, and the Codex probe when
|
||
a skill rendered before the upgrade sources it). Such a helper finds its own
|
||
directory from bash (`BASH_SOURCE`) or zsh (`%x`). In any other shell (dash,
|
||
sh), or when the shell cannot say which file it is reading, it stops instead of
|
||
guessing a path. The message names the shell it saw.
|
||
|
||
**Fix.** For `cannot load`, re-run `./setup` from your gstack checkout (or
|
||
`/gstack-upgrade`). For `cannot locate`, run the skill from bash or zsh (the
|
||
macOS and Linux defaults), or tell the helper where gstack is installed:
|
||
|
||
```bash
|
||
export GSTACK_ROOT=~/.claude/skills/gstack # your install dir; it holds bin/
|
||
```
|
||
|
||
If you see `gstack: sourcing gstack-codex-probe is deprecated ...`, a skill
|
||
rendered before the upgrade is still sourcing the probe. It keeps working
|
||
until a release on or after 2026-10-21; run `/gstack-upgrade` to re-render
|
||
your skills now.
|
||
|
||
**Expected result.** `~/.claude/skills/gstack/bin/gstack-codex-probe select-model exec`
|
||
prints `CODEX_SEL: <model>` (and `CODEX_MODEL: <model> (exec; source: ...)` on
|
||
stderr) from any shell, and preflights print a `CODEX_MODE` other than
|
||
`helper_unavailable`. `gstack-codex-probe help` lists every subcommand.
|
||
|
||
<a id="codex-sandbox-unavailable"></a>
|
||
### `Codex outside review unavailable: Codex's sandbox could not start here (...)`
|
||
|
||
**Meaning.** Codex runs every command inside a Linux sandbox (bubblewrap). In
|
||
many containers and devcontainers the kernel does not allow unprivileged user
|
||
namespaces, so the sandbox cannot start and every command Codex tries fails.
|
||
gstack reports this instead of trusting a review that read nothing.
|
||
`CODEX_MODE: sandbox_unavailable` is the same condition found by the free
|
||
preflight before any paid call.
|
||
|
||
**What is kept.** Nothing was sent for review, or the review was discarded. Your
|
||
code and files are unchanged.
|
||
|
||
**Fix.** Enable unprivileged user namespaces for the container (for Docker, a
|
||
seccomp profile that allows them), or, inside a container you trust, run
|
||
Codex without its sandbox for this shell:
|
||
|
||
```bash
|
||
export GSTACK_CODEX_NO_SANDBOX=1
|
||
```
|
||
|
||
**Expected result.** With namespaces enabled, the next review prints
|
||
`OUTSIDE_STATUS: completed`. With `GSTACK_CODEX_NO_SANDBOX=1`, every review
|
||
prints `WARNING: GSTACK_CODEX_NO_SANDBOX=1: ...` because Codex can then read
|
||
and write anything your user can. Only the exact value `1` works, and only from
|
||
your shell environment.
|
||
|
||
<a id="outside-review-commands-failed"></a>
|
||
### `... outside review unavailable: the reviewer could not run commands or read the diff (...)`
|
||
|
||
**Meaning.** The reviewer tried to run commands but none succeeded, or its
|
||
answer says it could not run commands or read the diff. A "no issues found"
|
||
written after that is not a review.
|
||
|
||
**What is kept.** Nothing; the answer is shown above but not counted.
|
||
|
||
**Fix.** Read the reviewer's stderr above, repair what it names, and re-run.
|
||
|
||
**Expected result.** `OUTSIDE_STATUS: completed` with a `VERDICT:` line.
|
||
|
||
<a id="outside-review-execution-failed"></a>
|
||
### `... outside review unavailable: the reviewer process failed (exit N: ...)`
|
||
|
||
**Meaning.** The provider CLI exited non-zero. The first stderr line is in the
|
||
parentheses.
|
||
|
||
**What is kept.** Partial output is shown above; it is not counted.
|
||
|
||
**Fix.** Repair the cause in the provider diagnosis (log in again, fix the
|
||
model, check the network), then re-run.
|
||
|
||
**Expected result.** `OUTSIDE_STATUS: completed`.
|
||
|
||
<a id="outside-review-timeout"></a>
|
||
### `... outside review unavailable: the reviewer hit its time limit and was stopped (exit 124)`
|
||
|
||
**Meaning.** The provider did not finish within its deadline and was stopped.
|
||
|
||
**What is kept.** Partial output is shown above; it is not counted.
|
||
|
||
**Fix.** Re-run with a smaller scope, or check the provider's status and your
|
||
network.
|
||
|
||
**Expected result.** `OUTSIDE_STATUS: completed`.
|
||
|
||
<a id="outside-review-empty-response"></a>
|
||
### `... outside review unavailable: the reviewer returned no response`
|
||
|
||
**Meaning.** The provider exited successfully but returned nothing.
|
||
|
||
**Fix.** Read the stderr above, then re-run.
|
||
|
||
**Expected result.** `OUTSIDE_STATUS: completed`.
|
||
|
||
<a id="outside-review-refused"></a>
|
||
### `... outside review unavailable: the reviewer declined to review`
|
||
|
||
**Meaning.** The provider answered with a refusal instead of a review.
|
||
|
||
**Fix.** Re-run; if it declines again, rely on the native review.
|
||
|
||
**Expected result.** `OUTSIDE_STATUS: completed`.
|
||
|
||
<a id="outside-review-missing-markers"></a>
|
||
### `... outside review unavailable: the response lacks the markers this gate requires (...)`
|
||
|
||
**Meaning.** The answer did not contain what the gate checks: a
|
||
`Recommendation: ... because ...` line, a `SCORE:`/`AMBIGUITIES:` pair, or
|
||
severity tags.
|
||
|
||
**Fix.** Re-run the review.
|
||
|
||
**Expected result.** `OUTSIDE_STATUS: completed`.
|
||
|
||
<a id="outside-review-unverified"></a>
|
||
### `... outside review: ran, verdict unverified (...)` / `OUTSIDE_STATUS: unverified` / `GATE: UNVERIFIED`
|
||
|
||
**Meaning.** The review completed but had no severity tag or label and gave
|
||
no explicit no-findings conclusion, so no pass or fail can be read from it
|
||
([how the verdict is read](#outside-review-verdict)).
|
||
|
||
**What is kept.** The full answer is shown above.
|
||
|
||
**Fix.** Read the output above and decide. It is not a pass, and /ship and
|
||
/review list it as missing coverage.
|
||
|
||
<a id="outside-review-unparsed-severity"></a>
|
||
### `... outside review: ran, verdict unverified (the review labels a finding with a P0-P3 severity in a form the parser cannot read (line N: "..."))` / `REASON: unparsed_severity`
|
||
|
||
**Meaning.** The reply starts a line with a P0-P3 token, the place a finding's
|
||
label goes, but in a form gstack does not read as one severity, for example
|
||
`## P1/P2 findings` or `- P2/P3 nits`. gstack will not guess which severity
|
||
applies, so the verdict is unverified instead of `clean`. The quoted line in
|
||
parentheses (also printed as `UNPARSED:`) is the first such label. When the
|
||
same reply also has a readable P0 or P1 finding, that finding blocks
|
||
(`VERDICT: findings`, exit 3) and the unparsed line is still printed.
|
||
|
||
**What is kept.** The full reply is shown above.
|
||
|
||
**Fix.** Read the quoted line and the reply, and decide; it is not a pass.
|
||
Re-run the review and ask the reviewer to label each finding `[P1] <title>` or
|
||
`P1: <title>`. To re-check a saved or corrected reply, run from your gstack
|
||
checkout:
|
||
|
||
```bash
|
||
bun lib/outside-review-result.ts --verdict execution reply.md
|
||
```
|
||
|
||
(use `review` or `structured` for those gates).
|
||
|
||
**Expected result.** `VERDICT: findings` or `VERDICT: clean` with a P0-P3
|
||
`FINDINGS:` value, and no `UNPARSED:` line.
|
||
|
||
<a id="codex-model-unusable"></a>
|
||
### `CODEX_MODE: model_unusable` / `MODEL_UNUSABLE`
|
||
|
||
**Meaning.** Codex rejected the selected model: the account cannot use it
|
||
(HTTP 400), the model is retired, or a custom provider's `base_url` is wrong
|
||
(HTTP 404). The `CODEX_MODEL:` line names the model and where it came from.
|
||
|
||
**Fix.** Choose a model your account can use, or correct the provider:
|
||
|
||
```bash
|
||
export GSTACK_CODEX_MODEL=<supported-model>
|
||
# or edit model / base_url in ${CODEX_HOME:-~/.codex}/config.toml
|
||
```
|
||
|
||
**Expected result.** `CODEX_MODE: ready`.
|
||
|
||
<a id="codex-quota-exhausted"></a>
|
||
### `CODEX_MODE: quota_exhausted` / `MODEL_QUOTA_EXHAUSTED`
|
||
|
||
**Meaning.** Codex refused the call because the account behind it hit its
|
||
usage limit (`You've hit your usage limit`, or `insufficient_quota`). The line
|
||
under the marker is Codex's own message, with its reset time and where to buy
|
||
more. The model choice is fine. gstack reports outside coverage as
|
||
unavailable, never as a pass, and caches the result for 15 minutes, so the
|
||
rest of the run (and other skills) make no Codex call. The HINT line says how
|
||
many minutes remain.
|
||
|
||
**Fix.** Wait for the reset time in Codex's message, or add credits or a
|
||
higher plan for that account. To use a different account, sign in again:
|
||
|
||
```bash
|
||
codex login
|
||
```
|
||
|
||
To re-check before gstack's 15-minute cache expires (for example, right after
|
||
buying credits), skip the cached result for one check, or delete it:
|
||
|
||
```bash
|
||
export GSTACK_CODEX_PROBE_RETRY=1 # unset it again afterwards
|
||
# or
|
||
rm -f ~/.gstack/.codex-model-probe # <state root>/.codex-model-probe
|
||
```
|
||
|
||
**Expected result.** After the reset (or after `codex login`, which changes
|
||
the auth signature and re-probes at once), `CODEX_MODE: ready`.
|
||
|
||
<a id="codex-rate-limited"></a>
|
||
### `CODEX_MODE: unverified (rate_limited)` / `MODEL_PROBE_RATE_LIMITED` / `unavailable: Codex rate-limited the review`
|
||
|
||
**Meaning.** Codex answered HTTP 429 (too many requests), which usually clears
|
||
within seconds. It is a different state from `quota_exhausted` and is never
|
||
cached. At probe time the review still runs, and its own result decides. A 429
|
||
during the review itself means that review failed, so coverage is missing,
|
||
never a pass.
|
||
|
||
**Fix.** Re-run the review in a minute. If 429s persist, check the rate limits
|
||
for the account or API key on the provider's dashboard.
|
||
|
||
**Expected result.** `CODEX_MODE: ready`, and the review completes.
|
||
|
||
<a id="codex-mode-unverified"></a>
|
||
### `CODEX_MODE: unverified` / `MODEL_PROBE_INCONCLUSIVE`
|
||
|
||
**Meaning.** The short model check timed out or hit a network error, so gstack
|
||
could not confirm the model works. The review still runs and its own result is
|
||
checked.
|
||
|
||
**Fix.** Nothing now. If the review then fails, its message says why.
|
||
|
||
<a id="codex-auth-failed"></a>
|
||
### `CODEX_MODE: not_authed` / `AUTH_FAILED`
|
||
|
||
**Meaning.** No Codex credentials were found: no `CODEX_API_KEY`, no
|
||
`OPENAI_API_KEY`, no `auth.json`, and no set environment variable named by a
|
||
custom provider's `env_key` in `config.toml`.
|
||
|
||
**Fix.**
|
||
|
||
```bash
|
||
codex login
|
||
```
|
||
|
||
or export the variable your provider's `env_key` names.
|
||
|
||
**Expected result.** `CODEX_MODE: ready`.
|
||
|
||
<a id="outside-review-disabled"></a>
|
||
### `Codex review skipped (codex_reviews disabled)` / `CODEX_MODE: disabled`
|
||
|
||
**Meaning.** You turned outside reviews off. This is `not run`, never a pass and
|
||
never an outage.
|
||
|
||
**Fix.** To turn them back on:
|
||
|
||
```bash
|
||
gstack-config set codex_reviews enabled
|
||
```
|
||
|
||
<a id="outside-review-consent-missing"></a>
|
||
### `outside review: not run (outside reviews are turned off ... ). Fix: gstack-config set codex_reviews enabled ...` / `OUTSIDE_STATUS: not_run reason=consent_missing`
|
||
|
||
**Meaning.** The send itself refused. `gstack-codex-probe run-with-timeout`,
|
||
`gstack-claude-code`, `gstack-outside-voice` and the unattended
|
||
`gstack-autoplan` runner read `codex_reviews` before they send repository
|
||
content to an outside model. When it is `disabled`, or when the config file
|
||
exists but cannot be read (or holds a value other than `enabled` or
|
||
`disabled`), they print this line, start no CLI and make no API request. An
|
||
unset `codex_reviews` means the default, `enabled`, and the first send on a
|
||
machine prints the [notice](#codex-review-notice) below. A review you ask for by
|
||
name (`/codex`, `/claude-code`) passes `--user-invoked` and still runs. This is
|
||
`not run`, never a pass and never an outage; `/autoplan` records it as the
|
||
phase's outside outcome (`status: disabled`, `outcome: not_run`,
|
||
`reason: consent_missing`).
|
||
|
||
**Fix.** To allow outside reviews again:
|
||
|
||
```bash
|
||
gstack-config set codex_reviews enabled
|
||
gstack-config get --stored codex_reviews # enabled, disabled, unset, or unreadable (exit 2)
|
||
```
|
||
|
||
If the stored read exits 2, fix the permissions or the malformed
|
||
`codex_reviews` line in the `config.yaml` it names, then re-run.
|
||
|
||
<a id="codex-review-notice"></a>
|
||
### `NOTICE: gstack outside reviews send the review prompt and code to Codex (...) using ...`
|
||
|
||
**Meaning.** Outside reviews are on by default. The first one on a machine
|
||
says which provider receives your prompt and code and which login or key pays
|
||
for it. It shows once (gstack records `.codex-review-notice-shown` in its state
|
||
directory) and never blocks the review.
|
||
|
||
**Fix.** Nothing, if that is what you want. To stop sending code to Codex:
|
||
|
||
```bash
|
||
gstack-config set codex_reviews disabled
|
||
```
|
||
|
||
**Expected result.** Later reviews print `Codex review skipped (codex_reviews disabled)`
|
||
or `CODEX_MODE: disabled`, and /ship and /review show the outside review as not run.
|
||
|
||
---
|
||
|
||
## /ship, /review and /document-release
|
||
|
||
<a id="ship-no-version-source"></a>
|
||
### `Shipped without a version change: no version source is configured (no VERSION file, no .gstack/version-path). ...`
|
||
|
||
**Meaning.** /ship found no version file it owns: there is no `VERSION`, no
|
||
`.gstack/version-path`, or release automation (release-please, Changesets,
|
||
semantic-release, a workspace monorepo, a placeholder package.json version)
|
||
owns the version. This is `not run` for the version bump.
|
||
|
||
**What is kept.** Everything else ships. No bump, no CHANGELOG version header,
|
||
no `vX.Y.Z` PR title prefix.
|
||
|
||
**Fix.** To have /ship version releases, create `VERSION`, or pin the file:
|
||
|
||
```bash
|
||
echo package.json > .gstack/version-path && git add .gstack/version-path
|
||
```
|
||
|
||
**Expected result.** The next /ship bumps that file and writes a CHANGELOG header.
|
||
|
||
<a id="ship-version-source-broken"></a>
|
||
### `gstack-version-bump: classify: version source is broken: <path> ...`
|
||
|
||
**Meaning.** The pinned version file does not exist, is empty, unreadable, or
|
||
contains no parsable version. /ship stops instead of inventing `0.0.0.0`.
|
||
|
||
**What is kept.** Nothing was written.
|
||
|
||
**Fix.** Repair that file, or correct `--version-path` / `.gstack/version-path`.
|
||
|
||
**Expected result.** `gstack-version-bump classify` prints the current version.
|
||
|
||
<a id="review-base-stale"></a>
|
||
### `BASE_REFRESH: stale <rev>` / `Base coverage: stale at <rev>`
|
||
|
||
**Meaning.** /review could not fetch the base branch (offline, no credentials,
|
||
or a read-only `.git` such as Codex's sandbox), so it reviewed against your
|
||
local `origin/<base>` at `<rev>`.
|
||
|
||
**Fix.** Run `git fetch origin <base>` outside the sandbox and rerun /review if
|
||
the base has moved.
|
||
|
||
<a id="review-fingerprint-tmpdir"></a>
|
||
### `gstack-wtree: cannot create a temp index under <dir>; set TMPDIR to a writable directory` / `gstack-review-log: cannot create a private object directory under <dir>; no working-tree fingerprint. Set TMPDIR to a writable directory.`
|
||
|
||
**Meaning.** /review fingerprints the working tree in a private temporary
|
||
object directory (Codex's sandbox keeps `.git` read-only). The temp directory
|
||
could not be created, so this review has no fingerprint and later steps cannot
|
||
reuse its evidence.
|
||
|
||
**Fix.** `export TMPDIR=<a writable directory>`, then run /review again.
|
||
|
||
<a id="review-fingerprint-stage"></a>
|
||
### `gstack-wtree: cannot stage the working tree: <git error>`
|
||
|
||
**Meaning.** The fingerprint's objects could not be written. Standalone
|
||
`gstack-wtree` writes into the repository's object store.
|
||
|
||
**Fix.** Run it through /review (`gstack-review-log` supplies a private object
|
||
directory), or make `.git/objects` writable.
|
||
|
||
<a id="plan-audit-not-run"></a>
|
||
### `Plan completion audit: not run (no plan is bound to this branch and no docs/designs/ file matches). Fix: ...`
|
||
|
||
**Meaning.** /ship and /review audit a PR against the plan bound to its branch
|
||
(plan mode, /autoplan, or a `Plan: <path>` line in the PR body). With none bound
|
||
and no matching `docs/designs/` file, they no longer pick the newest unrelated
|
||
plan; the audit is `not run`.
|
||
|
||
**Fix.** Add `Plan: docs/designs/<file>.md` to the PR body, or run /autoplan.
|
||
|
||
**Expected result.** The next /ship or /review prints the audit against that plan.
|
||
|
||
<a id="plan-eng-review-not-persisted"></a>
|
||
### `PERSISTED: no` / `PERSIST: gstack-review-log '{...}'` / `no saved Eng Review: run the PERSIST: line ...`
|
||
|
||
**Meaning.** /plan-eng-review ran where the caller forbids writes (for example
|
||
a delegating agent keeps the reviewer read-only, so it cannot edit the plan it
|
||
judges). The review completed: it ends `DONE` with `PERSISTED: no`, records
|
||
telemetry `success`, and returns the full report and decision ledger in its
|
||
reply. Nothing was saved, so /ship's readiness dashboard has no Eng Review
|
||
record and reports NOT CLEARED with "no saved Eng Review". A real failure (a
|
||
permitted save that failed, stale evidence) still ends `BLOCKED`.
|
||
|
||
**What is kept.** The reply holds the complete report; its last line is the
|
||
review-log command with this review's values.
|
||
|
||
**Fix.** From the reviewed repository, run the reply's last line without the
|
||
`PERSIST: ` prefix:
|
||
|
||
```bash
|
||
~/.claude/skills/gstack/bin/gstack-review-log '{"skill":"plan-eng-review",...}'
|
||
```
|
||
|
||
Or rerun /plan-eng-review where it may write its report and log.
|
||
|
||
**Expected result.** `gstack-review-read` lists the `plan-eng-review` entry and
|
||
/ship's dashboard shows the Eng Review row (CLEAR when its status is `clean`).
|
||
|
||
<a id="learnings-bun-missing"></a>
|
||
### `gstack-learnings-search: bun not found on PATH, so learnings could not be read. Fix: ...`
|
||
|
||
**Meaning.** Learnings search (and `gstack-timeline-read`, "the timeline") needs
|
||
Bun. It exits 127 instead of looking like "no learnings".
|
||
|
||
**Fix.** Install Bun from https://bun.sh, then `cd <gstack checkout> && ./setup`.
|
||
|
||
**Expected result.** Skills print your prior learnings again.
|
||
|
||
<a id="learnings-unavailable"></a>
|
||
### `LEARNINGS: unavailable (<first stderr line>)`
|
||
|
||
**Meaning.** A skill's learnings lookup failed (often Bun is missing). Skills
|
||
used to show nothing here, which looked like "no learnings".
|
||
|
||
**What is kept.** Your learnings file is untouched; the skill continues without them.
|
||
|
||
**Fix.** Read the reason in parentheses. If it names Bun, install it and re-run
|
||
`./setup`.
|
||
|
||
<a id="log-tmpdir-unwritable"></a>
|
||
### `gstack-learnings-log: could not create a temp file in <dir>, so the learning was not recorded. Fix: ...`
|
||
|
||
**Meaning.** The learnings, question and preference logs write through a temp
|
||
file in `${TMPDIR:-/tmp}`. A sandbox that blocks it used to drop the entry silently.
|
||
|
||
**Fix.** `export TMPDIR=<a writable directory>` and log again.
|
||
|
||
<a id="plan-tune-not-calibrated"></a>
|
||
### `CALIBRATION: not calibrated: no recorded signals`
|
||
|
||
**Meaning.** Your logged answers matched no psychographic signal, so /plan-tune
|
||
has nothing to calibrate from. It no longer says "calibrated".
|
||
|
||
**Fix.** Answer more registered questions, then `gstack-developer-profile --derive`.
|
||
|
||
<a id="skill-start-former-bucket"></a>
|
||
### `gstack: earlier data for this project is in projects/<slug> (a worktree's old bucket). Merge it: ...`
|
||
|
||
**Meaning.** Linked worktrees now share the main checkout's project identity.
|
||
Learnings and checkpoints a worktree wrote under its own old bucket still exist.
|
||
|
||
**Fix.** Merge them (logs are de-duplicated, conflicts listed, nothing overwritten):
|
||
|
||
```bash
|
||
gstack-slug --adopt-legacy --from <slug>
|
||
```
|
||
|
||
Or stop the reminder: `gstack-slug --adopt-legacy --dismiss <slug>`.
|
||
|
||
---
|
||
|
||
## Design skills
|
||
|
||
<a id="design-not-available"></a>
|
||
### `DESIGN_NOT_AVAILABLE: <path> --version exited <code>` (or `timed out after 10s`, `is not installed`, `no timeout/gtimeout/perl ...`)
|
||
|
||
**Meaning.** Design skills now run `"$D" --version` with a 10-second deadline
|
||
before claiming `DESIGN_READY`. Exit 137 means macOS killed the binary at launch
|
||
(a bad code signature); setup prints the same condition as
|
||
`design unavailable: <bin> is killed at launch (exit 137) after re-signing`.
|
||
|
||
**What is kept.** Nothing was generated or spent.
|
||
|
||
**Fix.** Re-run setup, which rebuilds and re-signs the binary:
|
||
|
||
```bash
|
||
cd <gstack checkout> && ./setup
|
||
```
|
||
|
||
If the message names a missing timeout tool, install coreutils (`gtimeout`) or perl.
|
||
|
||
**Expected result.** The next design skill prints `DESIGN_READY`.
|
||
|
||
<a id="design-image-model-invalid"></a>
|
||
### `GSTACK_DESIGN_IMAGE_MODEL="<value>" is not a gpt-image model name (expected something like gpt-image-2); fix it or unset it to use gpt-image-2`
|
||
|
||
**Meaning.** The design binary refuses an image tool model override that is not
|
||
a `gpt-image-*` model name, before sending any request.
|
||
|
||
**Fix.** `export GSTACK_DESIGN_IMAGE_MODEL=gpt-image-2` (or another gpt-image
|
||
model your key can use), or `unset GSTACK_DESIGN_IMAGE_MODEL`.
|
||
|
||
<a id="design-taste-profile-unavailable"></a>
|
||
### `TASTE_PROFILE_UNAVAILABLE: could not resolve the project slug (gstack-slug failed). Fix: run ./setup.`
|
||
|
||
**Meaning.** /design-consultation and /design-shotgun load your taste profile.
|
||
This says it could not be found, instead of acting as if you had none.
|
||
|
||
**Fix.** `cd <gstack checkout> && ./setup`.
|
||
|
||
<a id="design-variant-save-failed"></a>
|
||
### `cannot save paid image to <path>: <code> (<cause>). Image bytes were received; saved a recovery copy to <tmp path>. Fix: ...`
|
||
|
||
**Meaning.** The paid image generation succeeded but writing it to the output
|
||
path failed (disk, permissions, or every name through `-999` taken). gstack never
|
||
buys the same image twice and never overwrites an existing image.
|
||
|
||
**What is kept.** The image, at the printed recovery path in your temp directory.
|
||
|
||
**Fix.** Follow the printed fix (free disk space, make the folder writable, or
|
||
pick a new `--output`), then copy the recovery file to where you want it.
|
||
|
||
---
|
||
|
||
## Codex and other env-var hosts
|
||
|
||
<a id="gstack-no-install-found"></a>
|
||
### `gstack: no install found (tried <path>). Fix: ./setup --host <host> from your gstack checkout; ./setup --status shows it.`
|
||
|
||
**Meaning.** On Codex, Factory, OpenCode, Cursor, Copilot and Kiro every bash
|
||
block finds gstack on its own: an exported `GSTACK_ROOT` (with `bin/` and
|
||
`lib/`), then a repo-local install, then the host's global install
|
||
(`$CODEX_HOME/skills/gstack` on Codex). None existed.
|
||
|
||
**Fix.**
|
||
|
||
```bash
|
||
cd <gstack checkout> && ./setup --host codex # or your host
|
||
./setup --status
|
||
```
|
||
|
||
**Expected result.** `./setup --status` lists the install and says the router is
|
||
a real file and its section links resolve.
|
||
|
||
<a id="setup-status-router-symlink"></a>
|
||
### `router is a symlink, which Codex skips` / `section links: N of M broken (first: ...)`
|
||
|
||
**Meaning.** `./setup --status` checks each registered install. Codex ignores a
|
||
symlinked SKILL.md, and a broken section link means a carved skill cannot load
|
||
its next step.
|
||
|
||
**Fix.** `cd <source> && ./setup --host <host>` (the command is printed).
|
||
|
||
<a id="setup-skill-copy-saved"></a>
|
||
### `saved your edited <path> to <backup> (setup rewrites gstack's SKILL.md copies on every run)`
|
||
|
||
**Meaning.** Installed router and nested SKILL.md files are now real copies,
|
||
rewritten by every setup. Your hand edit was saved first.
|
||
|
||
**What is kept.** Your edit, at `<backup>`. Move the customization into your own skill.
|
||
|
||
<a id="setup-skill-copy-backup-failed"></a>
|
||
### `error: could not back up an edited SKILL.md copy under <root>; the previous runtime root is unchanged`
|
||
|
||
**Fix.** Fix permissions on `<state root>/backups`, then `./setup --host <host>`.
|
||
|
||
<a id="upgrade-not-a-checkout"></a>
|
||
### `ERROR: <dir> is not a gstack checkout; nothing was changed. Re-run Step 2.` / `INSTALL_DIR is not set: re-run Step 2 ...`
|
||
|
||
**Meaning.** /gstack-upgrade now checks, before any git command, that it is
|
||
inside gstack's own checkout. A block ran without the path Step 2 printed, so it
|
||
stopped instead of touching your project.
|
||
|
||
**Fix.** Run Step 2 again and prefix the block with `INSTALL_DIR=<printed path>`.
|
||
On Windows (Git Bash) with `INSTALL_DIR` set correctly, older /gstack-upgrade
|
||
text compared two spellings of the same path and refused anyway; recover with
|
||
`cd <install dir> && git pull --ff-only --autostash && ./setup` (see
|
||
[the hook false positive](#setup-hook-does-not-parse)).
|
||
|
||
<a id="skill-over-size-limit"></a>
|
||
### `<host>/<skill>/SKILL.md is <bytes> bytes, over the 160,000-byte limit. Fix: carve sections with usesLazySections() for this skill.`
|
||
|
||
**Meaning.** For contributors: hosts read at most 160,000 bytes of one SKILL.md.
|
||
`bun run gen:skill-docs` fails in the repo and warns under `--install-root`.
|
||
|
||
---
|
||
|
||
## Setup and auto-update
|
||
|
||
<a id="bun-too-old"></a>
|
||
### `gstack needs Bun 1.3.3 or newer (1.4.2 recommended); found <version> at <path>. Nothing was installed or changed.`
|
||
|
||
**Meaning.** Bun older than 1.3.3 silently ignores the build flags that stop
|
||
gstack's compiled tools from reading a project's `.env`, so setup refuses it
|
||
before writing anything. The same link appears on two warnings that do not stop
|
||
setup: `warning: gstack is tested on Bun 1.4.2 (CI pin); found <version>` (1.3.3
|
||
up to 1.4.2 works but is untested) and `warning: could not read the Bun version
|
||
(...)` (setup continued; check `bun --version`).
|
||
|
||
**Fix.**
|
||
|
||
```bash
|
||
bun upgrade
|
||
./setup
|
||
```
|
||
|
||
**Expected result.** `bun --version` prints 1.4.2 or newer and setup finishes
|
||
with no Bun warning.
|
||
|
||
<a id="auto-update-bun-too-old"></a>
|
||
### `gstack auto-update: update held (bun-too-old: found Bun <version> at <path>; gstack <version> needs <floor> or newer); nothing was installed or changed. Fix now: ...`
|
||
|
||
**Meaning.** Team-mode auto-update fetched a release whose setup needs a newer
|
||
Bun than the one setup would run. It left your checkout and installed skills at
|
||
the current revision and prints this line once per session start. (An older
|
||
auto-updater running its first update cannot make this check.)
|
||
|
||
**Fix.** `bun upgrade`; the next session start resumes the update. To update
|
||
now: `bun upgrade && cd <gstack checkout> && git pull --ff-only && ./setup`.
|
||
|
||
<a id="auto-update-incomplete"></a>
|
||
### `gstack auto-update: setup did not finish (<reason>); installed skills may be out of date. gstack retries automatically. Fix now: cd <dir> && ./setup`
|
||
|
||
**Meaning.** The pull worked but setup (or migrations) failed. gstack no longer
|
||
says "just upgraded". It retries after 1 hour, then 6, then 24, and prints this
|
||
line once per session start. A `(bun not found on PATH)` reason means install
|
||
Bun first.
|
||
|
||
**What is kept.** The previous install stays active.
|
||
|
||
**Fix.** Run the printed command.
|
||
|
||
<a id="auto-update-pull-failed"></a>
|
||
### `gstack auto-update: git pull did not finish (exit N: ...); gstack is not updating. Fix now: ...`
|
||
|
||
**Fix.** `cd <gstack checkout> && git pull --ff-only && ./setup`.
|
||
|
||
<a id="setup-hook-does-not-parse"></a>
|
||
### `gstack setup: refusing to register hooks that do not parse (Claude Code would block tool calls with them): <file>:<line>: <error>`
|
||
|
||
**On Windows (Git Bash), from 1.91.31: a false positive.** If every
|
||
TypeScript hook fails with `bun could not bundle the TypeScript entry` (the
|
||
question-log, question-preference, auq-error-fallback and timeline-stop hooks,
|
||
`/autoplan`'s publication hook and `/plan-ceo-review`'s mode-handoff hook),
|
||
your hooks are fine. Git Bash handed Bun its paths as `C:/…` while the checker
|
||
looked them up as `/c/…` (#3088, #3100); the checker on main reads both
|
||
spellings. The same Git Bash spelling made `/gstack-upgrade` stop with
|
||
`ERROR: <dir> is not a gstack checkout; nothing was changed.` One recovery
|
||
covers both: update by hand, then run setup from the update.
|
||
|
||
```bash
|
||
cd ~/.claude/skills/gstack && git pull --ff-only --autostash && ./setup
|
||
```
|
||
|
||
Use your install directory if it is not `~/.claude/skills/gstack` (`./setup
|
||
--status` prints it). Expected result: setup finishes with exit 0 and no
|
||
refusal line, and later auto-updates go through.
|
||
|
||
**Meaning, everywhere else.** Claude Code runs gstack's hook shims through
|
||
`/bin/sh`, and a hook that does not parse exits 2, which blocks the tool call
|
||
it guards in every session. Setup parse-checks every hook it registers and
|
||
every hook a skill's frontmatter runs (`/autoplan`, `/careful`, `/freeze`,
|
||
`/guard`, `/investigate` and `/plan-ceo-review`): the shim, the gstack shell
|
||
helpers it sources (such as `careful/bin/hook-extract.sh` and
|
||
`bin/gstack-state-root.sh`), and the TypeScript it runs with its local imports.
|
||
A merge conflict marker line in any of those files fails as `unresolved merge
|
||
conflict marker`, even when the file still parses (markers inside a heredoc, a
|
||
string or a template literal do). The TypeScript is bundled from the gstack
|
||
checkout, so the `tsconfig.json` or `bunfig.toml` of the project you run it
|
||
from is never read. It registers the hooks that parse, skips the ones listed,
|
||
finishes the rest of the install, and exits non-zero. Claude Code runs hooks
|
||
straight from `~/.claude/skills/gstack`, so a skipped hook that an earlier
|
||
setup registered, or that a skill runs, keeps running the broken file until it
|
||
is fixed. This is a gstack bug, or a half-applied edit or merge in your
|
||
checkout: report the printed `<file>:<line>`.
|
||
|
||
**Fix.**
|
||
|
||
```bash
|
||
git -C ~/.claude/skills/gstack status # half-applied edits or merge conflicts?
|
||
git -C ~/.claude/skills/gstack checkout -- <file> # or finish the merge
|
||
cd ~/.claude/skills/gstack && ./setup
|
||
```
|
||
|
||
**Expected result.** Setup finishes with exit 0 and no refusal line.
|
||
|
||
<a id="auto-update-hook-does-not-parse"></a>
|
||
### `gstack auto-update: update held (hook-does-not-parse: <file>:<line>: <error>); nothing was installed or changed, and your current hooks keep running. ...`
|
||
|
||
**On Windows (Git Bash), from 1.91.31: a false positive.** If the held
|
||
reason is `<hook>.ts:1: bun could not bundle the TypeScript entry`, the
|
||
release is fine: your installed checker misread Git Bash's `C:/…` paths
|
||
(#3088, #3100), and the auto-updater checks every release with it, so it
|
||
holds every release until you update once by hand. Recovery (this also clears
|
||
`/gstack-upgrade`'s `is not a gstack checkout` refusal):
|
||
|
||
```bash
|
||
cd ~/.claude/skills/gstack && git pull --ff-only --autostash && ./setup
|
||
```
|
||
|
||
Use your install directory if it is not `~/.claude/skills/gstack`. Expected
|
||
result: setup finishes with exit 0, the next session start prints no held
|
||
line, and later updates install on their own.
|
||
|
||
**Meaning, everywhere else.** Team-mode auto-update fetched a release with a
|
||
hook that does not parse or still holds a merge conflict marker (the same
|
||
check setup runs). It checked the incoming revision before moving your
|
||
checkout, so your checkout, installed skills and registered hooks stay at the
|
||
current revision. The incoming release's own `bin/gstack-hook-check` decides,
|
||
so a checker bug in your install cannot hold later releases; your installed
|
||
checker decides only when the incoming one crashes, prints nothing or prints
|
||
a report it cannot verify. gstack checks again at the next update check and
|
||
installs the first release whose hooks parse. This is a gstack bug: report the
|
||
printed `<file>:<line>`.
|
||
|
||
**Fix.** Wait for the next release. A manual `git pull` followed by `./setup`
|
||
cannot be checked before the pull; setup then refuses the broken hook (see the
|
||
entry above).
|
||
|
||
<a id="auto-update-hook-check-unavailable"></a>
|
||
### `gstack auto-update: update held (hook-check-unavailable: <reason>); nothing was installed or changed, and your current hooks keep running. ... Fix now: cd <dir> && git pull --ff-only --autostash && ./setup ...`
|
||
|
||
**Meaning.** Neither the incoming release's hook checker nor your installed
|
||
one gave a verified report on the incoming hooks (a crash, no output, an
|
||
unreadable report, or no Bun to bundle the TypeScript hooks), so the
|
||
auto-updater cannot tell whether moving your checkout would break Claude Code
|
||
tool calls. It left your checkout, skills and hooks at the current revision.
|
||
The `<reason>` names what each checker did.
|
||
|
||
**Fix.** Run the printed command. Setup checks every hook it registers and
|
||
says what is wrong (for example that Bun is missing).
|
||
|
||
<a id="setup-runtime-root-contains-checkout"></a>
|
||
### `gstack setup: refusing to replace <root> for <host>: this runtime directory contains the source checkout setup runs from (<checkout>), so replacing it would delete the checkout. Nothing was changed.`
|
||
|
||
**Meaning.** Setup installs Codex, Factory, OpenCode, Cursor, Copilot and
|
||
Antigravity by building a runtime root (such as `~/.cursor/skills/gstack`) and
|
||
swapping it in for the old one, and Kiro by writing into `~/.kiro/skills/gstack`.
|
||
Your gstack checkout lives at that root or inside it, so the swap would delete
|
||
the checkout (#3084), and Kiro would link the checkout into itself. Setup refuses before
|
||
writing anything. `./setup --status` and `gstack-doctor` suggest the move for
|
||
that host instead of `./setup --host <host>`.
|
||
|
||
**Fix.** Move the checkout out, then run setup from its new place. Setup
|
||
prints the exact commands. When `~/.claude/skills/gstack` is a symlink to the
|
||
checkout, the checkout takes the symlink's place:
|
||
|
||
```bash
|
||
rm ~/.claude/skills/gstack && mv ~/.cursor/skills/gstack ~/.claude/skills/gstack
|
||
cd ~/.claude/skills/gstack && ./setup --host claude && ./setup --host cursor
|
||
```
|
||
|
||
Otherwise move it anywhere outside the host's skills directory:
|
||
|
||
```bash
|
||
mv ~/.cursor/skills/gstack ~/gstack && cd ~/gstack && ./setup --host cursor
|
||
```
|
||
|
||
**Expected result.** Both setups finish with exit 0, and `./setup --status`
|
||
lists each host as `current`.
|
||
|
||
<a id="host-renders-pruned"></a>
|
||
### `pruned <dir> (<host> is not installed from this checkout): ...` / `kept <path>: not proven generated (...)` / `host-render backup: <dir> (...)`
|
||
|
||
**Meaning.** A gstack checkout keeps skills only for the agents installed from
|
||
it, listed in `.gstack-installed-hosts` in the checkout (#1694). Older installs
|
||
rendered every agent's copy, so a global Claude install carried 632 `SKILL.md`
|
||
files (34.7 MB) that Claude Code and Cursor-agent scan. setup removed the copy
|
||
for an agent that is not installed from this checkout. Generated files were
|
||
moved to the backup directory it printed; links into the checkout were removed;
|
||
files it could not prove gstack generated (`kept ...`) were left in place.
|
||
`prune.log` in the backup lists every path. The same prune runs once from the
|
||
upgrade migration. Agents in the record are never pruned.
|
||
|
||
**Fix.** Nothing, if you don't use that agent. To get an agent back, install
|
||
it from this checkout, which records it and renders its skills:
|
||
|
||
```bash
|
||
./setup --host <name>
|
||
```
|
||
|
||
For an instruction-only agent you render by hand (Hermes, OpenClaw, GBrain),
|
||
add its name to `.gstack-installed-hosts` first, then run
|
||
`bun run gen:skill-docs --host <name>`. To restore one file, `mv` it back from
|
||
the backup. A `kept` file is yours: move or delete it when you no longer need it.
|
||
The backup sits in gstack's state root, which no agent scans; delete it once
|
||
you are sure you don't need it (about 36 MB for a pre-1.91.65 Claude install).
|
||
|
||
**Expected result.** The next `./setup` prints no `pruned` line, and
|
||
`./setup --status` lists the agents you use.
|
||
|
||
<a id="cso-windows-msvc-compile"></a>
|
||
### `CSO unavailable: its native helper was not built (windows-msvc-compile)`
|
||
|
||
**Meaning.** Visual Studio's compiler was found but the /cso native helper did
|
||
not compile. setup prints the first compiler error. It used to say "install
|
||
Visual Studio".
|
||
|
||
**Fix.** Fix the printed compiler error, then re-run `./setup`.
|
||
|
||
<a id="cso-build-or-publish-failed"></a>
|
||
### `CSO unavailable: the native helper's <stage> step failed (...)` / `CSO publish step failed (...) for <revision>; previous CSO kept: <revision>`
|
||
|
||
**Meaning.** The /cso native helper is optional. Its build prerequisites were
|
||
present, but the `build` step (compiling the helper) or the `publish` step
|
||
(swapping the new helper into `bin/` under a lock) failed. setup used to stop
|
||
here (#3071); now it finishes everything else and says which step failed. When
|
||
an earlier helper was installed and the failed publish restored it, setup
|
||
prints `previous CSO kept` and /cso keeps using that helper; otherwise /cso
|
||
reports `not assessed`. `interrupted` means the step was killed before it could
|
||
record a result. The outcome is in `bin/.gstack-cso-build-result`, and
|
||
`gstack-doctor`'s `cso` row reads the same record. The build output is in
|
||
`bin/.gstack-cso-build.log`.
|
||
|
||
**Fix.** Read the log, fix what it reports, then retry from your gstack checkout:
|
||
|
||
```bash
|
||
bun run build:cso && ./setup
|
||
```
|
||
|
||
On Windows, setup and the build use PowerShell 7 (`pwsh`) when it is installed
|
||
and fall back to Windows PowerShell 5.1. The root cause of the staged files
|
||
vanishing during publish in #3071 is still unknown; attach the log there if you
|
||
hit it. CI sets `GSTACK_STRICT_BUILD=1`, which makes these failures fatal so
|
||
build regressions cannot hide.
|
||
|
||
**Expected result.** setup prints no CSO line, and the doctor's `cso` row is
|
||
`ok`.
|
||
|
||
<a id="cso-windows-docker"></a>
|
||
### `Docker found at <path>, but native Windows Docker transport is not supported yet; static assessment only.` / `docker.exe at <path> is outside the trusted install locations (...)`
|
||
|
||
**Meaning.** On Windows, /cso looks for `docker.exe` only under the install
|
||
folders Windows reports for Program Files, Program Files (x86) and the Windows
|
||
directory, by its real path, with no symlink or junction on the way. A
|
||
user-writable directory is untrusted, because the Docker child carries
|
||
registry credentials; that refusal cannot be overridden.
|
||
|
||
Even a trusted `docker.exe` cannot run /cso's isolated containers yet: /cso
|
||
admits only a local Unix Docker socket, and Docker Desktop on Windows speaks
|
||
over a named pipe. /cso reports this and runs its static assessment only; no
|
||
container or runtime check runs.
|
||
|
||
**Fix.** For runtime checks, run /cso from Linux or macOS (WSL2 counts as
|
||
Linux) with a local Docker socket. On Windows, static assessment is the
|
||
supported mode; if the refusal named a user directory, install Docker Desktop
|
||
under Program Files.
|
||
|
||
<a id="cso-capacity"></a>
|
||
### `Snapshot manifest cap exceeded: ...` / `64 MiB source cap exceeded: ...` / `Symlink or special source file: <path>`
|
||
|
||
**Meaning.** /cso copies the repository into a private snapshot before any
|
||
audit starts, and refuses the whole run when the snapshot would be incomplete
|
||
or too large. Each message names the cap that applied:
|
||
|
||
- **Snapshot manifest cap (16 MiB).** One entry per tracked or nonignored
|
||
untracked file; about 50,000 files fit.
|
||
- **64 MiB source cap.** The full size of every such file outside dependency
|
||
and VCS directories counts, including files over 1 MiB whose contents are
|
||
withheld from the audit.
|
||
- **Symlinks.** A tracked symlink anywhere in the repository stops the run.
|
||
|
||
**Fix.** No setting raises these caps. Run /cso on a smaller checkout that
|
||
holds the code you want audited and no symlinks. Add your file count or size to
|
||
[#2993](https://github.com/garrytan/gstack/issues/2993).
|
||
|
||
<a id="conductor-auq-hook-removed"></a>
|
||
### `removed the AskUserQuestion preference hook: it breaks Conductor's native AskUserQuestion (#2207). ...`
|
||
|
||
**Meaning.** In Conductor, setup no longer installs the plan-tune
|
||
question-preference hook and removes one it added before. The other plan-tune
|
||
hooks stay.
|
||
|
||
**Fix.** To keep it anyway: `gstack-config set plan_tune_hooks yes`, then `./setup`.
|
||
|
||
<a id="browse-extension-id-invalid"></a>
|
||
### `Error: browse_extension_id '<v>' is not a Chrome extension ID ... Existing value left unchanged.`
|
||
|
||
**Fix.** Copy the 32-letter ID (letters a to p) from `chrome://extensions`, then
|
||
`gstack-config set browse_extension_id <id>`.
|
||
|
||
---
|
||
|
||
## Install check (`gstack-doctor --check`) and the Capy installer
|
||
|
||
`gstack-doctor --check` prints one `PASS | FAIL | SKIP <id>` row per component
|
||
and ends with `gstack: ok <revision> for=<workflow>` (exit 0) or
|
||
`gstack: fail <id,id>` (exit 1). Every FAIL row carries a `fix:` clause and one
|
||
of the codes below. Read the trailer directly; never pipe `--check` through
|
||
`tail` or another command whose exit status would replace the doctor's
|
||
(docs/capy-install.md).
|
||
|
||
<a id="doctor-component-missing"></a>
|
||
### `FAIL <id> — <detail>; fix: <command> (COMPONENT_MISSING; ...)`
|
||
|
||
**Meaning.** A component the selected workflow needs (`--for <skill>`,
|
||
`--require <csv>`, or by default every host profile installed from this
|
||
checkout) is not installed, not built, not current, or not authenticated. The
|
||
row names it: `claude` or `codex` (a host profile missing or stale), `patch`
|
||
(the Codex render lacks the model behavioral patch), `codex-cli` (the Codex CLI
|
||
is required and absent or not logged in), `browse-bundle`, `browser` (Chromium
|
||
does not launch or render), `cso` (the native helper is required and not
|
||
built).
|
||
|
||
**Fix.** Run the command in the row's `fix:` clause. For `browser`, that is
|
||
`<checkout>/bin/gstack-browser-ensure`, which installs Chromium on first use
|
||
when the installer skipped it (`SKIP browser (lazy; ...)`).
|
||
|
||
**Expected result.** The row turns `PASS` and the trailer reads `gstack: ok`.
|
||
|
||
<a id="doctor-runtime-below-minimum"></a>
|
||
### `FAIL runtime — bun <version> ... (RUNTIME_BELOW_MINIMUM; ...)`
|
||
|
||
**Meaning.** The Bun on `PATH` is absent, below gstack's security floor
|
||
(1.3.3: older Bun ignores the no-autoload compile flags, see
|
||
[bun-too-old](#bun-too-old)) or below the supported minimum (1.4.2, the CI pin
|
||
and `engines.bun`). The dashboard (`gstack-doctor` without `--check`) warns on
|
||
an untested version; `--check` fails it, because a gate run on an untested
|
||
runtime is the incident this check exists for. The default Capy cloud image
|
||
ships 1.3.14, so a fresh Capy machine fails here until the installer upgrades
|
||
Bun.
|
||
|
||
**Fix.** `bin/gstack-capy-install` upgrades Bun through setup's
|
||
checksum-verified recipe before running `./setup`. By hand: download
|
||
`https://bun.sh/install`, verify its SHA-256 against `GSTACK_BUN_INSTALL_SHA`
|
||
in `bin/gstack-bun-version.sh`, then `BUN_VERSION=1.4.2 bash <file>` and
|
||
re-run `./setup`.
|
||
|
||
<a id="doctor-project-pin-mismatch"></a>
|
||
### `FAIL pins — bun <version> outside engines.bun >=1.4.2 (package.json:<line>; ...) (PROJECT_PIN_MISMATCH; ...)`
|
||
|
||
**Meaning.** The target project (`--project <path>`, default the git toplevel
|
||
of the current directory, never the gstack checkout) pins a Bun or Node
|
||
version the running one does not satisfy. Pins come from `package.json`
|
||
(`packageManager`, `engines`), `.tool-versions`, `.nvmrc`, `.bun-version` and
|
||
`bun-version:` / `node-version:` lines in `.github/workflows/*.yml`. A range
|
||
(`>=1.4.2`) and an exact pin are told apart, and a CI pin keeps its platform
|
||
lane: a Windows job's pin never fails a Linux run. The dashboard warns; `--check`
|
||
fails. The row is `SKIP pins (no bun to evaluate the project pins)` when Bun
|
||
is absent, never silent.
|
||
|
||
**Fix.** Install the pinned version for this platform (the row names each
|
||
constraint with its source), or change the project's pin.
|
||
|
||
<a id="doctor-revision-unmet"></a>
|
||
### `FAIL revision — installed <version> (requested <ref>) (REVISION_UNMET; ...)`
|
||
|
||
**Meaning.** `--revision <ref>` asked the doctor to certify a specific gstack
|
||
revision (a VERSION string, tag, branch or commit) and the checkout is at
|
||
another. An unmet revision is never `ok`.
|
||
|
||
**Fix.** Move the checkout: `<checkout>/bin/gstack-upgrade` (the tip of the default
|
||
branch; `/gstack-upgrade` interactively), or
|
||
`git -C <checkout> fetch origin && git -C <checkout> checkout <ref> && ./setup`.
|
||
`bin/gstack-capy-install --revision <ref>` never moves an existing checkout; it
|
||
reports `gstack: ok <installed> (requested <ref>; run <checkout>/bin/gstack-upgrade to move)`.
|
||
|
||
<a id="capy-install-runtime-conflict"></a>
|
||
### `gstack-capy-install: Bun conflict: gstack needs >=<minimum>, <project> pins <constraint> (RUNTIME_PIN_CONFLICT; ...)`
|
||
|
||
**Meaning.** The installer installs one verified Bun archive: gstack's
|
||
supported minimum, or the project's exact pin when that pin is above the
|
||
minimum and listed in the message's `verified archives:`. Here the two do not
|
||
meet (for example the project pins `bun@1.3.14`, or a version gstack has no
|
||
verified archive for), so the installer changed nothing and stopped before
|
||
`./setup`.
|
||
|
||
**Fix.** Pin a verified version in the project, upgrade gstack to a release
|
||
that verifies the pinned version, or install gstack on a machine whose Bun the
|
||
project accepts. `--project <path>` names
|
||
the project whose pins apply.
|
||
|
||
<a id="browser-ensure-failed"></a>
|
||
### `gstack-browser-ensure: NEEDS_SETUP ... (BROWSER_UNAVAILABLE; ...)`
|
||
|
||
**Meaning.** The first browser skill run on a machine installed without
|
||
Chromium (`SKIP browser (lazy; gstack-browser-ensure installs on first use)`)
|
||
tried to install it through `bunx playwright install chromium` under setup's
|
||
lock, or Chromium is installed but does not launch.
|
||
|
||
**Fix.** Read the installer output above: offline or proxied machines need
|
||
network access to the Playwright CDN; on Ubuntu 24.04+ an AppArmor
|
||
user-namespace error needs `GSTACK_CHROMIUM_NO_SANDBOX=1`; then re-run
|
||
`<checkout>/bin/gstack-browser-ensure`. `--with-browser` on the installer
|
||
front-loads the install on machines that want it now.
|
||
|
||
<a id="capy-install-preflight-failed"></a>
|
||
### `gstack-capy-install: preflight: <tool> not found (INSTALL_PREFLIGHT_FAILED; ...)`
|
||
|
||
**Meaning.** The installer checks `git`, `curl`, `node` and `jq` before
|
||
`./setup`, and installs Bun when it is absent or below the supported minimum.
|
||
One of them is missing, or the Bun install script's checksum did not match
|
||
`GSTACK_BUN_INSTALL_SHA` (the download is discarded, nothing runs).
|
||
|
||
**Fix.** Install the named tool (`apt-get install -y jq`, Node from nodejs.org
|
||
or nvm), then re-run `bin/gstack-capy-install`. A checksum mismatch means the
|
||
upstream installer changed: update `GSTACK_BUN_INSTALL_SHA` only after
|
||
reviewing the new script.
|
||
|
||
<a id="upgrade-refused"></a>
|
||
### `gstack-upgrade: <cause>; nothing was changed; fix: <command> (UPGRADE_REFUSED)`
|
||
|
||
**Meaning.** `bin/gstack-upgrade` moves a checkout only by fast-forward to the
|
||
tip of its remote default branch. It refused before changing anything: the
|
||
directory is not a gstack checkout, HEAD is detached or on another branch (a
|
||
pin), a tracked file is modified, a local commit is not on the remote branch,
|
||
the incoming release's Bun floor is above the Bun on PATH, or another updater
|
||
holds `<state root>/.setup-lock`. It never stashes, resets or discards.
|
||
|
||
**Fix.** Resolve the named cause with the printed command (commit or move the
|
||
listed changes, check out the default branch, move local commits to another
|
||
branch), then re-run `bin/gstack-upgrade`. A reinstall that failed after the
|
||
fast-forward is completed by the next run (`reinstall pending from <version>`).
|
||
|
||
<a id="upgrade-fetch-failed"></a>
|
||
### `gstack-upgrade: could not reach origin (...) (UPGRADE_FETCH_FAILED)`
|
||
|
||
**Meaning.** The remote default branch could not be listed or fetched (offline,
|
||
proxy, credentials), so `--check` cannot say whether the checkout is current.
|
||
It never reports `current` without the fetch.
|
||
|
||
**Fix.** Check network access to the checkout's origin
|
||
(`git -C <checkout> remote -v`), then re-run.
|
||
|
||
<a id="revision-below-minimum"></a>
|
||
### `gstack: fail revision — installed <v> is below the supported minimum <m> (<cause>); fix: ... (REVISION_BELOW_MINIMUM)`
|
||
|
||
**Meaning.** `capy/support.env` on the remote default branch declares the
|
||
oldest revision whose installer, trailer and fix lines are honest
|
||
(`GSTACK_MIN_REVISION`), and this checkout is older. The Capy skill
|
||
bootstrapper reads it from `FETCH_HEAD`, so even a checkout that predates the
|
||
file learns it. `--check` only reports (`fast-forward available`); install
|
||
mode fast-forwards a clean, unpinned checkout, reinstalls and rechecks under
|
||
one lock. The cause names why nothing moved: `pinned by --revision`, `pinned:
|
||
HEAD is detached`, `tracked files changed: <files>`, `diverged: ...` or
|
||
`auto-upgrade disabled` (`GSTACK_CAPY_AUTO_UPGRADE=0`). `gstack-doctor --check
|
||
--min-revision <m>` fails the revision row the same way.
|
||
|
||
**Fix.** Run the fix clause: the bootstrapper without `--check`, or
|
||
`<checkout>/bin/gstack-upgrade`. For a pin, drop `--revision` or pin a
|
||
revision at or above the minimum. An interrupted install after the
|
||
fast-forward leaves the trailer `fail`; the next install run completes it.
|
||
|
||
<a id="capy-wrapper-stale"></a>
|
||
### `gstack-capy-skill: owner action — Drive copy is wrapper <n>, minimum <m>; copy <root>/capy/skill/gstack/ (CAPY_WRAPPER_STALE)`
|
||
|
||
**Meaning.** The Capy skill folder that ran (the bootstrapper's
|
||
`GSTACK_CAPY_WRAPPER`) is older than the checkout's `GSTACK_CAPY_MIN_WRAPPER`.
|
||
The run continues through the checkout's own wrapper (`bin/gstack-capy-session`);
|
||
only a checkout with no such wrapper refuses (`gstack: fail wrapper`).
|
||
|
||
**Fix.** The owner copies `<checkout>/capy/skill/gstack/` over the skill folder
|
||
(on Capy, the user-scope Drive `skills/gstack/`). Agents never write it.
|
||
|
||
<a id="install-unverified"></a>
|
||
### `INSTALL: fail verdict — <detail>; fix: ... (INSTALL_UNVERIFIED)`
|
||
|
||
**Meaning.** On a Capy machine (`GSTACK_CAPY=1`, set by the `GSTACK_LAUNCH`
|
||
line), an unattended or spawned workflow starts only with a passing install
|
||
verdict (`<state root>/install-verdicts/<sha256 of the checkout path>.json`,
|
||
named as `GSTACK_INSTALL_VERDICT`) for this checkout, its current HEAD and a
|
||
workflow at least as wide as the one starting: a verdict for `--for autoplan`
|
||
does not cover `qa`'s browser, and a planning-only pins SKIP does not cover a
|
||
workflow that runs the repo's tests. `gstack-skill-start` printed this row and
|
||
`GSTACK_RESULT: skill=<s> status=refused run=-`, exit 3. The same start refuses
|
||
an unattended run whose Bun on PATH is below the supported minimum
|
||
(`INSTALL: fail runtime ... (RUNTIME_BELOW_MINIMUM)`) on any host.
|
||
|
||
**Fix.** Run the Capy skill installer with `--for <workflow>` on this machine
|
||
and launch with the `GSTACK_LAUNCH` line it prints.
|
||
`GSTACK_ALLOW_UNSUPPORTED_RUNTIME=1` admits anyway (printed as
|
||
`INSTALL_VERDICT: overridden`); record it as a deviation.
|
||
|
||
---
|
||
|
||
## Browser
|
||
|
||
<a id="browse-runtime-version-skew"></a>
|
||
### `[browse] this install's browse CLI (<root>, build <hash>) and the gstack checkout's server bundle (<checkout>, build <hash>) are from different builds, so the server was not started. ...`
|
||
|
||
**Meaning.** On Windows a host runtime root (`~/.codex/skills/gstack` and the
|
||
other env-var hosts) holds a copy of browse with no `node_modules`, so its CLI
|
||
starts the server bundle in the gstack checkout recorded in `.source-path`.
|
||
The checkout was rebuilt (or updated) without refreshing this runtime root, so
|
||
the two builds differ and browse refuses rather than run a mismatched server.
|
||
|
||
**Fix.** Re-run setup from the checkout named in the message; it rebuilds and
|
||
refreshes every runtime root:
|
||
|
||
```bash
|
||
cd <checkout> && ./setup
|
||
```
|
||
|
||
<a id="browse-chain-no-flow"></a>
|
||
### `[browse] chain: no flow to run (stdin was empty)` (or `stdin is a terminal`, `stdin could not be read (EAGAIN)`)
|
||
|
||
**Meaning.** `browse chain` with no arguments runs the JSON flow piped to it.
|
||
Nothing arrived, so it exits 1 before starting a browser. It used to exit 0 on
|
||
Windows having run nothing.
|
||
|
||
**Fix.** Pipe the flow, or pass it as an argument:
|
||
|
||
```bash
|
||
echo '[["goto","https://example.com"],["text"]]' | browse chain
|
||
browse chain 'goto https://example.com | text'
|
||
```
|
||
|
||
<a id="browse-chromium-path-failed"></a>
|
||
### `Chromium at GSTACK_CHROMIUM_PATH=<path> failed to launch: ...`
|
||
|
||
**Meaning.** Headless browse and make-pdf now use `GSTACK_CHROMIUM_PATH`
|
||
(NixOS, macOS 13). That binary could not start.
|
||
|
||
**Fix.** Point `GSTACK_CHROMIUM_PATH` at a working Chromium, or
|
||
`unset GSTACK_CHROMIUM_PATH` to use gstack's bundled one.
|
||
|
||
<a id="browse-profile-in-use"></a>
|
||
### `Headed Chromium profile <dir> is in use by Chromium PID <pid> (started by PID <ppid>: <name>). ...`
|
||
|
||
**Meaning.** Headed browse keeps one profile per project
|
||
(`<project>/.gstack/chromium-profile`). A live browser owns this one, and
|
||
browse never kills it. `... is locked by PID <pid> on another host (<host>)`
|
||
means the lock came from another machine through a synced folder.
|
||
|
||
**Fix.** Close that browser, or `export CHROMIUM_PROFILE=<another dir>`. For the
|
||
other-host case, stop it there, or remove `<dir>/SingletonLock` once you are
|
||
sure nothing uses it.
|
||
|
||
<a id="browse-profile-per-project"></a>
|
||
### `[browse] Headed browse now keeps a profile per project ...`
|
||
|
||
**Meaning.** First use copies your logins from `~/.gstack/chromium-profile`
|
||
when no browser is using it; otherwise it starts fresh and prints how to import.
|
||
`browse profiles` lists them, `browse profiles prune --days 30` removes idle ones.
|
||
|
||
<a id="browse-blocked-address"></a>
|
||
### `Blocked: <host> is a cloud metadata or link-local address` / `... resolves to a cloud metadata or link-local address`
|
||
|
||
**Meaning.** Browse refuses 169.254.0.0/16 (including container credential
|
||
endpoints), 100.100.100.200, fe80::/10 and fc00::/7 in every numeric spelling,
|
||
including after a redirect or a page-driven navigation. The tab is reset to
|
||
`about:blank` and the command fails. This cannot be overridden.
|
||
|
||
<a id="browse-forbidden-origin"></a>
|
||
### `forbidden origin` from the sidebar terminal
|
||
|
||
**Meaning.** The terminal accepts only gstack's extension. A self-built or forked
|
||
extension has a different ID. `BROWSE_EXTENSION_ID` is no longer read.
|
||
|
||
**Fix.** `gstack-config set browse_extension_id <your 32-letter id>`.
|
||
|
||
<a id="browse-type-selector-hint"></a>
|
||
### `hint: "<word>" looks like a CSS selector`
|
||
|
||
**Meaning.** Bare `browse type <text>` types into the focused element. The first
|
||
word looked like a selector, so it was typed as text.
|
||
|
||
**Fix.** `browse type --selector '<sel>' <text>` (put text that starts with `--`
|
||
after `--`).
|
||
|
||
<a id="browse-server-node-missing"></a>
|
||
### `server-node.mjs not found. Run bun run build` (Windows)
|
||
|
||
**Fix.** `cd <gstack checkout> && bun run build`.
|
||
|
||
<a id="freeze-helpers-out-of-date"></a>
|
||
### `[freeze] Hook helpers out of date (partial upgrade?)`
|
||
|
||
**Meaning.** `check-freeze.sh` and `hook-extract.sh` come from different gstack versions.
|
||
|
||
**Fix.** `cd <gstack checkout> && ./setup` (or `/unfreeze`).
|
||
|
||
<a id="browse-chromium-pid-unrecorded"></a>
|
||
### `[browse] Could not record the Chromium PID (CDP SystemInfo.getProcessInfo: <error>); browse stop cannot reap a surviving Chromium.`
|
||
|
||
**Meaning.** For a headless Chromium it launched, browse reads the browser's
|
||
process id over CDP so `browse stop` can kill a browser that outlives the
|
||
server. That read failed or took longer than 2 seconds, so this session's
|
||
browser is not recorded. Browsers gstack did not launch are never recorded or
|
||
killed.
|
||
|
||
**Fix.** After `browse stop`, check for a leftover browser with
|
||
`ps aux | grep -i chrom` and end it with `kill <pid>`.
|
||
|
||
---
|
||
|
||
## Windows
|
||
|
||
<a id="windows-smart-app-control"></a>
|
||
### `Windows blocked compiled gstack binaries at launch (Smart App Control or another application-control policy, #2595)` / `bash: .../browse.exe: Permission denied`
|
||
|
||
**Meaning.** Known issue (#2595, #2124). gstack compiles five binaries on your
|
||
machine with Bun: `browse`, `find-browse`, `design`, `pdf` and
|
||
`gstack-global-discover`. They are unsigned, and a binary built on one machine
|
||
never earns the reputation Smart App Control accepts instead of a signature, so
|
||
Windows 11 with Smart App Control on refuses to start them. Git Bash reports
|
||
that as `Permission denied`, which looks like a file-permission problem but is
|
||
code integrity (PowerShell says `An Application Control policy has blocked this
|
||
file`). setup now runs each binary's `--version` and names the blocked ones and
|
||
the skills that need them: the gstack browser fallback (`/browse`, `/qa`,
|
||
`/qa-only`, `/design-review`, `/canary`, `/benchmark`, `/pair-agent`,
|
||
`/scrape`, `/make-pdf`), the design binary (`/design-consultation`,
|
||
`/design-shotgun`, `/design-html`, `/plan-design-review`) and `/retro global`.
|
||
Every other skill works. `gstack-doctor` shows the same state in its
|
||
`browse bundle` and `binaries` rows. The exact Windows error text has not been
|
||
verified on a Smart App Control machine; setup prints the raw first line beside
|
||
its classification.
|
||
|
||
**Fix.** There is no per-file allowlist for Smart App Control. Today's options:
|
||
|
||
- Run gstack inside WSL (`wsl --install`, then install gstack in the Linux
|
||
distro). WSL runs gstack's Linux build, which Smart App Control does not check.
|
||
- Or turn Smart App Control off in Windows Security > App & browser control >
|
||
Smart App Control settings. On Windows 11 with the April 2026 update you can
|
||
turn it back on later without reinstalling Windows; on older builds turning it
|
||
off is permanent. gstack's binaries stay blocked whenever it is on.
|
||
|
||
Signed release binaries are the real fix and are tracked in `TODOS.md`. After
|
||
Windows allows the binaries, re-run `./setup`; the message goes away.
|
||
|
||
**Expected result.** setup prints no "Windows blocked" line, and
|
||
`gstack-doctor` shows `ok` for `browse bundle` and `binaries`.
|
||
|
||
---
|
||
|
||
## Memory and gbrain
|
||
|
||
Every message below is also in [docs/gbrain-sync-errors.md](gbrain-sync-errors.md)
|
||
with more detail, along with the per-page ingest messages (`FAILED`,
|
||
`re-queued`, `quarantined`, reconcile progress, kept-on-this-machine notices).
|
||
|
||
<a id="gbrain-db-unreachable"></a>
|
||
### `database host unreachable (<code> <host>); your gbrain config is unchanged. Fix: check network or VPN, then re-run /sync-gbrain.`
|
||
|
||
**Meaning.** A DNS or connection error reaching your gbrain database. It used to
|
||
look like a broken config, and setup stripped brain features while offline.
|
||
|
||
**Fix.** Restore the network or VPN, then `/sync-gbrain`.
|
||
|
||
<a id="memory-ingest-unattributed-failures"></a>
|
||
### `[memory-ingest] ERR: gbrain reported N failure(s) it did not attribute to a staged page ... Refusing to advance state.`
|
||
|
||
**What is kept.** Nothing is marked saved, so nothing is lost. After three such
|
||
runs gstack bisects the batch and quarantines the page that breaks it.
|
||
|
||
**Fix.** Re-run `/sync-gbrain`.
|
||
|
||
<a id="memory-reconcile-not-run"></a>
|
||
### `[memory-ingest] reconcile: not run (...)`
|
||
|
||
**Meaning.** After upgrading, gstack re-checks transcripts it had marked saved
|
||
and re-imports the ones missing from your brain. That check needs a local gbrain.
|
||
|
||
**Fix.** `/setup-gbrain`, then `gstack-memory-ingest --reconcile` (`--dry-run` to preview).
|
||
|
||
<a id="memory-ingest-locked"></a>
|
||
### `[memory-ingest] ERR: another memory ingest (pid <N>) is writing <state file>; not run.`
|
||
|
||
**Fix.** Wait for it to finish, then re-run `/sync-gbrain`.
|
||
|
||
<a id="memory-ingest-state-unsaved"></a>
|
||
### `[memory-ingest] ERR: could not save ingest state <path>: <error>.`
|
||
|
||
**Fix.** Fix permissions or free disk space under your gstack state root, then `/sync-gbrain`.
|
||
|
||
<a id="memory-ingest-transcript-checkout"></a>
|
||
### `[memory-ingest] kept N transcript page(s) for <repo> on this machine: gbrain source <id> ...`
|
||
|
||
On a managed gbrain brain (0.60+), an import lands only in a source bound to a
|
||
checkout this host owns, so gstack binds each transcript source to its own
|
||
checkout, `<state root>/gbrain-transcript-sources/<id>/`, before importing.
|
||
The partition stays on this machine, unstamped, and imports on a later run when:
|
||
|
||
- **held by another owner**: the source is bound to another host or another
|
||
directory. gstack never takes it over. Inspect it with
|
||
`gbrain sources writer status --source <id> --json`; the owner releases it
|
||
(`gbrain sources writer transfer prepare <id>` on the owning host) or you
|
||
re-bind it here with `gbrain sources set-path <id> <checkout>`.
|
||
- **the state root is ephemeral** (`GSTACK_EPHEMERAL=1`): a binding would point
|
||
at a directory that vanishes with the machine. Run with a durable root:
|
||
`GSTACK_STATE_ROOT=<durable directory> gstack-memory-ingest --bulk`.
|
||
- **bound to gstack's checkout, but it is missing or not active**: restore the
|
||
checkout from a backup of the state root, or follow `gbrain doctor --json`.
|
||
- **could not bind**: the message carries gbrain's refusal; inspect the source
|
||
with the writer status command above.
|
||
|
||
<a id="gbrain-artifacts-not-indexed"></a>
|
||
### `curated artifacts pushed to git, but gbrain source <id> has 0 indexed pages. Fix: gbrain sync --source <id>, then re-run /sync-gbrain`
|
||
|
||
**Fix.** `gbrain sync --source <id>`, then `/sync-gbrain`.
|
||
|
||
<a id="gbrain-dream-skipped"></a>
|
||
### `dream: skipped — the installed gbrain cannot run only the resolve_symbol_edges phase, and the full dream cycle costs about 35 minutes ...`
|
||
|
||
**Meaning.** `/sync-gbrain --dream` (and `--full`'s auto-build) now runs only the
|
||
call-graph phase. Your gbrain cannot scope it, so nothing ran.
|
||
|
||
**Fix.** `gstack-gbrain-install`, then `/sync-gbrain --dream`. Or run the full
|
||
cycle yourself: `gbrain dream --source <id>`.
|
||
|
||
<a id="gbrain-cycle-freshness-unknown"></a>
|
||
### `call graph for <source>: unknown: installed gbrain does not expose cycle_freshness`
|
||
|
||
**Fix.** `gstack-gbrain-install`.
|
||
|
||
<a id="gbrain-path-unavailable"></a>
|
||
### `[gbrain-sync] gbrain source <id>: path unavailable (<path>); gstack skips it.`
|
||
|
||
**Meaning.** A deleted worktree's code source. gstack never removes it on its own.
|
||
|
||
**Fix.** Preview, then remove the ones gstack can prove belonged to this machine:
|
||
|
||
```bash
|
||
gstack-gbrain-sync --prune-gone-worktrees --dry-run
|
||
gstack-gbrain-sync --prune-gone-worktrees
|
||
```
|
||
|
||
---
|
||
|
||
## Eval reports and pass-rates
|
||
|
||
These come from the paid eval census reports and `bun run eval:pass-rates`.
|
||
The walkthrough for a red census is [docs/evals/census-red.md](evals/census-red.md).
|
||
|
||
<a id="pass-rates-unknown-flag"></a>
|
||
### `eval:pass-rates: unknown flag <flag>` / `eval:pass-rates: unknown case <id> (not an E2E, judge or paid test file id)`
|
||
|
||
**Meaning.** The flag or case id is not recognized. The command exits 2 before
|
||
fetching anything.
|
||
|
||
**Fix.** `bun run eval:pass-rates --help` lists the flags. `--case` takes a
|
||
registry id from `test/helpers/touchfiles-data.ts` or a paid test file path.
|
||
|
||
<a id="failure-cause-newer"></a>
|
||
### `unknown failure_cause "<x>" (written by a newer gstack; update this checkout to read it)`
|
||
|
||
**Meaning.** A trial record names a failure cause this checkout does not know.
|
||
A newer gstack wrote it.
|
||
|
||
**Fix.** `git pull` (or rebase your branch onto `main`), then rerun the command.
|
||
|
||
<a id="paid-case-several-owners"></a>
|
||
### `--case <id>: registered by <file>, <file>; it needs exactly one`
|
||
|
||
**Meaning.** More than one paid test file registers the case, so
|
||
`test-paid-shards.ts --case` cannot pick one file.
|
||
|
||
**Fix.** Run the red shard's file directly; the report's `after a repair:` line
|
||
prints it: `EVALS=1 EVALS_TIER=<tier> bun test <file>`.
|
||
|
||
<a id="evidence-too-large"></a>
|
||
### `evidence: <artifact> too large to fetch (<N> MB)`
|
||
|
||
**Meaning.** The slice artifact holding this red's transcript is over 64 MB, so
|
||
`eval:pass-rates --run` does not download it.
|
||
|
||
**Fix.** Download it from the run page, or
|
||
`gh api repos/<owner>/<repo>/actions/artifacts/<artifact id>/zip > slice.zip`.
|
||
|
||
<a id="headroom-alarm"></a>
|
||
### `[headroom] <case> session <key>: max <s> of <s> (<pct>) over <n> sample(s), above the 85% cap. Cut work in the skill or fixture; budgets are never raised ...`
|
||
|
||
**Meaning.** `eval:pass-rates --gate` (the weekly report) found a case whose
|
||
slowest session used more than 85% of the timeout it armed. It is one timeout
|
||
away from a red.
|
||
|
||
**Fix.** Cut work in the skill or fixture. Budgets are never raised. Check the
|
||
case with `bun run eval:pass-rates --headroom --case <id>`.
|
||
|
||
<a id="cost-unknown"></a>
|
||
### `cost unknown (no billing captured for any of <N> trial(s))` / `cost $<x> known + <N> of <M> trial(s) cost unknown`
|
||
|
||
**Meaning.** PTY and Codex sessions record no billing, so their cost is
|
||
unknown. Only the known sum is shown.
|
||
|
||
**Fix.** None needed; this is informational.
|
||
|
||
<a id="cause-provider-stall"></a>
|
||
### `· cause provider_stall: no stream event for <N>s ...`
|
||
|
||
**Meaning.** The session streamed partial messages, then received no event of
|
||
any kind for at least 120 seconds while a model request was in flight and no
|
||
tool, permission prompt, hook or subagent was outstanding. Under EVAL_POLICY v1
|
||
it is still a failed trial.
|
||
|
||
**Fix.** Inspect it with `bun run eval:pass-rates --run <run id> --case <id>`.
|
||
A paid rerun happens only after a repair.
|
||
|
||
<a id="overlay-wrong-answer"></a>
|
||
### Overlay record with `taskCorrect: false` and `answerError` (contract v4)
|
||
|
||
**Meaning.** The trial completed with a wrong answer. Under overlay contract v4
|
||
that is a valid measurement in the comparison; only an overlay-ON wrong answer
|
||
fails the case.
|
||
|
||
**Fix.** Read `answerError` in the trial JSON. No rerun is needed.
|
||
|
||
<a id="detector-corpora-stale"></a>
|
||
### `[detector-corpora] <N> stale entries (refresh from a current census when one fails)`
|
||
|
||
**Meaning.** Some replay-corpus entries came from an older input series of
|
||
their case. This is informational; the corpus still replays them.
|
||
|
||
**Fix.** When that case goes red in a census, add the new capture to its corpus
|
||
and check it with `bun test test/detector-corpus-<case>.test.ts`.
|
||
|
||
<a id="auq-substance-panel"></a>
|
||
### `recommendation substance median <x> < 4 over samples [<a>,<b>,<c>] (boilerplate/weak)`
|
||
|
||
**Meaning.** The auq-matrix case scores each captured question's recommendation
|
||
with a 3-sample judge panel; fewer than 2 of the 3 samples reached 4, so their
|
||
median was below 4.
|
||
|
||
**Fix.** Read the logged samples and the captured question. A fix goes in the
|
||
skill text, followed by one diagnostic run of auq-matrix.
|
||
|
||
---
|
||
|
||
## Push guard (redaction)
|
||
|
||
<a id="redact-postgres-default-pair"></a>
|
||
### `db.url_with_password` HIGH on a default `postgres`/`postgres` login whose host is a service name like `@db` (pre-push BLOCKED)
|
||
|
||
**Meaning.** The default `postgres:postgres` pair is exempt only on `localhost`
|
||
or `127.0.0.1`. A service name like `@db` says nothing about where the URL ends
|
||
up deployed, so it still blocks.
|
||
|
||
**Fix.** Use `postgres://postgres:postgres@localhost:5432/...` in local and CI
|
||
config, or read the URL from an env var. If the credential is real, rotate it.
|
||
Bypass once: `GSTACK_REDACT_PREPUSH=skip git push`.
|
||
If the URL is a reviewed, public dev-only value, list it in
|
||
[`.gstack-redact-allowlist`](#redact-allowlist).
|
||
|
||
<a id="redact-allowlist"></a>
|
||
### `.gstack-redact-allowlist (<n> entries) suppressed <m> finding(s) in this push`
|
||
|
||
**Meaning.** The pushed commit carries `.gstack-redact-allowlist` at the repo
|
||
root. Each line (after trimming; `#` starts a comment) is one exact matched
|
||
span: the whole `postgres://USER:PASSWORD@host:port` URL for
|
||
`db.url_with_password`, the key for a key pattern, the address for `pii.email`. A finding is suppressed only when its
|
||
matched text equals an entry, so a password alone, a substring, or a different
|
||
key still reports and still blocks. The hook reads the file from each pushed
|
||
commit, never the working tree, and ignores a file over 64 KiB. Every push that
|
||
carries entries prints this line and each suppressed finding's file and line.
|
||
It works alongside `gstack.redact.allowEmail`, which stays a local setting.
|
||
|
||
**Fix.** Nothing, if every listed suppression is the reviewed value. Remove an
|
||
entry that no longer applies; rotate a credential that is real.
|
||
|
||
<a id="redact-version-as-ip"></a>
|
||
### `pii.ip_public` MEDIUM on a four-part version number
|
||
|
||
**Meaning.** The line does not declare the value as a version.
|
||
|
||
**Fix.** Write it as `version: 1.2.3.4`, `"version": "..."` or `v1.2.3.4`.
|
||
MEDIUM findings do not block pushes.
|
||
|
||
<a id="redact-prepush-email"></a>
|
||
### `MEDIUM pii.email <file>:<line>` from the pre-push hook
|
||
|
||
**Meaning.** The pushed lines add an email address that is not yours and not
|
||
already in the destination's commit metadata. The hook does not report your
|
||
own address (`git config user.email`), addresses listed in
|
||
`gstack.redact.allowEmail`, or author and committer addresses (mailmapped too)
|
||
from the history of the pushed commits, of the remote tip being replaced, and,
|
||
for a new branch pushed to a configured remote, of that remote's tracking refs.
|
||
An address only another remote knows still reports. If the hook also printed
|
||
`existing-email suppression was limited for this push`, it could not read the
|
||
history in 5 seconds, so known authors may be listed too.
|
||
|
||
**Fix.** Remove the address, or allow it for this repo:
|
||
`git config --add gstack.redact.allowEmail <address>`. If it is your own
|
||
address, check `git config user.email`. The allowlist only covers `pii.email`;
|
||
it never lets a HIGH finding through.
|
||
|
||
## Unattended runs and artifacts
|
||
|
||
Workflow commands (`gstack-artifact`, `gstack-gate`, `gstack-autoplan-timing`,
|
||
`gstack-review-log --findings`) end each error line with a code in
|
||
parentheses, for example `(ARTIFACT_STALE)`. The codes and anchors come from
|
||
`lib/result-codes.ts`; grep the code, not the prose. Every such command shares
|
||
one exit table: `0` ok, `1` fail, `2` usage, `3` refused or needs a flag. The
|
||
artifact contract itself is in [docs/unattended.md](unattended.md).
|
||
|
||
<a id="artifact-invalid-json"></a>
|
||
### `ARTIFACT_INVALID_JSON`
|
||
|
||
**Meaning.** `run.json`, `timing.json` or another JSON artifact did not parse.
|
||
|
||
**Fix.** Regenerate the run with the tool that writes it. A manifest is never
|
||
hand-edited; a partial write means the writer was interrupted.
|
||
|
||
<a id="artifact-unsupported-version"></a>
|
||
### `ARTIFACT_UNSUPPORTED_VERSION`
|
||
|
||
**Meaning.** The artifact's `schema_version` is one this gstack does not read
|
||
(`gstack-artifact schema <name>` prints the versions it validates).
|
||
|
||
**Fix.** Upgrade the consumer (`/gstack-upgrade`) or regenerate the run with
|
||
this gstack.
|
||
|
||
<a id="artifact-schema"></a>
|
||
### `ARTIFACT_SCHEMA`
|
||
|
||
**Meaning.** A required field is missing, has the wrong type, or holds a value
|
||
outside its enum. The message names the file, row and field.
|
||
|
||
**Fix.** Compare the row with `gstack-artifact schema <name>` and regenerate it.
|
||
|
||
<a id="artifact-missing"></a>
|
||
### `ARTIFACT_MISSING`
|
||
|
||
**Meaning.** `run.json` names an artifact that is not in the run directory.
|
||
|
||
**Fix.** Copy the whole run directory, not single files. If the file was never
|
||
written, rerun the phase that writes it.
|
||
|
||
<a id="artifact-stale"></a>
|
||
### `ARTIFACT_STALE`
|
||
|
||
**Meaning.** The file's bytes no longer hash to the value `run.json` recorded,
|
||
so the manifest does not describe this file.
|
||
|
||
**Fix.** Treat the run as partially rewritten. Regenerate it, or re-bind the
|
||
file through the tool that wrote it so the manifest is updated with it.
|
||
|
||
<a id="artifact-path-escape"></a>
|
||
### `ARTIFACT_PATH_ESCAPE`
|
||
|
||
**Meaning.** An artifact path resolves outside the run directory: a `..`
|
||
segment, an absolute path, or a symlink that points elsewhere.
|
||
|
||
**Fix.** Keep every artifact inside the run directory under a plain relative
|
||
name. A run directory is copied as one unit; nothing outside it is part of
|
||
the contract.
|
||
|
||
<a id="artifact-malformed-jsonl"></a>
|
||
### `ARTIFACT_MALFORMED_JSONL`
|
||
|
||
**Meaning.** A JSONL artifact has a line, usually the last, that is not one
|
||
JSON object. The writer was interrupted mid-append.
|
||
|
||
**Fix.** Rerun the phase that writes the file. Do not trim the line by hand;
|
||
the row it held is lost either way, and the manifest hash would change.
|
||
|
||
<a id="artifact-duplicate-id"></a>
|
||
### `ARTIFACT_DUPLICATE_ID`
|
||
|
||
**Meaning.** Two rows in one file share an id.
|
||
|
||
**Fix.** Ids are run-bound (`<run>-...`) and unique per file. Regenerate the
|
||
rows with the tool that assigns them.
|
||
|
||
<a id="artifact-unbound-id"></a>
|
||
### `ARTIFACT_UNBOUND_ID`
|
||
|
||
**Meaning.** A row id does not start with the run id in `run.json`, so it
|
||
could belong to another run.
|
||
|
||
**Fix.** Regenerate the row with this run's id.
|
||
|
||
<a id="artifact-dangling-ref"></a>
|
||
### `ARTIFACT_DANGLING_REF`
|
||
|
||
**Meaning.** A task's `findings`, `blocked_by` or `depends_on`, or a decision's
|
||
reference, names an id no artifact in this run defines.
|
||
|
||
**Fix.** Write the referenced finding, decision or task first, or drop the
|
||
reference. A `depends_on` entry may also name a `pr` label that another task
|
||
in the file carries.
|
||
|
||
<a id="artifact-dependency-cycle"></a>
|
||
### `ARTIFACT_DEPENDENCY_CYCLE`
|
||
|
||
**Meaning.** Tasks depend on each other in a cycle, so no lane could start.
|
||
|
||
**Fix.** Break the cycle in `depends_on`. The message lists the ids on it.
|
||
|
||
<a id="artifact-count-mismatch"></a>
|
||
### `ARTIFACT_COUNT_MISMATCH`
|
||
|
||
**Meaning.** A `counts` value in `run.json` disagrees with the rows in the file
|
||
it summarizes (for example `counts.findings: 65` beside 64 rows).
|
||
|
||
**Fix.** Regenerate `run.json` after the last row was written. A count never
|
||
replaces the file; the hash and the rows are the record.
|
||
|
||
<a id="artifact-run-interrupted"></a>
|
||
### `ARTIFACT_RUN_INTERRUPTED`
|
||
|
||
**Meaning.** `run.json.status` is not terminal, or a voice is still
|
||
`running`/`pending`. The artifacts describe an unfinished run.
|
||
|
||
**Fix.** Resume it (`gstack-autoplan resume --out <dir>`, PR B-runner) or
|
||
consume the artifacts as partial and say so.
|
||
|
||
<a id="artifact-reviewer-missing"></a>
|
||
### `ARTIFACT_REVIEWER_MISSING`
|
||
|
||
**Meaning.** A phase in `required_phases` is absent from `phases`, a voice has
|
||
no terminal status, or a completed voice has no bound output file (path and
|
||
hash) in `artifacts`.
|
||
|
||
**Fix.** Finish the phase. A missing voice is missing coverage; the validator
|
||
never fills it in as N/A.
|
||
|
||
<a id="artifact-guard-line-missing"></a>
|
||
### `ARTIFACT_GUARD_LINE_MISSING`
|
||
|
||
**Meaning.** The run says `session_kind: unattended`, but its
|
||
`review-record.md` does not carry the `GUARD_NOT_INSTALLED` line, so the
|
||
record does not state that publication order was unverified.
|
||
|
||
**Fix.** Start the run through `gstack-skill-start` (it prints the line) and
|
||
keep the line in the review record.
|
||
|
||
<a id="guard-not-installed"></a>
|
||
### `autoplan guard: not enforced by this host; publication order is unverified (GUARD_NOT_INSTALLED)`
|
||
|
||
**Meaning.** The autoplan publication hook only executes inside Claude Code.
|
||
On every other host, and in every `unattended` session, the hook is present
|
||
but never runs, so phase publication order rests on the snapshot hashes
|
||
recorded per phase rather than on the guard.
|
||
|
||
**Fix.** Nothing on this host. The line is printed once per run and belongs
|
||
in the review record; `gstack-artifact validate` requires it for an
|
||
unattended run. On Claude Code a *missing* hook file still denies every
|
||
guarded call (a broken install must not pass as an absent one).
|
||
|
||
<a id="review-status-mismatch"></a>
|
||
### `review-log: status mismatch ... (REVIEW_STATUS_MISMATCH)`
|
||
|
||
**Meaning.** The row handed to `gstack-review-log` claims a `status`,
|
||
`unresolved`, `issues_found` or `critical_gaps` that the `--findings` file
|
||
contradicts (for example `status: clean` beside open findings). The row was
|
||
not written.
|
||
|
||
**Fix.** Fix the findings file, not the claim, and log again. Omit the claimed
|
||
fields and let the row be derived from the file.
|
||
|
||
<a id="gate-rev-stale"></a>
|
||
### `GATE_REV_STALE`
|
||
|
||
**Meaning.** The reply names a `gate_rev` older than the gate list's current
|
||
revision, so it may answer items that changed.
|
||
|
||
**Fix.** Render the current list (`gstack-gate render <file>`), read it, and
|
||
reply with the printed `gate_rev`.
|
||
|
||
<a id="gate-reply-unparsed"></a>
|
||
### `GATE_REPLY_UNPARSED`
|
||
|
||
**Meaning.** Part of the reply matched no gate item or option. Nothing was
|
||
applied; the unparsed tokens are listed.
|
||
|
||
**Fix.** Reply with `all`, with `<id><option>` tokens (`d3b uc1a`), or with
|
||
`all except <tokens>`. A bare `yes` or `no` is ambiguous across multi-option
|
||
items and is never guessed.
|
||
|
||
## Queue-aware ship: policy, restamp, tree receipt, ship receipt
|
||
|
||
`gstack-ship-policy`, `gstack-restamp`, `gstack-tree-receipt`,
|
||
`gstack-ship-receipt` and `gstack-evidence bundle|ancestor` share the exit
|
||
table above and end each error line with a code in parentheses. The policy
|
||
file is described in [docs/ship-policy.md](ship-policy.md); the queue flow in
|
||
[docs/ship-policy.md#stamp-at-merge](ship-policy.md#stamp-at-merge).
|
||
|
||
<a id="policy-source-unavailable"></a>
|
||
### `policy source unavailable — fix: git fetch origin <base> --depth=1 (POLICY_SOURCE_UNAVAILABLE)`
|
||
|
||
**Meaning.** The policy is read from the committed copy on `origin/<base>`,
|
||
and that ref does not resolve here (a shallow or detached gate checkout, or a
|
||
fetch that never happened). The tool exits 3 and never falls back to the
|
||
working-tree file silently.
|
||
|
||
**Fix.** `git fetch origin <base> --depth=1`, then rerun. To run against the
|
||
branch's own file deliberately, pass `--policy-from head --allow-repo-commands`;
|
||
the receipt then says `policy: head (unreviewed)`.
|
||
|
||
<a id="policy-invalid"></a>
|
||
### `POLICY_INVALID`
|
||
|
||
**Meaning.** `.gstack/ship-policy.json` has an unknown key, a wrong type or a
|
||
value outside its enum. The message names the key. Unknown keys are errors so
|
||
a typo never silently means "default".
|
||
|
||
**Fix.** `gstack-ship-policy validate` lists every problem;
|
||
`gstack-ship-policy init --explain` prints each key, its meaning and default.
|
||
|
||
<a id="policy-containment"></a>
|
||
### `POLICY_CONTAINMENT`
|
||
|
||
**Meaning.** A repo-controlled input escapes its box: an absolute path or a
|
||
`..` segment in `stamp_paths`, `release_outputs`, `changelog` or `mirror`; a
|
||
glob whose expansion passes 512 regex characters; more than 64 entries in a
|
||
list; a `release_tool` over 512 characters or holding a newline. At write time
|
||
the same code names a matched file that resolves through a symlink to outside
|
||
the repository.
|
||
|
||
**Fix.** Keep paths repo-relative, globs short and lists small. A mirror may
|
||
start with `..` (a sibling checkout) but may not contain an interior `..`.
|
||
|
||
<a id="policy-exists"></a>
|
||
### `POLICY_EXISTS`
|
||
|
||
**Meaning.** `gstack-ship-policy init` found an existing policy file and wrote
|
||
nothing.
|
||
|
||
**Fix.** Edit the file, or pass `--force` to replace it with the example.
|
||
|
||
<a id="repo-commands-not-allowed"></a>
|
||
### `repo commands not executed (pass --allow-repo-commands) (REPO_COMMANDS_NOT_ALLOWED)`
|
||
|
||
**Meaning.** The policy declares a command (`release_tool`), and the tool was
|
||
run standalone. File presence never authorizes execution: outside `/ship` and
|
||
`/land-and-deploy` a repo-declared command runs only with
|
||
`--allow-repo-commands`. The commands were listed and nothing was written.
|
||
|
||
**Fix.** Pass `--allow-repo-commands` when you mean to execute the repo's
|
||
code (you already do when you run its tests), or drop the release tool.
|
||
|
||
<a id="queue-stale"></a>
|
||
### `QUEUE_STALE`
|
||
|
||
**Meaning.** `gstack-restamp` was told the base SHA (`--expect-base`) or the
|
||
merge order (`--expect-order`) the allocation assumed, and one of them moved:
|
||
the base advanced, a predecessor is not merged, or a successor merged first.
|
||
The JSON result carries `expected`, `actual` and `recompute`.
|
||
|
||
**Fix.** Run the printed `recompute` command, which recomputes the version
|
||
from the current base, then merge with the server-side head condition
|
||
(`MERGE_CONDITION: sha=<head>`).
|
||
|
||
<a id="restamp-version-source"></a>
|
||
### `RESTAMP_VERSION_SOURCE`
|
||
|
||
**Meaning.** The configured version source (`VERSION`, `.gstack/version-path`)
|
||
is absent, ambiguous or broken at the base revision or on the branch, so there
|
||
is no version to bump from.
|
||
|
||
**Fix.** Create `VERSION` or pin the manifest path in `.gstack/version-path`
|
||
on the base branch. gstack never substitutes `0.0.0.0`.
|
||
|
||
<a id="restamp-merge-conflict"></a>
|
||
### `RESTAMP_MERGE_CONFLICT`
|
||
|
||
**Meaning.** `--after <pr>` builds the merge of the branch and the
|
||
predecessor's head in a throwaway worktree, and that merge conflicts. The
|
||
branch is untouched.
|
||
|
||
**Fix.** Merge the predecessor (or the base once it merges) into the branch
|
||
under `/ship` Step 3's conflict rules, then run `--after` again.
|
||
|
||
<a id="restamp-release-tool-failed"></a>
|
||
### `RESTAMP_RELEASE_TOOL_FAILED`
|
||
|
||
**Meaning.** The policy's `release_tool` exited non-zero inside the staging
|
||
worktree. The branch was not written; the journal records the stage.
|
||
|
||
**Fix.** Run the tool by hand in the repo, fix it, then rerun `gstack-restamp`.
|
||
|
||
<a id="restamp-output-outside-allowed"></a>
|
||
### `RESTAMP_OUTPUT_OUTSIDE_ALLOWED`
|
||
|
||
**Meaning.** The release tool changed a path outside its allowed-output set
|
||
(the version files, lockfiles, the CHANGELOG, `stamp_paths` and
|
||
`release_outputs`). The restamp refused before writing anything to the branch.
|
||
|
||
**Fix.** Add the path to `release_outputs` on the base branch when it is a
|
||
legitimate generated output, or stop the tool from writing it.
|
||
|
||
<a id="restamp-conflict"></a>
|
||
### `RESTAMP_CONFLICT`
|
||
|
||
**Meaning.** While applying or rolling back, a file no longer held the bytes
|
||
the journal recorded (someone edited it after the stage ran). Overwriting it
|
||
would lose that edit, so the restamp stopped.
|
||
|
||
**Fix.** Read `.gstack/tmp/restamp-journal.json`, reconcile the named file by
|
||
hand, then rerun `gstack-restamp` (idempotent: a complete stamp is a no-op).
|
||
|
||
<a id="restamp-predecessor-moved"></a>
|
||
### `PREDECESSOR MOVED <old> -> <new> — fix: gstack-restamp --after <pr> again (RESTAMP_PREDECESSOR_MOVED)`
|
||
|
||
**Meaning.** The gate ran on a synthetic tree built against a predecessor
|
||
head that is no longer that PR's head, so the gated tree is not the tree that
|
||
will merge. The receipt refuses `gate-reuse: eligible`.
|
||
|
||
**Fix.** `gstack-restamp --after <pr>` again, gate the new synthetic tree, and
|
||
read the receipt again.
|
||
|
||
<a id="changelog-duplicate-heading"></a>
|
||
### `CHANGELOG_DUPLICATE_HEADING`
|
||
|
||
**Meaning.** Two sections share one version heading, usually a stamp that
|
||
re-headed `[Unreleased]` beside an entry that already carried the version.
|
||
|
||
**Fix.** Merge them into one entry under the new version.
|
||
|
||
<a id="changelog-top-mismatch"></a>
|
||
### `CHANGELOG_TOP_MISMATCH`
|
||
|
||
**Meaning.** After the stamp, the first entry's heading is not the version in
|
||
`VERSION`.
|
||
|
||
**Fix.** `gstack-restamp` moves the PR's section to the top; if a hand edit
|
||
undid that, fix the heading or rerun the restamp.
|
||
|
||
<a id="changelog-section-missing"></a>
|
||
### `CHANGELOG_SECTION_MISSING`
|
||
|
||
**Meaning.** The CHANGELOG has neither an `[Unreleased]` section nor one
|
||
headed by the version currently stamped on the branch, so there is nothing to
|
||
re-head.
|
||
|
||
**Fix.** Under stamp-at-merge, write the PR's entry under `## [Unreleased]`;
|
||
the restamp turns it into `## [<version>] - <date>`.
|
||
|
||
<a id="gate-reuse-not-eligible"></a>
|
||
### `gate-reuse: not-eligible (<what changed>) — fix: <rerun command> (GATE_REUSE_NOT_ELIGIBLE)`
|
||
|
||
**Meaning.** Tree identity alone is not gate evidence. The receipt compares
|
||
the gated identity (tree modulo stamps, runtime pins, lockfile hash, CI
|
||
workflow hashes, gate command hash, executed selection, runner image) with the
|
||
current one and names what differs, or the stamp-sensitive gates that must
|
||
rerun on the final tree.
|
||
|
||
**Fix.** Rerun the named lanes with the printed command on the stamped tree,
|
||
or pass `--force-gate <reason>` in `/ship` to run the full gate again.
|
||
|
||
<a id="evidence-identity-unknown"></a>
|
||
### `EVIDENCE_IDENTITY_UNKNOWN`
|
||
|
||
**Meaning.** The bundle (or ledger record) lacks a committed tree id, runtime
|
||
versions, lockfile hashes or the command hash, so no reuse decision can rest on
|
||
it. Imported assertions without verification count as unknown too.
|
||
|
||
**Fix.** Rebuild the bundle on the gate machine after the lanes ran on a
|
||
committed head: `gstack-evidence bundle --out <file> --label <lane>...`.
|
||
|
||
<a id="evidence-bundle-invalid"></a>
|
||
### `EVIDENCE_BUNDLE_INVALID`
|
||
|
||
**Meaning.** The bundle is not valid JSON or not `gstack-evidence-bundle` v1.
|
||
|
||
**Fix.** Rebuild it with `gstack-evidence bundle`; a bundle is never hand-edited.
|
||
|
||
<a id="preregistration-not-ancestor"></a>
|
||
### `PREREGISTRATION_NOT_ANCESTOR`
|
||
|
||
**Meaning.** A `preregistration_shas` entry in the policy is not an ancestor
|
||
of HEAD, so the preregistered eval does not describe this branch.
|
||
|
||
**Fix.** Merge or rebase so the commit is in the branch history, or remove the
|
||
entry on the base branch. `gstack-evidence ancestor <sha>...` prints one line
|
||
per entry; the ship receipt records the result.
|
||
|
||
<a id="history-policy-violation"></a>
|
||
### `HISTORY_POLICY_VIOLATION`
|
||
|
||
**Meaning.** The policy says `history: merge-only` and the plan would squash,
|
||
rebase or otherwise rewrite the branch: `gstack-ship-policy check-history
|
||
--plan squash|rebase` refuses (exit 3) before anything moves, and without
|
||
`--plan` it reports the violation (exit 1) when the pushed branch is no longer
|
||
an ancestor of HEAD.
|
||
|
||
**Fix.** `git merge origin/<base>` and keep every commit; merge the PR with a
|
||
merge commit.
|
||
|
||
<a id="receipt-missing"></a>
|
||
### `RECEIPT_MISSING`
|
||
|
||
**Meaning.** `gstack-ship-receipt read --pr <n>` found no
|
||
```` ```gstack-ship-receipt ```` block in the PR body.
|
||
|
||
**Fix.** `/ship` Step 18 writes it (`gstack-ship-receipt write ... --pr <n>`);
|
||
for an older PR, write it once by hand with the same command.
|
||
|
||
<a id="receipt-invalid"></a>
|
||
### `RECEIPT_INVALID`
|
||
|
||
**Meaning.** The block exists but does not validate against the `ship-receipt`
|
||
schema (`gstack-artifact schema ship-receipt`).
|
||
|
||
**Fix.** Regenerate it with `gstack-ship-receipt write`; never hand-edit the
|
||
block.
|
||
<a id="execution-unknown"></a>
|
||
### `EXECUTION_UNKNOWN`
|
||
|
||
**Meaning.** `gstack-autoplan` recorded a dispatch intent for an attempt (the
|
||
prompt was handed to a reviewer) but no terminal record followed: no `submit`,
|
||
no cancellation. The runner was killed, or the parent never came back. The
|
||
provider may have finished and charged; snapshot hashes prove which input was
|
||
sent, not whether the review completed. The spend ledger keeps the attempt's
|
||
reservation.
|
||
|
||
**Fix.** If the reviewer finished, submit its result with the same attempt id
|
||
(`gstack-autoplan submit --out <dir> --phase <p> --voice <v> --result <file>
|
||
--model <id> --attempt <id>`); a late result is still bound. If it did not,
|
||
redispatch explicitly with `gstack-autoplan next --out <dir> --redispatch
|
||
<attempt>`. `resume` and `next` never redispatch an ambiguous attempt on their own.
|
||
|
||
<a id="run-locked"></a>
|
||
### `RUN_LOCKED`
|
||
|
||
**Meaning.** Another `gstack-autoplan` process holds `<dir>/run.lock` (pid,
|
||
host and start time are in the file). Every mutating command takes the lock
|
||
for its duration, so two writers never interleave.
|
||
|
||
**Fix.** Wait for the owner to exit. If its pid is gone (a killed runner),
|
||
`gstack-autoplan resume --out <dir>` takes ownership and reconciles open
|
||
attempts; a lock whose pid is alive is never stolen.
|
||
|
||
<a id="run-not-initialized"></a>
|
||
### `RUN_NOT_INITIALIZED`
|
||
|
||
**Meaning.** The `--out` directory has no `run.json` written by
|
||
`gstack-autoplan`, so `submit`, `status`, `answer` and `resume` have nothing to
|
||
act on.
|
||
|
||
**Fix.** Start the run with `gstack-autoplan next --plan <file> --out <dir>`;
|
||
the first `next` initializes the run (restore point, active plan, manifest).
|
||
|
||
<a id="phase-not-awaiting"></a>
|
||
### `PHASE_NOT_AWAITING`
|
||
|
||
**Meaning.** `submit` named a phase or voice that is not waiting for a result:
|
||
the phase is already closed, not yet opened, or the voice already has a bound
|
||
result.
|
||
|
||
**Fix.** Run `gstack-autoplan status --out <dir>` and submit for the attempt
|
||
the last `next` printed (`ATTEMPT: ... phase=<p> voice=<v>`).
|
||
|
||
<a id="attempt-mismatch"></a>
|
||
### `ATTEMPT_MISMATCH`
|
||
|
||
**Meaning.** The `--attempt` id is not the open attempt for that phase and
|
||
voice, or that attempt was already submitted. A result is bound exactly once;
|
||
a second submit of the same attempt is refused rather than overwriting the
|
||
first.
|
||
|
||
**Fix.** Pass the attempt id printed by `next` for this voice. To replace a
|
||
bound result, reopen the phase through `answer` (a scope-changing choice) or
|
||
start a new run.
|
||
|
||
<a id="result-receipt-missing"></a>
|
||
### `RESULT_RECEIPT_MISSING`
|
||
|
||
**Meaning.** The result file does not begin with the full-read receipt the
|
||
prompt asked for: `INPUT: <phase> <snapshot sha256>`. Either the reviewer did
|
||
not read the prompt file through EOF, or the result belongs to a different
|
||
snapshot (the plan changed between dispatch and submit).
|
||
|
||
**Fix.** The reviewer's first line must be the INPUT line from its prompt
|
||
file. If the plan changed, run `next` again for a fresh snapshot and
|
||
redispatch; a result for an older snapshot is never bound to the current phase.
|
||
|
||
<a id="result-findings-missing"></a>
|
||
### `RESULT_FINDINGS_MISSING`
|
||
|
||
**Meaning.** The result has no valid canonical findings block. Every reviewer
|
||
returns, beside its prose, one fenced block:
|
||
|
||
````
|
||
```gstack-findings
|
||
{"severity":"High","title":"...","file":"lib/x.ts","line":12,"fix":"..."}
|
||
```
|
||
````
|
||
|
||
one JSON object per line with at least `severity` (`Critical`, `High`,
|
||
`Medium`, `Low`, `Informational`) and `title`. An empty fence means the
|
||
reviewer found nothing; a missing fence or a malformed row means the result
|
||
cannot be reconciled and is refused.
|
||
|
||
**Fix.** Ask the reviewer for the fence (the prompt names it), or add the rows
|
||
from its prose yourself and submit again. Reconciliation runs on these rows,
|
||
not on the prose; a prose-only review has no completeness check.
|
||
|
||
<a id="model-family-conflict"></a>
|
||
### `MODEL_FAMILY_CONFLICT`
|
||
|
||
**Meaning.** The outside voice's model is in the same family as the native
|
||
reviewer (both Anthropic, both OpenAI, ...), or its family cannot be
|
||
determined from the id. Two voices from one family are one voice; the record
|
||
says which was which.
|
||
|
||
**Fix.** Run the outside voice on a model from another family and pass its id
|
||
with `--model`. Families recognized: anthropic, openai, google, xai, deepseek,
|
||
qwen, zai, moonshot, meta, mistral, minimax. An id the table cannot classify
|
||
is refused; name the model fully (`openai/gpt-6-astra`, not `astra`).
|
||
|
||
<a id="spend-cap-exceeded"></a>
|
||
### `SPEND_CAP_EXCEEDED`
|
||
|
||
**Meaning.** `spent + reserved + next_estimate` would exceed `--spend-cap`.
|
||
The ledger (`<dir>/spend.json`) counts unsettled reservations and unknown
|
||
charges at their reservation, so a crashed attempt keeps its cost held until
|
||
it is settled or released.
|
||
|
||
**Fix.** Raise `--spend-cap`, settle attempts whose actual cost is known
|
||
(`submit --usage-usd <n>`), release attempts that never ran (`gstack-autoplan
|
||
release --attempt <id>`), or stop here. Host-dispatched subagent spend is
|
||
`unknown`, never zero.
|
||
|
||
<a id="consensus-missing"></a>
|
||
### `CONSENSUS_MISSING`
|
||
|
||
**Meaning.** `next` for the Eng phase found a closed prior phase without its
|
||
consensus record (`<phase>-consensus.md` plus its reconciliation rows), so the
|
||
Eng prompt would be sent without that phase's summary.
|
||
|
||
**Fix.** `gstack-autoplan resume --out <dir>` re-closes the phase through
|
||
reconciliation. The Eng dispatch is refused until every closed phase has its
|
||
summary; it is never sent partially.
|
||
|
||
<a id="outside-runner-unavailable"></a>
|
||
### `OUTSIDE_RUNNER_UNAVAILABLE`
|
||
|
||
**Meaning.** `gstack-outside-voice run --runner <r>` cannot run here:
|
||
`codex-cli` found no `codex` binary or no login, `api` found neither
|
||
`OPENAI_API_KEY` nor `ANTHROPIC_API_KEY` for the model's family, or
|
||
`host-subagent` was called without `--result <file> --model <id>`. When
|
||
another runner is configured (`--fallback` or `GSTACK_OUTSIDE_RUNNER`), the
|
||
line reads `codex-cli unavailable; using <runner>` and the review proceeds on
|
||
that runner; it is a full outside voice, not a degraded one. With no other
|
||
runner, this is missing coverage.
|
||
|
||
**Fix.** `gstack-codex-login` logs Codex in from `OPENAI_API_KEY`
|
||
(unattended machines); or set the API key and pass `--runner api`; or run the
|
||
review on a cross-family subagent and submit its file with `--runner
|
||
host-subagent --result <file> --model <id>`.
|
||
|
||
<a id="reality-row-missing"></a>
|
||
### `REALITY_ROW_MISSING`
|
||
|
||
**Meaning.** `gstack-plan-reality check --phase <p> <review>` found no
|
||
`REALITY: <row> …` line for a row the review must carry (every tier 1 row:
|
||
`premise-table`, `already-done`, `surface-check`, `binding-decisions`,
|
||
`numbers`, `deferred-asks`, `pr-count`; plus the tier 2 rows whose scope was
|
||
passed with `--scope`), or a tier 1 row was marked `n/a`. The check prints
|
||
`PLAN_REALITY: phase=<p> rows=<n>/<m> missing=<ids> verdict=<v>` and exits
|
||
`1`. In an unattended run the verdict is `incomplete` and the phase does not
|
||
close; interactively it is `repair`: produce the missing rows and re-check, at
|
||
most twice, then warn at the gate with each row still missing.
|
||
|
||
**Fix.** `gstack-plan-reality rows --phase <p> [--scope <csv>]` prints each
|
||
row's check. Work it and end the review with one line per row,
|
||
`REALITY: <row> pass|finding <summary> <file:line>`.
|
||
|
||
<a id="reality-receipt-missing"></a>
|
||
### `REALITY_RECEIPT_MISSING`
|
||
|
||
**Meaning.** A `REALITY:` line names its row and status but carries no
|
||
receipt. A receipt is `path:line`, `path:line-line` or a commit sha; it is what
|
||
makes the row a check rather than a claim.
|
||
|
||
**Fix.** Add where the check was made to the line, for example
|
||
`REALITY: already-done pass 0 hits since 2026-10-09 for bin/gstack-doctor 91bbd9e`
|
||
or `REALITY: surface-check finding bun upgrade --version absent on 1.3.14 bin/gstack-capy-install:52`.
|
||
|
||
## Pre-gate: generated-file registry, gstack-pregate, issue links
|
||
|
||
Both tools read their pin file (`.gstack/generated.json`, `.gstack/pregate.json`)
|
||
from `origin/<base>`, never the working tree, and run repo-declared commands only
|
||
under `--allow-repo-commands` or inside `/ship`. Details: docs/pregate.md.
|
||
|
||
<a id="regen-registry-invalid"></a>
|
||
### `REGEN_REGISTRY_INVALID`
|
||
|
||
**Meaning.** `.gstack/generated.json` is not valid JSON, has an unknown key, a
|
||
missing `id`/`inputs`/`outputs`, or a duplicate id.
|
||
|
||
**Fix.** `gstack-regen validate` prints every error with its JSON path; compare
|
||
with `gstack-regen init --explain` and fix the file on the base branch.
|
||
|
||
<a id="regen-containment"></a>
|
||
### `REGEN_CONTAINMENT`
|
||
|
||
**Meaning.** A registry glob escapes the repository (`..`, absolute, more than 64
|
||
per list) or a command is not one line under 512 bytes.
|
||
|
||
**Fix.** Repo-relative globs only; one line per command.
|
||
|
||
<a id="regen-stale"></a>
|
||
### `REGEN_STALE`
|
||
|
||
**Meaning.** A registered output differs from what its command produces in a
|
||
scratch `git worktree` of the current tree: the output is stale or hand-edited.
|
||
|
||
**Fix.** Run the printed command (`gstack-regen write --only <id>` does it in
|
||
place) and commit the outputs. Never hand-merge a generated file: on a conflict
|
||
take either side, then regenerate.
|
||
|
||
<a id="regen-command-failed"></a>
|
||
### `REGEN_COMMAND_FAILED`
|
||
|
||
**Meaning.** A registered command exited non-zero in the scratch worktree.
|
||
|
||
**Fix.** Run it by hand in the repository and read its output; the scratch
|
||
worktree has no `node_modules` of its own, so a command that needs them must
|
||
reference the repository's (gstack's entries do through `bun run`).
|
||
|
||
<a id="pregate-config-invalid"></a>
|
||
### `PREGATE_CONFIG_INVALID`
|
||
|
||
**Meaning.** `.gstack/pregate.json` has an unknown key, a wrong type, a glob
|
||
that escapes the repository, a runner without `{files}`, or `secrets` in `warn`.
|
||
|
||
**Fix.** `gstack-pregate validate`. `secrets` and required-lane obligations
|
||
cannot be downgraded; every other check id in `warn` is honoured only from the
|
||
base branch's copy.
|
||
|
||
<a id="pregate-secrets"></a>
|
||
### `PREGATE_SECRETS`
|
||
|
||
**Meaning.** `gstack-redact` found a HIGH finding, or a MEDIUM finding in the
|
||
`secret` category, in the lines this branch adds.
|
||
|
||
**Fix.** Rotate the credential, remove it from the added lines (history
|
||
included), rerun. Not downgradable.
|
||
|
||
<a id="pregate-strays"></a>
|
||
### `PREGATE_STRAYS`
|
||
|
||
**Meaning.** A file was added at the repository root, in a new top-level
|
||
directory, or under a scratch name (`zz-*`, `tmp*`, `scratch*`).
|
||
|
||
**Fix.** Delete or move it. An intentional root file goes in `strays.allow`.
|
||
|
||
<a id="pregate-literals"></a>
|
||
### `PREGATE_LITERALS`
|
||
|
||
**Meaning.** A changed exported constant (or a named timeout/retry literal) still
|
||
appears with its old value in a test file the branch did not update — nightly-
|
||
and Heavy-only files included.
|
||
|
||
**Fix.** Update every listed `file:line`, or keep the old value.
|
||
|
||
<a id="pregate-no-lane"></a>
|
||
### `PREGATE_NO_LANE`
|
||
|
||
**Meaning.** A touched file maps to no test through static imports, repo-path
|
||
string literals, the `bin/<name>` → `test/<name>*.test.ts` convention or a
|
||
`dependencies` declaration. That is a failure, not a pass.
|
||
|
||
**Fix.** Add a test that names the file, or declare its lane (an explicit `[]`
|
||
for files with no behaviour, such as docs) in `.gstack/pregate.json`. `--explain`
|
||
prints every mapping and why.
|
||
|
||
<a id="pregate-lane-failed"></a>
|
||
### `PREGATE_LANE_FAILED`
|
||
|
||
**Meaning.** A local lane the touched files select exited non-zero under
|
||
`set -o pipefail`, or reported zero tests (ZERO-RUN).
|
||
|
||
**Fix.** Read the printed lane log, fix the failure, rerun
|
||
`gstack-pregate --stage tests`. A `| tail`-hidden exit is still an exit.
|
||
|
||
<a id="pregate-incomplete"></a>
|
||
### `PREGATE_INCOMPLETE`
|
||
|
||
**Meaning.** A check timed out, its tool is unavailable, repo commands were not
|
||
allowed, or the working tree changed while the check ran. Incomplete is never
|
||
pass and blocks publication.
|
||
|
||
**Fix.** Pass `--allow-repo-commands`, install the tool, raise `--timeout`, or
|
||
keep the tree still, then rerun.
|
||
|
||
<a id="pregate-stale"></a>
|
||
### `PREGATE_STALE`
|
||
|
||
**Meaning.** `pregate.json` was recorded against a different working-tree
|
||
fingerprint than the one being published (the final stamp, a regen, any edit).
|
||
|
||
**Fix.** Rerun `gstack-pregate` after the final generation and stamp; Step 18
|
||
refuses to write the ship receipt otherwise.
|
||
|
||
<a id="pregate-remote-uncleared"></a>
|
||
### `PREGATE_REMOTE_UNCLEARED`
|
||
|
||
**Meaning.** A `requires-remote` obligation (a platform lane this machine cannot
|
||
run, named by `workflow.yml/job`) has no receipt.
|
||
|
||
**Fix.** Dispatch that job, then
|
||
`gstack-pregate clear --remote <workflow.yml/job> --run-url <url>` (or
|
||
`--bundle-lane <label>` from an evidence bundle). The ship receipt carries each
|
||
obligation with its receipt.
|
||
|
||
<a id="issue-unreadable"></a>
|
||
### `ISSUE_UNREADABLE`
|
||
|
||
**Meaning.** An issue named by a `Fixes #N`/`Closes #N` trailer or `--issue N`
|
||
could not be read through `gstack-issue-guard`.
|
||
|
||
**Fix.** Check `gh auth status` and the number. Until it reads, the link is
|
||
rendered as `Refs #N`, never `Fixes`.
|
||
|
||
## Eval plans, lane ownership, evidence verification, the owner brief
|
||
|
||
Codes printed by `bin/gstack-eval-plan`, `bin/gstack-lane-check`,
|
||
`gstack-evidence verify`, `bin/gstack-owner-brief`, `bin/gstack-contributor-mode`
|
||
and `bin/gstack-banned-terms` (plan E1–E3). Each ends an error line in
|
||
parentheses; grep the code.
|
||
|
||
<a id="prereg-incomplete"></a>
|
||
### `PREREG_INCOMPLETE`
|
||
|
||
**Meaning.** `gstack-eval-plan prereg --check <file>` found a required
|
||
preregistration section missing or empty: `## Bars`, `## Arms`, `## Decoys`,
|
||
`## Stop rules` or `## Held-out exposure` (every held-out set the plan
|
||
touches, its prior exposure and the reservation on it).
|
||
|
||
**Fix.** Fill the named sections before any priced call. A plan that cannot
|
||
name its stop rule or its held-out exposure is not preregistered; the pilot
|
||
refuses to start without a passing check.
|
||
|
||
<a id="eval-price-missing"></a>
|
||
### `EVAL_PRICE_MISSING`
|
||
|
||
**Meaning.** An arm's model has no row in `lib/pricing.ts`, so the admission
|
||
estimate would be `$0`, which the ledger would admit without limit.
|
||
|
||
**Fix.** Add the row from the provider's pricing page (USD per million tokens
|
||
with `as_of`), or drop the arm. The table prices standard text tokens only;
|
||
see the excluded billing categories in `lib/pricing.ts`.
|
||
|
||
<a id="eval-dry-run-failed"></a>
|
||
### `EVAL_DRY_RUN_FAILED`
|
||
|
||
**Meaning.** The `$0` stub-model dry run over the real input file found a
|
||
problem the priced run would have paid for: a row without the fields the
|
||
prompt needs, an output budget too small for a reasoning model, or no retry
|
||
prompt text.
|
||
|
||
**Fix.** Repair the named rows or flags (`--output-budget`, `--retry-prompt`)
|
||
and rerun `gstack-eval-plan dry-run`. The checklist it prints is the record
|
||
the pilot requires.
|
||
|
||
<a id="eval-arms-unresolved"></a>
|
||
### `EVAL_ARMS_UNRESOLVED`
|
||
|
||
**Meaning.** `gstack-models resolve --role eval-arms` produced no arm and the
|
||
repo policy document fallback (`--policy-doc`, default the first of
|
||
`docs/model-policy.md`, `MODEL_POLICY.md`, `docs/MODELS.md`) named no priced
|
||
model.
|
||
|
||
**Fix.** Pass `--arms <model,model>`, point `--policy-doc` at the document
|
||
that lists the project's allowed models, or fix the model policy config.
|
||
|
||
<a id="eval-budget-exhausted"></a>
|
||
### `EVAL_BUDGET_EXHAUSTED`
|
||
|
||
**Meaning.** The ledger refused the next reservation: spent + reserved +
|
||
unknown charges + the estimate exceeds the cap (`SPEND_CAP_EXCEEDED`
|
||
underneath). The run stops with status `budget_exhausted`; nothing is retried.
|
||
|
||
**Fix.** Read the spend table (`gstack-eval-plan pilot report`), settle or
|
||
release the open attempts, then raise `--cap` on purpose or stop.
|
||
|
||
<a id="lane-conflict"></a>
|
||
### `LANE_CONFLICT`
|
||
|
||
**Meaning.** `gstack-lane-check` found a planned file that an in-flight
|
||
branch or pull request already changes; the line names the PR number, the
|
||
branch and the hunk count.
|
||
|
||
**Fix.** Give the file one owner: wait for that PR to merge, stack this lane
|
||
on its branch, or drop the file from the plan. Two lanes editing one file is
|
||
the merge-conflict incident the check exists to prevent.
|
||
|
||
<a id="lane-ref-unavailable"></a>
|
||
### `LANE_REF_UNAVAILABLE`
|
||
|
||
**Meaning.** A `--branch` or `--pr` could not be resolved: the ref is not
|
||
fetched, or `gh` is missing or not logged in.
|
||
|
||
**Fix.** `git fetch origin <branch>`, or `gh auth status`. An unresolved lane
|
||
is reported as such and never counted as clear.
|
||
|
||
<a id="evidence-unverified"></a>
|
||
### `EVIDENCE_UNVERIFIED`
|
||
|
||
**Meaning.** `gstack-evidence verify <bundle>` could not verify a lane claim
|
||
on this machine: the committed tree differs from the bundle's, the recorded
|
||
log is absent or its hash differs, or the probe was not rerun.
|
||
|
||
**Fix.** Check out the bundle's commit and run `--rerun` to execute the lane
|
||
command here, or treat the claim as an imported assertion that never counts
|
||
toward gate reuse.
|
||
|
||
<a id="banned-term-found"></a>
|
||
### `BANNED_TERM_FOUND`
|
||
|
||
**Meaning.** The diff adds a term listed in `.gstack/banned-terms.json`
|
||
outside that term's `allowed_paths`. The finding names the term, the file and
|
||
line, and the allowed locations.
|
||
|
||
**Fix.** Remove the term from the added line or move the text to an allowed
|
||
path. `/review` and `/document-release` run this check on every diff.
|
||
|
||
<a id="banned-terms-invalid"></a>
|
||
### `BANNED_TERMS_INVALID`
|
||
|
||
**Meaning.** `.gstack/banned-terms.json` is not valid: `terms` must be a
|
||
non-empty list of strings, and each `allowed_paths` entry must be a plain
|
||
repo-relative glob (no `..`, no absolute path, no symlink escape).
|
||
|
||
**Fix.** `gstack-banned-terms init` writes a commented example with every key
|
||
at its default; `gstack-banned-terms validate` checks the file.
|
||
|
||
<a id="contributor-source-unavailable"></a>
|
||
### `CONTRIBUTOR_SOURCE_UNAVAILABLE`
|
||
|
||
**Meaning.** `gstack-contributor-mode detect` found no `maintainers` list in
|
||
`.gstack/ship-policy.json` and could not ask the collaborators API (`gh`
|
||
missing, not logged in, or no PR author resolvable), so the mode is off and
|
||
`source=off` is recorded.
|
||
|
||
**Fix.** Add `maintainers` to the ship policy (the explicit list always wins),
|
||
run `gh auth login`, or pass `--contributor` to force the mode for this review.
|
||
|
||
<a id="brief-input-missing"></a>
|
||
### `BRIEF_INPUT_MISSING`
|
||
|
||
**Meaning.** `gstack-owner-brief render --out <dir>` needs `gate.json`,
|
||
`decisions.jsonl` and `findings.jsonl` in the run directory; one is missing.
|
||
|
||
**Fix.** Reach the gate first (`gstack-autoplan next --out <dir>` until
|
||
`gate_pending`); the brief renders what the gate wrote.
|
||
|
||
<a id="draft-direction-unresolved"></a>
|
||
### `DRAFT_DIRECTION_UNRESOLVED`
|
||
|
||
**Meaning.** The phase-close rewrite found a `draft direction stands until the
|
||
owner decides` line whose decision id is still pending, so it was left as is.
|
||
|
||
**Fix.** Answer the gate for that id, then rerun `gstack-owner-brief close`;
|
||
only decided lines are rewritten, each to its chosen option and the date.
|
||
|
||
<a id="setup-crlf-repair"></a>
|
||
### `warning: left <n> locally edited file(s) with CRLF line endings that gstack pins to LF`
|
||
|
||
**Meaning.** Git for Windows checks out with `core.autocrlf=true`, so gstack's
|
||
nested shell scripts (skill `bin/` helpers, the Claude hook shims,
|
||
`lib/cso/images/*`, `scripts/app/*`) were written with CRLF until
|
||
`.gitattributes` pinned them to LF (#3110). Git Bash runs them anyway, but a
|
||
Linux bash reading the same checkout (a synced tree, WSL, a bind mount) fails
|
||
with `set: pipefail: invalid option name`, and a hook that fails that way
|
||
blocks the tool call it guards. Pulling the new rule does not fix existing
|
||
copies, because git never rewrites a file whose content did not change. Setup
|
||
finds those files with one `git ls-files --eol` pass and checks them out again
|
||
as LF. It leaves a file you edited locally alone and names it in this warning.
|
||
The same section covers `skipped the CRLF repair` (another setup held the
|
||
install registry lock) and `the CRLF repair could not rewrite every file`.
|
||
|
||
**Fix.** For edited files, stash and restore them; git writes them back with LF
|
||
and keeps your edits:
|
||
|
||
```bash
|
||
cd ~/.claude/skills/gstack && git stash && git stash pop
|
||
```
|
||
|
||
If the repair was skipped, re-run `./setup` once the other setup finishes. If
|
||
it failed, `git checkout -- . && ./setup` in the checkout discards local edits
|
||
and rewrites every file. Use your install directory if it is not
|
||
`~/.claude/skills/gstack` (`./setup --status` prints it). Expected result: the
|
||
next `./setup` prints no CRLF warning.
|