* fix(capy): the launch environment is sourced from a 0600 file and no skill fence may print the environment (GSTA-104) During GSTA-96's unattended /ship on #3425, the Step 14.5 docs-audit child re-typed the GSTACK_LAUNCH assignments as `export $(...)`; the substitution came back empty, the bare `export` printed the whole environment, and three API key values landed in its tool output. - bin/gstack-capy-install writes the assignments to <verdict>.env (mode 0600, idempotent PATH prepend) and prints GSTACK_LAUNCH_FILE: <path>. - ship Step 14.5 prompt, document-release's spawned note, autoplan/DRIVER.md and gstack-skill-start's capy-asks block say: source the file; never export $(...), env, printenv or bare set in a tool fence. - test/launch-env-echo-ratchet.test.ts greps every generated SKILL.md, sections, DRIVER.md, the Capy skill and bin/ shell helpers, with negative controls under test/fixtures/launch-env-echo/. - lib/redact-engine.ts: env.value (HIGH) finds the value of any *_KEY, *_TOKEN, *_SECRET, *_PASSWORD variable in the writer's environment, so a leaked value cannot reach a PR body, receipt, report or Drive file. - scripts/lib/shard-engine.ts scrubs the Capy launch markers from shard environments (GSTA-99's finding). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(ci): DRIVER.md back to 159 lines; shard-engine recorded non-behavioral at HARNESS_VERSION 6; verdict-count test lists .json only Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: register test/fixtures/launch-env-echo as a free fixture Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(redact): env-value fixtures stay out of pregate's env.kv net A credential-named assignment with an entropic literal on one line is what the repo's own pregate secrets scan refuses in added lines, so the fixture values move into plainly named constants and the scanned text is built from them. The scan under test still sees the same values through processEnv. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(windows): the late-completion remote-caller case gives its second attempt a 2s budget (run 40263984171) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(redact): env.value trims the value, keeps a prefix value's longer match, hides the preview and skips PUBLIC names /ship Step 9.4 review fixes for the GSTA-104 env-value scan: - a value whose environment copy ends in a newline (a key read from a file) is searched for trimmed, so its leaked form still matches (the length check already trimmed; the search used the raw value); - when one credential value is a prefix of another, the dedup key carries the span length, so the longer value is redacted whole in either order instead of leaving its tail in the text and dropping its finding; - the env.value preview is a fixed mask instead of the first characters of a live credential; - framework-public names (NEXT_PUBLIC_*_KEY, *_PUBLIC_KEY) are not scanned, since those values are committed on purpose; - the header and the processEnv docstring state the name rule (KEY, TOKEN, SECRET, PASSWORD, PASSWD, any case) and that process.env is the engine's one ambient input. Step 7 coverage extensions and a regression test for each fix in test/redact-env-value.test.ts; docs/capy-install.md names the full rule. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * refactor(capy-install): write_launch_file quotes through shq; header names the launch file Review finding: _sq duplicated shq() from the same file. The launch file's assignments now go through shq, and the header comment describes the GSTACK_LAUNCH_PATH and GSTACK_LAUNCH_FILE lines beside GSTACK_LAUNCH. Step 7 coverage: the verdict test sources the launch file and runs --check; the single-quote path test sources the file too; a new case makes the launch-file directory unwritable and checks the run says so, prints no GSTACK_LAUNCH_FILE line, leaves no temp file and keeps its exit code and trailer (linux, non-root). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(preload): strip the Capy launch markers so a direct bun test matches the shard sandbox Red Team finding: the launch-marker scrub lived only in createShardSandbox, so a direct `bun test <file>` run under an unattended Capy session inherited GSTACK_SESSION_KIND, GSTACK_CAPY, GSTACK_EPHEMERAL and GSTACK_INSTALL_VERDICT (GSTA-99's symptom). The preload now deletes the same four names, and test/shard-engine.test.ts imports CAPY_LAUNCH_MARKERS instead of re-typing them. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(ratchet): the launch-env echo scan catches backtick and $VAR exports, typeset -p, env flags and a set in any segment Review findings 11 and 12: export-subst also trips on `export \`...\`` and an unquoted `export $VAR` (both splat a NAME=value file; `export "$name"` is one word and stays allowed); the export rule adds `typeset -p`; env trips with only flags (`env -0`) and as `/usr/bin/env`; set is judged per segment and with redirections, allowed only when piped straight into an anchored grep filter. Quoted strings are blanked offset-preserving so a `#` inside quotes no longer hides the rest of the line. The negative control covers each new form on both sides. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(ship): Step 14.5 names where the launch-file placeholder comes from The /document-release prompt line read `. <file>` with no word on what fills it. It now says the placeholder is the GSTACK_LAUNCH_FILE: path the parent's own install check printed. Worded to stay under the ship skill's pinned parity size (313,279 of 313,281 union bytes). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(redact): env.value admits *_PASS, *_PAT, *_APIKEY and *_PWD, and skips path values and gstack's lock token /ship Step 11 adversarial findings. The name rule also covers DB_PASS, GITHUB_PAT, *_APIKEY and *_PWD (the shell's own PWD and OLDPWD stay out). A credential-named variable whose value is a filesystem path (SSL_KEY=/etc/ssl/ private/x.key) names where the credential lives, so the path is no finding, and GSTACK_BRAIN_LOCK_TOKEN (pid-random-epoch, written to the lock owner file) is not a secret. Without these, an ordinary mention of that path or token in a PR body, receipt or CSO report was a HIGH fail-closed blocker. The adversarial pass also recommended an entropy floor so dev defaults such as DB_PASSWORD=postgres stop matching prose; declined here on purpose, because the scan's promise is to catch a leaked value whatever its shape, and a weak real password is still a credential. Recorded as an open decision in the ship report. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(capy-install): a failed launch-file write removes only its own temp file The failure path ran rm -f "$LAUNCH_FILE".tmp.*, which also deleted the temp file a concurrent installer run (two threads on one machine, same verdict path) was about to mv, so that run lost its launch file too. write_launch_file now cleans up its own $tmp and returns 1; the caller only reports. The file's comment no longer embeds the state-root path (a newline in GSTACK_STATE_ROOT could have ended the comment). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: the launch markers come from one list, and the echo ratchet scans setup and scripts/*.sh The preload imports CAPY_LAUNCH_MARKERS instead of re-typing the four names, and the ratchet asserts the installer exports every name on that list, so a fifth marker cannot be added to one place only. The shell scan also covers ./setup and the scripts/*.sh install scripts callers run first. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(changelog): the Unreleased entry states the review-fixed env.value rules, the ratchet forms and the one test-budget change Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(redact): the env.value path exemption asks whether the file exists instead of matching a shape Second review cycle: a path-shaped rule also matched a base64 secret that starts with "/" and holds another "/" (about 1 in 150 AWS-shaped keys), which then went unscanned. The exemption now fires only when the value names a file that exists on the scanning machine (SSL_KEY=/etc/ssl/private/x.key). The module header names that stat as the engine's one filesystem read. NON_SECRET_ENV_NAMES is exported so the test can prove bin/gstack-brain-lock.sh still exports every name in it; the PWD/OLDPWD fixtures hold plain words so only the name rule keeps them out. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: the installer's own-temp-file cleanup is exercised, and the ratchet coverage test names setup and scripts An mv shim first on PATH refuses only the launch-file target, so the temp file exists when the move fails: the run removes that file alone (a sibling from a concurrent run survives), says so, prints no GSTACK_LAUNCH_FILE line and keeps the earlier file, exit code and trailer. The ratchet coverage test asserts setup, scripts/build.sh and scripts/sandbox-doctor.sh are scanned. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(redact): env.value has no path exemption and matches the value in normalized form Second adversarial pass: the existence check let the scanned agent exempt any "/"- or "~/"-prefixed secret by creating a path of that name, a silent bypass of the one guard this branch adds, so the exemption is gone; a path-valued SSL_KEY that must appear in a text goes through the allowlist. The engine is back to no I/O. The value is now normalized the way the text is before indexOf, so a credential NFKC rewrites (fullwidth digits) still finds its leaked ASCII form. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(capy-install): the launch file is written only on a passing run; the ratchet checks the marker list both ways A gstack: fail run no longer writes or announces a sourceable file whose PATH prepend points at an unverified Bun. The ratchet also asserts every variable the launch file exports is a CAPY_LAUNCH_MARKERS entry or GSTACK_STATE_ROOT, so a sixth export cannot reach fixture children unstripped. The Capy skill's prose no longer offers re-typing the assignments as an alternative to sourcing. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(capy-install): the launch file is part of the ok verdict Third adversarial pass: a passing run whose launch file could not be written still said gstack: ok with exit 0 and printed only the GSTACK_LAUNCH line, leaving a shell-evaluating caller with the re-typing path the branch exists to remove. The file is now written before the verdict is recorded; a write failure on a passing run turns the run into "gstack: fail launch-file — could not write the launch file <path>; fix: … (LAUNCH_FILE_UNWRITABLE)" with exit 1 and an ok:false verdict, and every failing run removes the launch file an earlier passing run left beside the verdict. The two failure tests assert the new contract, the doctor-FAIL tests assert no launch file is announced or left, and the docs, Capy skill and CHANGELOG say the same. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(redact): the env.value allowlist and placeholder rules see the normalized value too An allowlisted value in NFC did not suppress a match on its NFKC-composed form, and a value that is a placeholder only after normalization was still searched for. Fixture nits from the third review cycle: fixed directory string instead of process.cwd(), the fullwidth assertion guards its own vacuity, a zero-width-padded value under the floor is pinned, and the ratchet comment names the one kept export and the PATH case line. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(capy-install): LAUNCH_FILE_UNWRITABLE is a registered result code; a kept profile verdict keeps its launch file Fourth review cycle. The new trailer code now has its row in lib/result-codes-capy.ts (the owner module is at its size cap) and its docs/troubleshooting.md anchor, so the install-side code scan resolves it. A failing profile check (--for review-voice) that keeps a fuller ok verdict for this HEAD no longer deletes that verdict's launch file: the fail branch removes the file only when the run rewrites the verdict, and setup_failed removes it too. The docs say "a failing run that rewrites the verdict", and that an unwritable directory keeps both the old file and the old verdict. A new test runs a passing install, then a failing profile check, and asserts the file and the ok verdict both survive. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(redact): the normalized allowlist and placeholder rules each have an assertion that fails without them Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(capy-install): a setup failure after a passing install removes the earlier launch file Fifth review cycle: setup_failed's cleanup had no assertion. The setup failure test now installs once (launch file present), commits a new HEAD so setup runs again, fails that setup, and asserts the launch file is gone and the verdict says ok:false before the next run sets up again and ends ok. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: state-root, contributor reference and capy-install follow the launch-file contract (Step 14.5 audit) The /document-release audit (docs-3449-1792992068) corrected three authored docs: docs/state-root.md shows the GSTACK_LAUNCH_FILE line and says to apply it with ". <that file>" instead of prefixing or exporting the assignments; docs/CONTRIBUTOR_REFERENCE.md names scan()'s one ambient input instead of calling it pure; docs/capy-install.md's bun-shim bullet and exit table name the launch file and the fail launch-file trailer. 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>
15 KiB
/autoplan driver: run the review loop from a parent agent
Read this file instead of autoplan/SKILL.md when you (a parent agent on any host) drive an
unattended /autoplan run through gstack-autoplan: the reviewers are subagents you dispatch,
and the runner keeps the state. The commands live in <root>/bin/, where <root> is the
folder that holds this autoplan/ directory; gstack-autoplan --help and the first next
print DRIVER: <root>/autoplan/DRIVER.md. Run every command with the GSTACK_LAUNCH assignments your installer printed: . <the GSTACK_LAUNCH_FILE it printed> in the shell that runs the command,
never export $(...), env, printenv or a bare set in a tool fence (a bare export prints every variable, credentials included). Read printed lines and files, never prose: every workflow
command ends with GSTACK_RESULT: skill=autoplan status=<s> run=<dir>, and every error line
ends with (<CODE>).
Phases and scope
Order: ceo → design → dx → eng. Design runs only with UI scope, DX only with DX scope, Eng always and last.
Scope flags on the first next: --ui, --no-ui, --no-dx, --developer-tool, --agent-primary. --developer-tool and --agent-primary only enable DX.
The first next prints SCOPE: ui=… (terms; visual; files) dx=… (detected) source=ui:<s>,dx:<s>
(each axis is flag, marker or detected). UI needs a rendered file path or a UI term next
to a visual noun in the Implementation plan. To keep flags for later runs on the same plan, add
the SCOPE_PIN: marker it prints (<!-- gstack-autoplan-scope: … -->) as the plan's first
line yourself; gstack never writes the plan.
The loop
nextprints oneATTEMPT: <id> phase=<p> voice=native|outside prompt=<file> result=<file>per voice. Dispatch the native prompt to a subagent on your model and the outside prompt to a subagent on another model family (orgstack-outside-voice run). Each writesresult=.- Submit each voice exactly once (
BOUND:). Both voices in closes the phase (PHASE_CLOSED:). - Repeat
nextuntilstatus=gate_pending(GATE: gate_rev=<n>); render it withgstack-gate render <dir>/gate.json --alland relay every page. You never approve the gate for the owner. Offerrec(every recommendation, plan changes included) as the default. answerwith the owner's reply and--by human(the owner typed the tokens) or--by agent:<id> --owner-reply "<their words>"(you relayed them); without--byan unattended run refuses (ANSWER_BY_REQUIRED).APPLY: <id> <title> -> <change>lines are accepted plan changes (statusamend_pending): edit the plan, thenrevise(the printedNEXT:names it), orrevise --settle <id>when the line says no plan edit is needed. Until thennextrefuses withPLAN_AMEND_PENDINGandexportwill not mark the run complete.REOPENED: <phases>means loopnextagain (Eng last).APPROVED:meansexport.
On Capy
- Always drive
gstack-autoplan; never hand-adaptautoplan/SKILL.md. A plan you reviewed by hand is not an autoplan run: do not present it as one. - Every prompt ends with a
GSTACK TRAILERblock: the reviewer runs the--for review-voiceinstall on its own machine (no ./setup, no skills; seconds when warm) and starts its reply with thegstack:line. Never tell a reviewer that no install is needed; relay each reviewer's line with its result, and treat a missing line asgstack: fail not-installed. - Set
GSTACK_AUTOPLAN_CHECKPOINT_DIRto the private project-user Drive folder (or opt every run in withgstack-config set autoplan_checkpoint drive) so a run survives machine replacement: a generation is published at rest, after each bound voice and after each phase transition; restore withresume --out <new dir> --from <dir>/<run>, which never redispatches a bound voice. - Plans carry
## Before → after,## Why it matters,## What caused this work,## Big decisions; relay the printedRELAY: gstack-owner-brief render --captainoutput to your Captain (or the person), thengstack-approvals import --autoplan <dir>afteranswer(capy/skill/gstack/captain.md).
Commands
gstack-autoplan next --out <dir> --plan <plan.md> [scope flags] [--light] [--spend-cap <usd>] [--max-rounds <n>] [--scaffold]first call: init the run, print PHASE:, SCOPE:, DRIVER: and one ATTEMPT: per voice; --max-rounds caps review rounds per phase (default 3 unattended), and an unattended Capy run reserves 5.00 per host attempt unless --estimate-usd says otherwise, so --spend-cap binds.gstack-autoplan next --out <dir> [--redispatch <attempt>]later calls: open the next phase or reprint open attempts; never redispatches on its own.gstack-autoplan submit --out <dir> --phase <p> --voice native|outside --record <file.md> --model <id> [--attempt <id>]bind a free-form review record (canonical fence, one findings table or list, orNo findings.).gstack-autoplan submit ... --record <file.md> --attest "<reason>" --attempt <id>bind a record with no INPUT: line as receipt: attested (a wrong INPUT: always refuses).gstack-autoplan submit --out <dir> --phase <p> --voice native|outside --result <file> --model <id> --attempt <id>bind a canonical result (the RESULT FORMAT below).gstack-autoplan submit --out <dir> --phase <p> --voice outside --unavailable|--failed "<reason>"close the phase native-only when no outside voice can run.gstack-autoplan answer --out <dir> --gate-rev <n> --reply "<reply>" --by human|agent:<id> [--owner-reply <text>|-] [--rec-hash <h>]answer the gate with the reply grammar below; an agent answer stores the owner's words verbatim.gstack-autoplan recommend --out <dir> --gate-rev <n> --item <id> --option <k> --basis "<why>"change one recommendation before relay; then render and relay again (arecneeds the new--rec-hash).gstack-autoplan revise --out <dir> --plan <plan v2> --incorporates <decision ids>bind the amended plan; reopens every phase whose reviewed plan changed, Eng last.gstack-autoplan revise --out <dir> --settle <decision id|d4>discharge a packaging or ordering change with no plan edit; reopens nothing.gstack-autoplan revise --out <dir> --withdraw <decision id>drop a pending amendment instead of applying it.gstack-autoplan close --out <dir> --phase <p> --deferred "<reason>" --by human|agent:<id>close a phase accepted-with-findings: its open findings go to tasks.jsonl, and Critical, urgent or user-challenge findings become gate items; also the recovery fromSTOP: reason=budget_exhausted.gstack-autoplan budget --out <dir> [--spend-cap <usd>] [--max-rounds <n>] --by human|agent:<id>the owner's journaled cap raise; after a budget stop,resumecontinues the run.gstack-autoplan export --out <dir>write the artifact set; thengstack-artifact validate <dir>/run.json.gstack-autoplan export --out <dir> --public <pub>a machine-path-free copy to commit or share (recorded roots rewritten, plan.md code kept, provenance digests); the private run is not touched; thengstack-artifact validate --public <pub> --private <dir>/run.jsonon this machine.gstack-autoplan status|resume --out <dir>read the manifest (PENDING: lines name open attempts); take ownership after a crash (EXECUTION_UNKNOWN lists unfinished attempts).gstack-autoplan resume --out <new dir> --from <checkpoint>restore the newest valid checkpoint generation (GSTACK_AUTOPLAN_CHECKPOINT_DIR) into an empty directory and take ownership;checkpoints --from <dir>lists them.
Reviewer result format
Every prompt ends with this contract; submit --result enforces it, submit --record accepts
the looser record shapes above.
RESULT FORMAT (required by gstack-autoplan; the parent binds your file by this contract):
- The first line of your result is exactly: INPUT:
- Write the complete review as prose, then END with one fenced block of canonical findings, one JSON object per line:
{"id":"F1","severity":"High","title":"<one line>","file":"<path or omit>","line":12,"fix":"<the fix>"}
severity is Critical | High | Medium | Low | Informational. Add "user_challenge": true only when you recommend changing the plan's stated direction AND the change alters its scope, cost or direction (then also add "changes": "scope" | "cost" | "direction") or only its delivery packaging or ordering, with no Implementation-plan edit (then "changes": "packaging" | "ordering"). Naming and other reversible choices are ordinary findings, not user challenges. Add "urgent": true and "suggested_owner": "" for a security or data-loss finding this plan does not own. An empty fence means no findings; a missing fence is refused. 3. Write the whole result to this file:
Gate replies
A reply binds to the gate_rev you read (--gate-rev). Bare yes/no is unparsed, never guessed.
rec: every item takes its recommendation, plan changes included (each prints APPLY:);allis an alias.as-is: keep every direction; no plan change.d3b uc1a: one token per item you set; unanswered auto items take their default.rec except d3b: recommendations, with the listed overrides.
Run directory
Runner state: run.json, attempts.jsonl, run.lock, spend.json, plan-active.md, restore.md, plan-source.md. Reviewer files: <phase>-native-prompt.md,
<phase>-outside-prompt.md, <phase>-<voice>.<attempt>.md (the reviewer writes it), its sealed
<phase>-<voice>.<attempt>.bound.md (what submit bound), <phase>-consensus.{json,md},
gate.json, decisions.jsonl. export writes plan.md, review-record.md, findings.jsonl,
tasks.jsonl, timing.json and rehashes run.json; gstack-artifact validate <dir>/run.json
is the completion check. Copy the whole directory, never single files.
Result codes
RUN_NOT_INITIALIZED: start withgstack-autoplan next --plan <file> --out <dir>(the first call initializes the run)PLAN_SECTIONS_INVALID: add the missing heading, or rerun the firstgstack-autoplan nextwith --scaffold to review a scaffolded copy (your plan file is never edited); duplicate, reversed or empty sections are fixed by handRUN_LOCKED: wait for it to exit, or if its pid is gone rungstack-autoplan resume --out <dir>to take ownershipEXECUTION_UNKNOWN: submit the result file if the reviewer finished (gstack-autoplan submit ... --attempt <id>), or redispatch explicitly withgstack-autoplan next --redispatch <id>; nothing is redispatched automaticallySPEND_CAP_EXCEEDED: raise--spend-cap, settle or release unsettled attempts, or stop; unknown charges count as their reservationPHASE_NOT_AWAITING: rungstack-autoplan status --out <dir>and submit for the attemptnextprintedATTEMPT_MISMATCH: pass the attempt idnextprinted for this voice; a result is bound exactly onceRESULT_RECEIPT_MISSING: the reviewer must read the prompt file through EOF and begin its result with the INPUT line the prompt names; a result for another snapshot is refusedRESULT_FINDINGS_MISSING: end the review with one ```gstack-findings fence, one JSON object per line with at least severity and title;[]rows are not findings, an empty fence means no findingsMODEL_FAMILY_CONFLICT: pass--model <id>of a different family (anthropic, openai, google, xai, deepseek, qwen, zai, moonshot, meta, mistral); a same-family result is not an outside voiceCONSENSUS_MISSING: resume the run so the phase closes through reconciliation; the Eng prompt is never sent without every prior phase summaryGATE_REV_STALE: render the current gate list, read it, and reply with its gate_revGATE_REPLY_UNPARSED: reply withrec,as-is,<id><option>tokens (for exampled3b uc1a), orrec except <tokens>; bare yes/no is ambiguousGATE_REC_STALE: render the gate again (gstack-gate render <dir>/gate.json --all), relay it, and answer with the printed--rec-hashOWNER_REPLY_REQUIRED: pass the owner's words exactly with--owner-reply "<text>",--owner-reply -(stdin) or--owner-reply-file <path>GATE_OWNER_REPLY_MISMATCH: relay exactly what the owner chose (or their reply itself, e.g.--reply rec); ask the owner when their words and the tokens disagreePLAN_AMEND_PENDING: edit the plan, thengstack-autoplan revise --out <dir> --plan <file> --incorporates <decision id,...>; settle a packaging or ordering change withgstack-autoplan revise --out <dir> --settle <id>; or drop one withgstack-autoplan revise --out <dir> --withdraw <decision id>AMENDMENT_NOT_SETTLEABLE: edit the plan, thengstack-autoplan revise --out <dir> --plan <file> --incorporates <decision id>(add--settle <id>to settle packaging/ordering changes in the same call); settle alone only on the plan the run already reviewsCHECKPOINT_INVALID: rungstack-autoplan checkpoints --from <dir>to see each generation's hash status and restore an intact one; nothing was written to --outGSTACK_VERSION_CHANGED: nothing to do for an older run (journaled forward migrations ran); a run written by a newer gstack needs that gstack: upgrade with bin/gstack-upgrade
Coordinator contract (inherited by every lane)
Merge conditions. A lane's PR merges only when: the required gate lanes are green on the stamped tree or the tree receipt says gate-reuse: eligible; gstack-lane-check reports clear for its planned files against every other open lane; the PR body opens with the gstack-ship-receipt block; and the coordinator, not the lane, says merge. A lane never merges, rebases another lane, or force-pushes.
Behavior-change notices. Every user-visible behavior change outside the plan's own items is listed in the PR body under ## Behavior changes with the file, the old behavior and the new one, before the review starts. An omitted change found in review is a CRITICAL finding, whatever its size.
Spend caps. Each lane runs under the --spend-cap the coordinator names; a lane that reaches it stops incomplete (STOP: reason=budget_exhausted) and reports spent, reserved and unknown charges. Paid evals are never retried to change a verdict; a rerun needs a named repair. Host-subagent spend is reported as unknown, never zero.
Before done. A lane pushes a WIP commit to its branch after each step and gates a pushed SHA. It is done only when gstack-pregate run --allow-repo-commands passes; requires-remote needs --stage tests --remote --spend-cap <cap>. Where .gstack/pregate.json has a sealed key, it runs gstack-pregate sealed before any push or PR creation; a refusal stops the push.
Handoff format. A lane reports in this order, identifiers first: PR URL (or no PR with branch and SHA), GSTACK_RESULT line, the gstack: ok|fail <rev> line, the PREGATE: line, gate summary (lanes passed/failed/skipped), behavior changes (or none), spend (spent/reserved/unknown), then what it could not verify. Full thread ids (jam_…), never short codes, in every cross-thread message. Prose after the identifiers may be truncated; nothing before them may be.