v1.91.96.0 fix: honest install and self-upgrade on Capy (field wave A, items 1-5) (#3164)

* 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>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-10-13 14:41:58 +00:00
1 parent dbf400ec3f
commit ada251bbc0
36 files changed
+2474 -137

No files matched your search

+9
View File
@@ -125,6 +125,15 @@ sentence: "Version bump for branch-ahead discipline. No user-facing changes yet.
there. Do not pad. Do not explain the plan that will ship eventually. Do not narrate
the branch's history. When real work lands, the entry will replace this at /ship time.
### Raising the supported minimum (`capy/support.env`)
Release stamping can also raise `GSTACK_MIN_REVISION` in `capy/support.env`.
Every Capy machine below it is fast-forwarded at its next session start, so
raise it only for install-honesty or security fixes, and set it at stamp time
to the VERSION being released. It never exceeds VERSION
(test/capy-skill-wrapper.test.ts pins this). Details:
[capy-install.md](capy-install.md#raising-the-supported-minimum).
## Entry format
### Release-summary format (every `## [X.Y.Z]` entry)
+150 -30
View File
@@ -1,47 +1,114 @@
# Installing gstack on a Capy cloud machine
`bin/gstack-capy-install` installs gstack on a fresh Capy cloud machine
(Ubuntu) and ends with one line a parent agent greps. It is the repo-owned
installer the user-scope Capy skill runs after cloning; nothing in the skill
decides what "installed" means.
Three layers, each owned by gstack:
| Layer | Where | Job |
|---|---|---|
| Bootstrapper | `capy/skill/gstack/scripts/install.sh` (copied once into the owner's Drive skill folder; frozen) | clone when absent; fetch origin's default branch and read the supported minimum from `FETCH_HEAD:capy/support.env`; in install mode fast-forward a clean, unpinned checkout below it; exec the full wrapper |
| Full wrapper | `bin/gstack-capy-session` (moves with the checkout) | map the session start onto the installer: `--check` certifies, otherwise install or repair |
| Installer | `bin/gstack-capy-install` | Bun, both host profiles, the doctor, the install verdict, the launch line, the trailer |
The vendored skill (`capy/skill/gstack/SKILL.md`) tells a Capy agent how to use
them. The Drive folder is a mirror: the owner copies `capy/skill/gstack/` there
when asked; nothing writes the Drive automatically.
Direct use, without the skill:
```bash
git clone --single-branch --depth 1 https://github.com/garrytan/gstack.git ~/.local/share/gstack
~/.local/share/gstack/bin/gstack-capy-install --for autoplan
```
What it does, in order:
## Session start
1. Reuses the checkout it lives in (or clones into `--root <dir>`), under a
lock. It never resets or pulls an existing checkout. `--revision <ref>`
pins a fresh clone; an existing checkout at another revision is reused and
the trailer says `(requested <ref>; run gstack-upgrade to move)`.
2. Preflight: `git`, `curl`, `node`, `jq`, then Bun. The default Capy image
ships Bun 1.3.14, below gstack's supported minimum (1.4.2), so the installer
downloads the exact release archive, verifies its SHA-256 against
`bin/gstack-bun-version.sh` and installs it to `$BUN_INSTALL` (default
`~/.bun`). It does that only when gstack's minimum and the target project's
Bun pin (`--project <path>`, default the git toplevel of the current
directory) agree; a conflict prints both constraints and stops
(`RUNTIME_PIN_CONFLICT`, exit 3) before anything is installed.
1. `bash <skill>/scripts/install.sh --check --for <skill>` (under a second when
a checkout exists). It fetches refs only and never moves the checkout. With
no checkout it prints `gstack: fail not-installed — ...` and the session
records that line and continues when it runs no gstack workflow; install
right before the first workflow.
2. When the trailer is `gstack: fail revision ...` or `gstack: fail runtime ...`
and a checkout exists, run the same command without `--check`. Below the
supported minimum, a clean checkout on the remote default branch is
fast-forwarded (`gstack-capy-skill: upgraded <old> -> <new>`), reinstalled
and rechecked as one transaction under `.gstack-capy-install.lock` (the
bootstrapper holds it and hands it over with `GSTACK_CAPY_LOCK_HELD=1`).
A pinned (`--revision`, detached HEAD, another branch), dirty or diverged
checkout is never moved; the trailer names the cause and the exact commands
([REVISION_BELOW_MINIMUM](troubleshooting.md#revision-below-minimum)).
`GSTACK_CAPY_AUTO_UPGRADE=0` disables the fast-forward.
3. When the fetch fails (offline), the bootstrapper says `remote minimum
unknown (fetch failed)` and checks the local install only; it never claims
the minimum.
The upgrade target is always the tip of the default branch: every commit there
is a stamped release that passed CI. Recovery runs forward, never back: an
install interrupted after a fast-forward leaves the trailer `fail`, and the
next install or `bin/gstack-upgrade` run completes the pending install stages
(setup is never skipped after a fast-forward) or refuses and names the cause.
Checking out an older commit is not a supported rollback; it does not undo
setup migrations that already ran.
Between minimums, `bin/gstack-upgrade --check` says `current <v>` or `behind
<installed> -> <remote> (<n> commits)`, and `bin/gstack-upgrade` fast-forwards
and reinstalls (`bin/gstack-capy-install` on a Capy install, else `./setup
--refresh-registered`). It refuses a dirty, diverged or pinned checkout with
[UPGRADE_REFUSED](troubleshooting.md#upgrade-refused) and never stashes or
resets.
## What the installer does
1. Reuses the checkout it lives in (or clones into `--root <dir>`), under the
lock. It never resets or pulls; `--revision <ref>` pins a fresh clone, and an
existing checkout at another revision is reused with the trailer note
`(requested <ref>; run <checkout>/bin/gstack-upgrade to move)`.
2. Preflight: `git`, `curl`, `node`, `jq`, then Bun. A supported Bun already in
`$BUN_INSTALL/bin` (default `~/.bun/bin`) is reused. The project's pins
(`--project <path>`, default the git toplevel of the current directory) are
read with the doctor's own parser (`bin/gstack-runtime-pins.ts`: engines,
packageManager, `.tool-versions`, `.bun-version` and CI `bun-version:` lanes
for this platform). When neither the Bun on PATH nor the installed one
satisfies gstack's minimum and the pins, the installer downloads one exact
release archive, verifies its SHA-256 against `bin/gstack-bun-version.sh`
and installs it into `$BUN_INSTALL/bin`, beside any different system Bun (no
symlink into a shared bin directory). That archive is the supported minimum
(1.4.2), or the project's exact pin when it is above the minimum and gstack
carries a verified archive for it (`GSTACK_BUN_VERIFIED`: 1.4.2, 1.4.3), so a
repo whose CI pins `bun-version: 1.4.3` gets 1.4.3. With an exact pin, only
that version satisfies it. Any other exact pin (below the minimum, two
different ones, or one without a verified archive), or a range gstack's
minimum does not satisfy, stops with both
constraints ([RUNTIME_PIN_CONFLICT](troubleshooting.md#capy-install-runtime-conflict),
exit 3) before `./setup`. With no Bun at or above the security floor, the
verified one is installed first so the pins can be read.
3. `./setup --host claude --no-prefix`, then `./setup --host codex --model
gpt-6-astra`, both with `--no-browser`: Chromium is lazy by default.
gpt-6-astra`, both with `--no-browser`. Skipped when this exact HEAD was set
up with the same hosts, model and browser flag (recorded in the git dir
after a successful setup) and `gstack-doctor --check` passes; a warm rerun
takes about a second.
4. `gstack-doctor --check` for the requested workflow (`--for <skill>`,
`--only <skill>`, or `--require <id,id>`; default `claude,codex,patch`).
`--only <skill>`, or `--require <id,id>`; default `claude,codex,patch`),
run with the launch PATH, so the trailer certifies the Bun workflows run.
5. The install verdict, written atomically to
`<state root>/install-verdicts/<sha256 of the checkout realpath>.json`:
checkout, HEAD, the workflow and required components it certified, whether
pins were skipped as planning-only, the trailer, the remote minimum, the
launch PATH and the time.
`--check` runs only steps 4 and 5 (no clone, Bun install or setup).
## The one line to grep
The last line is the doctor's trailer:
The last line is the trailer:
```
gstack: ok 1.91.70.0 for=autoplan
gstack: ok 1.91.96.0 for=autoplan
gstack: fail runtime,pins
```
Exit 0 only with `gstack: ok`. Read the exit status and the trailer directly:
```bash
out=$(~/.local/share/gstack/bin/gstack-capy-install --for autoplan 2>&1); rc=$?
out=$(bash <skill>/scripts/install.sh --check --for autoplan 2>&1); rc=$?
printf '%s\n' "$out" | grep '^gstack: '
[ "$rc" -eq 0 ]
```
@@ -52,7 +119,13 @@ the incident this installer exists to end. Every `FAIL <id>` row carries a
`fix:` clause and a reason code with its [troubleshooting
anchor](troubleshooting.md#install-check-gstack-doctor---check-and-the-capy-installer).
A parent agent confirms each subagent machine with this grep; one machine's
install never covers another.
install never covers another, and a subagent's first report carries its own
`gstack:` line.
`SKIP pins (planning-only; repo pins differ: ..., launch bun <v>)` is not a
failure: planning workflows (`--for autoplan`, the plan reviews,
`office-hours`) never run the target repo's tests. Workflows that do (`review`,
`ship`, `qa`) and `--require` keep `pins` required.
When a lane reports to another thread (a handoff, a `gstack-lane-check`
result, a coordinator contract), store and use the full thread id (`jam_…`)
@@ -73,23 +146,70 @@ time separately.
## The launch line
The installer's last lines include:
Every successful run, `--check` included, prints before the trailer:
```
GSTACK_LAUNCH: GSTACK_SESSION_KIND=unattended GSTACK_STATE_ROOT=<path> GSTACK_EPHEMERAL=1
GSTACK_LAUNCH: PATH=<dir>:$PATH GSTACK_SESSION_KIND=unattended GSTACK_STATE_ROOT=<path> GSTACK_EPHEMERAL=1 GSTACK_CAPY=1 GSTACK_INSTALL_VERDICT=<file>
GSTACK_LAUNCH_PATH: <dir>
```
A child shell's `export` cannot configure the parent's later commands, so the
parent supplies these assignments to every gstack command it launches (prefix
the command, or export them once in the shell that runs it).
`PATH=<dir>:$PATH` and `GSTACK_LAUNCH_PATH` appear only when the certified Bun
is not first on PATH (the Capy image's `/usr/local/bin/bun` is 1.3.14; 57 gstack
commands start with `#!/usr/bin/env bun`). A child shell's `export` cannot
configure the parent's later commands, so the parent supplies these
assignments to every gstack command it launches (prefix the command, or
export them once in the shell that runs it). A fresh shell without them keeps
reporting the Bun it would really run: `FAIL runtime — bun 1.3.14 at
/usr/local/bin/bun ...; verified bun 1.4.2 found at ~/.bun/bin (not on PATH);
fix: export PATH="$HOME/.bun/bin:$PATH" or prefix commands with the
GSTACK_LAUNCH line`.
`GSTACK_EPHEMERAL=1` tells gstack the disk vanishes with the machine, so the
doctor's `state root` row reads `durable=no` and unattended runs say
`learnings: skipped` instead of writing silently. A durable root is opt-in:
set `GSTACK_STATE_ROOT` to a directory that outlives the machine. The
installer only prints the option and never writes to the Capy drive (its
policy excludes memory and verification evidence). See
installer never writes to the Capy drive. See
[docs/state-root.md](state-root.md#ephemeral-machines).
`GSTACK_CAPY=1` turns on install admission. An unattended session (any host)
refuses to start a workflow on a Bun below the supported minimum; with
`GSTACK_CAPY=1`, an unattended or `GSTACK_SESSION_KIND=spawned` session also
needs a passing verdict for this checkout, its current HEAD and a workflow at
least as wide as the one starting. A refusal prints `INSTALL: fail ...` and
`GSTACK_RESULT: skill=<s> status=refused run=-` and exits 3
([INSTALL_UNVERIFIED](troubleshooting.md#install-unverified)). Ambient
spawned sessions (OpenClaw) only warn. `GSTACK_ALLOW_UNSUPPORTED_RUNTIME=1`
admits anyway and says so.
## Bake into a Capy snapshot
Machines that never run a workflow should not pay the install (about 30 s
cold). A project that runs gstack on most machines can bake it instead: the
project Setup's initialize step runs
```bash
git clone --single-branch --depth 1 https://github.com/garrytan/gstack.git ~/.local/share/gstack
~/.local/share/gstack/bin/gstack-capy-install --require claude,codex,patch
```
and snapshots capture `$HOME` (the checkout, `~/.bun/bin`, both host
profiles). Each session then pays only the `--check`. A baked revision still
moves forward at session start when it falls below the supported minimum, and
the baked `~/.bun/bin` reaches workflows through the `GSTACK_LAUNCH` line. The
project Setup is shared by every thread in the project, so changing it is the
project's decision.
## Raising the supported minimum
`capy/support.env` holds `GSTACK_MIN_REVISION` and `GSTACK_CAPY_MIN_WRAPPER`.
Raise `GSTACK_MIN_REVISION` only for install-honesty or security fixes, at
stamp time, to the VERSION being released: every machine below it is
fast-forwarded at its next session start. It never exceeds VERSION
(test/capy-skill-wrapper.test.ts). Raise `GSTACK_CAPY_MIN_WRAPPER` together with
`GSTACK_CAPY_WRAPPER` in the bootstrapper when the bootstrapper itself
changes; older Drive copies keep working and print an owner action
([CAPY_WRAPPER_STALE](troubleshooting.md#capy-wrapper-stale)).
## Exit codes
| Exit | Meaning |
@@ -0,0 +1,283 @@
# gstack field wave (October 12, 2026): honest upgrades, closable unattended runs, plan-to-gate obligations
## URGENT, outside this plan
None known at plan time. The reviewers fill this block.
## Implementation plan
### Context
Base: origin/main `dbf400e` (v1.91.95.0). Input: field evidence from the GBRA threads that ran gstack on Capy cloud machines during gbrain fix waves 15 and 16 (2026-10-10..12, gstack 1.91.22–1.91.94), plus a 12-machine survey. Owner approval to run the wave: Garry, 2026-10-12 ("start it"), relayed by the gbrain coordinator thread. Garry merges gstack; this wave opens one PR and does not merge it.
Every item was reproduced on 1.91.95.0 before this plan was written, on a simulated Capy host (this machine: Ubuntu 24.04, `/usr/local/bin/bun` 1.3.14, no Codex CLI, a verified Bun 1.4.2 in `~/.bun/bin` off PATH). Receipts live in four investigation reports (lanes A, B1, B2, CD); the load-bearing ones are quoted inline below.
Already fixed on main and only verified here: item 6 for new runs (#3160, `decisionId` mints `<run>-r<rev>-<label>`). A 3-rev run driven only through the CLI (`next`/`submit` with hand-written results, `answer "all except d1b"` twice, then `all`, `export`) printed `ARTIFACT_VALID … status=complete`. Residual: a run gated on ≤1.91.94 and exported after upgrading produces 14×`ARTIFACT_DUPLICATE_ID` and 6×`ARTIFACT_DANGLING_REF`, because `finishGate` keeps old rows (lib/autoplan-gate.ts:111) while `buildTasks` points at the new id (lib/autoplan-export.ts:33).
Premise corrected by reproduction: item 11's words "doctor", "status" and "description" are not UI triggers. What sets `ui=yes` is non-visual uses of `component|form|screen|layout` (lib/autoplan-prompts.ts:23-26, two occurrences of any term) and Review-record prose, because UI detection reads the whole plan file (:41) while DX reads only the Implementation plan (bin/gstack-autoplan-snapshot.ts:25-26).
Repo constraints: SKILL.md is generated from `.tmpl` (`bun run gen:skill-docs`); new logic goes in `lib/*.ts` behind thin `bin/` entrypoints; registered modules are capped by `test/module-size-ratchet.test.ts`; `bin/gstack-autoplan-snapshot.ts` and `setup` are not grown; context-budget and parity ratchets apply to template edits; tests follow docs/test-value-bar.md (structure and machine-read tokens, not sentences). Pregate and ship-policy validators refuse unknown keys (lib/pregate/config.ts:58, lib/ship-policy.ts:158), so gstack ships before any consuming repo commits new keys.
Thesis: **a parent agent on Capy can trust what gstack tells it about itself, close what it starts, and carry plan obligations into the repo's own gates without hand-written records.**
### Release promises (the finish line)
- **P1. Install state is honest and self-correcting.** Every fix line names a command that exists. At session start a checkout below the supported minimum fast-forwards itself, reinstalls and rechecks; workflows refuse while the trailer is `fail`. The trailer agrees with the Bun that workflows run. A stale Capy skill wrapper is refused by name.
- **P2. An unattended /autoplan run closes in the run directory.** Phases reviewed by Capy subagents (free-form records or the canonical format) bind with `submit`; a host without the outside voice can mark it unavailable; a revised plan reopens affected phases in the same run; a 3-rev run validates; reruns ask only scope, cost or direction questions; user-challenge defaults follow agreeing voices.
- **P3. Plan obligations reach the repo's mechanical gates before a lane reports done,** paid gates are dry-run and priced before approval, a restamp is proved stamp-only by machine, and sealed or eval output is kept off any machine that can push to a public repo (owner custody, the primary control), with a local check that refuses an accidental push where local hooks run.
### Delivery shape
One wave PR on `capy/gstack-field-wave`, PATCH release (1.91.96.0 or the next free slot from `bin/gstack-next-version` at ship time). Four build lanes run in parallel on fresh machines, each on its own branch off `dbf400e`, each committing bisected commits without touching VERSION, CHANGELOG or package.json version; the integrator merges the lane branches, regenerates generated files, stamps, writes the CHANGELOG and runs the full free suite once on the integrated tree.
File ownership (to keep merges mechanical):
- Lane A owns `bin/gstack-capy-install`, `bin/gstack-doctor*`, `bin/gstack-bun-version.sh`, new `bin/gstack-upgrade`, `bin/gstack-skill-start`/`gstack-skill-end` (runtime gate only), new `capy/`, `docs/capy-install.md`.
- Lane B1 owns `lib/autoplan-run.ts`, `lib/autoplan-export.ts`, `lib/autoplan-state.ts`, `bin/gstack-autoplan` (`submit`, new `revise`), `bin/gstack-autoplan-snapshot.ts` (narrow `revise`/scaffold hooks only), `bin/gstack-artifact`.
- Lane B2 owns `lib/autoplan-gate.ts` (`buildGate`, answer/reopen logic), `lib/autoplan-reconcile.ts`, `lib/autoplan-prompts.ts`, `lib/gate-list.ts`, `lib/plan-reality.ts`, `bin/gstack-plan-reality`, `autoplan/` templates and the new driver doc. B1 adds only new functions to `lib/autoplan-gate.ts` (the `revise` reopen) and calls B2's existing ones.
- Lane CD owns `bin/gstack-pregate`, `lib/pregate/**`, `lib/eval-plan.ts`, `lib/pricing.ts`, `bin/gstack-eval-plan`, `lib/restamp.ts`, `lib/ship-policy.ts`, `lib/tree-receipt.ts`, `bin/gstack-redact-prepush`, new `lib/sealed-guard.ts`, `lib/coordinator-contract.ts`.
- Shared append-only files (`lib/result-codes.ts`, `docs/troubleshooting.md`, `docs/unattended.md`, `test/fixtures/module-size-ratchet.json`): each lane appends its own entries; the integrator resolves.
Every fix lands with a test that fails on `dbf400e` first, run on a simulated Capy host: fixture PATH with a stub or real Bun 1.3.14 first, no `codex`, scratch `HOME`/`GSTACK_HOME`/`GSTACK_STATE_ROOT`. No existing assertion is weakened; the only existing pins that change are named per item as intentional contract changes.
### Lane A: install and upgrade honesty (items 1–5)
**A1. `bin/gstack-upgrade` exists and every fix line names a real command (item 1).** No revision has ever had `bin/gstack-upgrade` (`git log --all -- bin/gstack-upgrade` is empty), yet bin/gstack-capy-install:138, docs/capy-install.md:18, docs/troubleshooting.md:1132 and the Capy skill (install.sh:34,55; SKILL.md:40) name it. The `/gstack-upgrade` skill cannot run unattended (gstack-skill-start:616 skips upgrades; its fallback uses AskUserQuestion and `git reset --hard`, gstack-upgrade/SKILL.md.tmpl:166-190).
- New `bin/gstack-upgrade` (bash 3.2, the gstack-capy-install style): `--check` fetches (bounded by `timeout 60` when present) and prints `gstack-upgrade: current <ver>` (exit 0) or `gstack-upgrade: behind <installed> -> <remote ver> (<n> commits)` (exit 1), writing nothing. Default mode refuses with exit 3 and changes nothing when the directory is not a gstack checkout, HEAD is detached or off the remote default branch (a `--revision` pin), the tree is dirty after discarding only the render footprint (`SKILL.md`, `*/SKILL.md`, `*/sections/*.md`, as the skill's fast path does), or the incoming Bun floor holds (`gstack_bun_incoming_hold`). Then `git merge --ff-only FETCH_HEAD`; divergence is `UPGRADE_REFUSED` (exit 3; never stash, never reset). Then `bin/gstack-capy-install` when `--capy` is passed (Bun preflight before setup), else `./setup --refresh-registered` (migrations run inside setup, setup:3852).
- Fix text: REV_NOTE becomes `run <root>/bin/gstack-upgrade to move`; `_dc_check_revision`'s fix names `$SRC/bin/gstack-upgrade` (keeping `/gstack-upgrade` as the interactive alternative); gstack-skill-end:46's `UPGRADE_AVAILABLE` line names the bin. New result code `UPGRADE_REFUSED` with a troubleshooting anchor.
- Tests: new `test/gstack-upgrade-bin.test.ts` (bare remote with commits A and B, depth-1 clone at A): `--check` exits 1 with `behind 1.0.0 -> 1.0.1` and HEAD unchanged; default exits 0 with HEAD at B and `setup --refresh-registered` logged; a dirty tracked file and a diverged local commit each exit 3 with HEAD, file bytes and reflog unchanged. Fails today: exit 127. A static test asserts that every `bin/<name>` a trailer or fix line names exists in the repo. Intentional pin change: test/gstack-capy-install.test.ts:203 (REV_NOTE text).
**A2. A checkout below the supported minimum upgrades itself at session start, and workflows refuse while the trailer fails (item 2).** Reproduced: a 1.91.12 checkout (7fca42ad8) printed the identical `gstack: fail installer` line on every run, `--check` or not; `--check` says run the installer and the installer says run the missing `bin/gstack-upgrade`, a closed loop. Checkouts ≥1.91.71 but behind pass forever, because the revision row compares only against an explicit `--revision` (doctor-check.sh:325). Unattended runs skip the update check (gstack-skill-start:119-123). Fast-forward plus the current installer on a real installed 1.91.12 checkout ran migrations 1.91.19 and 1.91.65 and ended `gstack: ok 1.91.95.0` (fetch plus ff of a depth-1 clone: 3.4 s).
- New `capy/support.env` (shell-sourceable, two keys): `GSTACK_MIN_REVISION=<x.y.z.w>` (the oldest revision whose installer, trailer and fix lines are honest; set to this wave's release) and `GSTACK_CAPY_MIN_WRAPPER=<n>`.
- The Capy skill wrapper (vendored in A4) at session start, in `--check` and install mode alike: `git fetch --depth 1 origin <default branch>` (bounded), read `capy/support.env` from `FETCH_HEAD` (so an old checkout learns today's minimum from the remote), compare with the checkout's VERSION. Below the minimum, and the checkout is clean, unpinned (no `--revision`, on the default branch) and fast-forwardable: `git merge --ff-only FETCH_HEAD`, run `bin/gstack-capy-install`, then `--check`; print `gstack-capy-skill: upgraded <old> -> <new>`. Pinned, dirty or diverged: no change, and the trailer is `gstack: fail revision — installed <v> is below the supported minimum <min>; fix: <exact commands>` (exit 1). Fetch failure (offline) degrades to the local check and says `remote minimum unknown (fetch failed)`, never a false ok. This reverses the documented "existing checkouts are never pulled" rule for below-minimum checkouts only; the reversal is the owner's request and is fast-forward-only, so it is reversible (`git checkout <old sha>`).
- `gstack-doctor --check` gains `--min-revision <v>` (the wrapper passes the remote minimum); the revision row FAILs below it with `REVISION_BELOW_MINIMUM` and a fix naming the wrapper or `bin/gstack-upgrade`.
- Workflow refusal: in an `unattended` or `spawned` session, `gstack-skill-start` runs the cheap verdict (the PATH Bun against the supported minimum, the checkout VERSION against the local `capy/support.env`) and, when either fails, prints the failing row with its fix and `GSTACK_RESULT: skill=<s> status=refused run=-`, exit 3. Interactive sessions keep today's warning. The Capy skill tells the agent never to run a workflow without a `gstack: ok` trailer from this session.
- Subagent drift: the coordinator contract's handoff format (lib/coordinator-contract.ts, rendered byte-identical in three places) gains, first, "the `gstack:` trailer line from this machine's `--check`"; the vendored skill says a subagent's first report to its parent carries that line.
- Tests (fail-first): `test/capy-skill-wrapper.test.ts` builds a bare remote whose main carries `capy/support.env` with a minimum above the clone's VERSION and a stub installer; running the vendored wrapper with `--check` fast-forwards, runs the installer and ends `gstack: ok <new>` (today: the wrapper does not exist; the drive copy repeats `fail installer`); a dirty checkout gets `gstack: fail revision` naming the commands and HEAD unchanged; an offline remote says `remote minimum unknown`. `test/gstack-doctor.test.ts`: `--min-revision` above VERSION FAILs the revision row. `test/gstack-skill-start.test.ts` (or the existing session-kind test file): unattended with a stub Bun 1.3.14 first on PATH prints `status=refused`.
- Live acceptance (the wave's done criterion): on this machine, a real 1.91.12 checkout at `~/.local/share/gstack` (moved aside first, restored after) upgrades itself to the wave head at session start through the vendored wrapper pointed at a local mirror whose default branch is the wave branch, and ends `gstack: ok`.
**A3. The trailer agrees with the Bun that workflows run (item 3).** Reproduced: a fresh shell's `--check` says `FAIL runtime` and `FAIL pins` while the installer's own `--check` said ok; the installer exports PATH only to itself (gstack-capy-install:108) and `GSTACK_LAUNCH` (:145) carries no PATH; 57 bins use `#!/usr/bin/env bun`, so a fresh shell silently runs 1.3.14. The installer also re-downloads Bun on every run (it checks only the PATH Bun, :91). With `/usr/local/bin/bun` 1.4.3 and gbrain's CI pin 1.4.2, the trailer is `fail pins — bun 1.4.3 outside CI 1.4.2` and the installer never installs 1.4.2, because its pin parser (:79-90) ignores CI pins.
- One helper in bin/gstack-bun-version.sh, `gstack_bun_launch_dir`: the first of `$BUN_INSTALL/bin`, `~/.bun/bin` holding a Bun that satisfies gstack's minimum and the project pin, else empty.
- `GSTACK_LAUNCH:` carries `PATH=<dir>:$PATH` when that directory is not already first on PATH. The wrapper's `--check` runs the doctor with the same PATH, so its trailer is the verdict for commands launched with `GSTACK_LAUNCH`. A bare doctor run in a fresh shell keeps reporting the PATH Bun (that is what an unprefixed workflow runs), but its runtime row says `bun 1.3.14 at /usr/local/bin/bun; verified bun 1.4.2 found at ~/.bun/bin (not on PATH); fix: export PATH="$HOME/.bun/bin:$PATH" or prefix commands with the GSTACK_LAUNCH line`. Same for the pins row.
- The installer reuses an installed verified Bun instead of downloading again, and reads the project's pins through `gstack-runtime-pins.ts` (runs on any Bun above the floor): when an exact pin (engines or the CI lane for this platform) equals the version gstack carries a verified archive for, it installs that version into `$BUN_INSTALL/bin` alongside a different PATH Bun; any other exact pin stops with `RUNTIME_PIN_CONFLICT` naming both. No symlink into a shared bin directory (it would shadow the system Bun for every project on the machine).
- Tests: test/gstack-capy-install.test.ts: after installing Bun, `GSTACK_LAUNCH: PATH=<home>/.bun/bin:$PATH GSTACK_SESSION_KIND=unattended …` (today: no PATH); a second run downloads nothing (today: two curls); stub PATH Bun 1.4.3 plus a CI pin of 1.4.2 downloads `bun-v1.4.2` and launches with its PATH (today: no curl). test/gstack-doctor.test.ts: PATH Bun 1.3.14 plus a verified 1.4.2 in `$HOME/.bun/bin` gives a runtime FAIL row containing `found at` and `not on PATH`, and with the launch PATH gives PASS.
**A4. A stale Capy skill wrapper is refused by name (item 4).** No copy of the Capy skill has ever been in the repo; Drive keeps only the current version, so the 10-07 copy (1,667 bytes, legacy "skills … are ready" line) is unrecoverable.
- Vendor the canonical skill at `capy/skill/gstack/SKILL.md` and `capy/skill/gstack/scripts/install.sh` (precedent: `openclaw/skills/`). The Drive folder becomes a mirror, updated by copying this directory when the owner asks; nothing writes the Drive automatically.
- The wrapper prints `gstack-capy-skill: wrapper <n> (<path>)` first and passes `GSTACK_CAPY_WRAPPER_REV=<n>` to the installer and doctor. `gstack-capy-install` refuses when that revision is below `GSTACK_CAPY_MIN_WRAPPER` from the checkout's `capy/support.env`: `gstack: fail wrapper — Capy skill wrapper <n> is older than this checkout's minimum <m>; fix: copy <root>/capy/skill/gstack/ over the skill folder (CAPY_WRAPPER_STALE)`, exit 1. Unset means a direct invocation and is allowed.
- A wrapper too old to call the installer cannot be caught from the checkout; the vendored SKILL.md states the rule that catches it: no `^gstack: ok` line in this session means not installed.
- Tests: `test/capy-skill-wrapper.test.ts`: the first line is the wrapper line; a fixture minimum of 99 gives `gstack: fail wrapper` with exit 1; `bash -n` passes; the wrapper's revision is at least the checkout minimum; every command the vendored SKILL.md names exists in `bin/`.
**A5. Machines that never run a workflow do not pay the install (item 5).** Measured cold install 32.5 s here (GitHub clone 7.2 s, Bun 2.6 s, `bun install` 10.6 s, build and render about 3 s, the rest of the claude setup 6.5 s, codex setup 2 s); a warm rerun 13 s. The field's 52 s is the same phases on slower machines.
- The installer skips both `./setup` runs when `gstack-doctor --check` already passes for the requested workflow at this checkout and revision (warm rerun about 1 s), and reuses the installed Bun (A3).
- The vendored SKILL.md makes the install lazy: run `--check` at session start (under 1 s, and it performs A2's upgrade when a checkout exists); with no checkout, record `gstack: not installed (no workflow yet)` and install right before the first workflow. Garry's user-scope AGENTS.md says to run the installer when the check fails; the wave reports the one-sentence amendment to the coordinator for Garry and changes nothing there itself.
- docs/capy-install.md gains "Bake into a Capy snapshot": the project Setup's initialize step runs the clone plus `gstack-capy-install --require claude,codex,patch`; snapshots capture `$HOME`; with A2 a baked revision still moves forward at session start, and with A3 the baked `~/.bun/bin` reaches workflows through `GSTACK_LAUNCH`.
- Tests: two installer runs on one fixture log setup only on the first (today: four setup calls). Intentional pin change: the default-install test (:86-105) gets a failing doctor stub before its first run so it still proves setup runs on a fresh checkout.
### Lane B1: closable runs (items 6, 7, 9)
**B1a. A 3-rev run validates, including runs gated before 1.91.95 (item 6).** `exportRun` rewrites legacy decision ids (rows with `gate_rev > 1` whose id lacks `-r<rev>-`) through `decisionId` before writing, and records `deviations: legacy decision ids rewritten (<n>)`. Safe: those runs never validated, and `ack` refuses invalid runs (bin/gstack-artifact:87-91), so no consumer holds the old ids. Tests: a new end-to-end CLI test drives three gate revisions through `next`/`submit`/`answer`/`export` and asserts `gstack-artifact validate` exit 0 with unique decision, finding, task and attempt ids (the regression pin for #3160); a fixture with legacy rev-2 ids exported on this version validates (today: 14 duplicate ids).
**B1b. `submit --record` closes phases that Capy subagents reviewed (item 7).** Four things block a parent today: the `INPUT:` receipt is required (lib/autoplan-run.ts:207-208), the findings fence is required (:209), `--attempt` is mandatory (bin/gstack-autoplan:99) though only one attempt can be open per phase and voice, and the CLI cannot mark the outside voice unavailable (`markOutside` is reachable only from `run`, bin:141), so on a host without Codex a phase can sit in `awaiting_result` forever. Hand-writing `review-record.md` does nothing: export regenerates it and validate reads `run.json`.
- `gstack-autoplan submit --out <dir> --phase <p> --voice native|outside --record <file.md> --model <id> [--attempt <id>]`. Under the run lock it binds to the single open attempt for that phase and voice (`--attempt` optional when exactly one is open); a phase `next` has not opened is refused with the `next` command to run. `--voice` stays native|outside and `--model` names the model, matching the existing path; the field request's `--voice <model>` is served by `--model`.
- Receipt: an `INPUT:` line must match the snapshot; with none, the record binds as `receipt: attested` (never stamped, since stamping would forge a verified receipt), the voice records it, and the review record shows a deviation line. Validate treats `attested` as terminal and lists it.
- Findings: the fenced block when present; else a markdown table with severity and title columns; else severity-tagged list items (`- [High] title`); else an explicit `No findings.` line; anything else is refused with `RESULT_FINDINGS_MISSING` naming the accepted shapes. The parser never guesses a severity.
- `submit --voice outside --unavailable "<reason>"` (and `--failed`) calls `markOutside` and closes the phase with `coverage=native_only`.
- When the last phase closes, `submit` writes the gate as today; `answer` plus `export` complete the run. The `--result` path is unchanged (its refusal pins in test/autoplan-run-recovery.test.ts:44-75 stay).
- Tests: a run whose three phases bind free-form records (one with a table, one with a list, one `No findings.`) through `--record` without `--attempt`, outside marked unavailable on one phase, reaches `gate_pending`, answers `all`, exports and validates; a record with a mismatched `INPUT:` is refused; prose with no findings shape is refused.
**B1c. `revise --plan <v2>` reopens affected phases in the same run; a plan without the two sections is scaffolded, not refused (item 9).** Reproduced: after editing the plan and answering `d1b`, the rerun used the same snapshot; an edit after CEO never reaches Eng because `openPhase` returns early once a snapshot exists (lib/autoplan-run.ts:117); the run reads only its private `plan-active.md` written once at init (:73-75). Plans with only one of the two sections, duplicated or reversed headings, or an empty section exit 1 with empty stdout and no result code (a plain Error falls through bin:217), and an empty section is misreported as a count error (gstack-autoplan-snapshot.ts:413-418).
- `gstack-autoplan revise --out <dir> --plan <v2> [--json]`: normalize v2 the way init does, replace `plan-active.md` atomically (restore point untouched), journal `plan_revised` with both hashes, and reopen every opened phase whose snapshot differs from v2's per-phase snapshot plus Eng; open attempts are superseded as `--redispatch` does; at `gate_pending` it is recorded as a `p1d` answer and bumps `gate_rev`. Scope may add phases, never remove them. Archives use a `.v<k>` suffix with a no-overwrite guard (today `reopenPhases` archives with `renameSync`, which silently overwrites). Prints `REVISED: plan=<sha12> reopened=<phases>` and the result line.
- Drift hints: `next` prints `PLAN_CHANGED: <source> differs from the reviewed plan; run gstack-autoplan revise --out <dir> --plan <source>` when `plan_source` changed since the last snapshot; `answer` prints the same as `PLAN_SOURCE_CHANGED` when a reply reopens phases.
- Sections: the first `next` returns `PLAN_SECTIONS_INVALID` (exit 3) with the exact problem and a fix line naming `--scaffold`; `next --scaffold` writes `<out>/plan-source.md` with the plan under `## Implementation plan` and an empty `## Review record`, never touching the user's file, and proceeds. Ambiguous plans (duplicate or reversed headings) still refuse, with the code. Snapshot `init` stays strict (test/autoplan-init.test.ts:125-138).
- Tests: revise after CEO closes reopens nothing closed but makes DX and Eng open on the v2 snapshot (prompt contains a v2-only sentence); revise at `gate_pending` bumps `gate_rev` and the rerun prompts carry v2; a second revise does not overwrite the first archive; a plan with only an Implementation plan exits 3 with `PLAN_SECTIONS_INVALID` and a non-empty stdout, and `--scaffold` proceeds.
### Lane B2: gate judgment, scope, receipts, context (items 8, 10–13)
**B2a. Reruns raise only scope, cost or direction challenges (item 8).** Reproduced: answering `uc2b` reopened Eng; the rerun's native voice raised a packaging challenge, which became `d1` with "b reopens eng", and `d1b` made rev 3. Causes: every override reopens its phase plus Eng (lib/autoplan-gate.ts:144-151); every `user_challenge` row becomes a gate item (:78-96) and the prompt asks for the flag on any merge, split, add or remove (lib/autoplan-prompts.ts:73); rerun prompts carry no gate decisions (writePrompts, :118-131); rev 2 re-asks an answered CEO item whose phase did not rerun (`buildGate` rescans every closed phase); Eng reran on byte-identical input.
- Findings gain an optional `changes: "scope"|"cost"|"direction"`. The prompt asks reviewers to set `user_challenge` only with `changes`; packaging, ordering, naming and other reversible choices are the agent's own decisions.
- On gate revisions after the first, a `user_challenge` without `changes` becomes an `auto` item whose override does not reopen any phase (`reopens: false`), recorded as an agent decision.
- Rerun prompts get a `DECIDED AT THE GATE` block listing every answered item (id, chosen option, text) so a rerun does not reopen settled questions; answers carry forward for items from phases that did not rerun, so they are not re-asked.
- `answer` prints one `APPLY: <decision id> <title> -> <chosen text>` line per accepted plan change, telling the parent to amend the plan (and, with B1c, to run `revise`).
- Tests: on a fixture run, a rerun native finding with `user_challenge` and no `changes` becomes an auto item, and answering it `b` does not reopen Eng (today: rev 3); a rerun prompt contains the `DECIDED AT THE GATE` block with the uc id; rev 2 does not re-ask a CEO item answered at rev 1 when CEO did not rerun.
**B2b. User-challenge defaults follow agreeing voices and say why (item 10).** Every `uc` item has `recommended: 'a'` (keep your direction, lib/autoplan-gate.ts:81), even though building a UC already requires both voices; the same branch also admits `disagree` rows. Flipping only `recommended` is wrong: editing gate.json so UCs recommend `b` and replying `all` printed `APPROVED: gate_rev=1 as-is`, so the change counted as accepted but was never applied and nothing reran.
- Gate options carry `changes_plan: true|false`; reopen and `APPLY:` decisions key on the chosen option's `changes_plan`, not on "differs from recommended".
- A UC whose voices confirmed each other recommends the change (`b`) with the basis rendered `recommended: b (both voices)`; a disagreement keeps `a` with `recommended: a (plan direction; voices disagree)`. The basis is stored on the gate item (`recommended_basis`) and in decisions.jsonl.
- This changes what `all` means for agreeing UCs: today it keeps every direction (and the CHANGELOG says the original direction stands); after the wave it accepts changes both voices agree on, and each is printed as `APPLY:`. The owner asked for exactly this; the CHANGELOG and docs/unattended.md say so, and it is reported to the coordinator as a default change.
- Tests: a confirmed UC renders `recommended: b (both voices)`; replying `all` prints an `APPLY:` line for it and reopens its phase plus Eng; a disagreeing UC renders `a (plan direction; voices disagree)`. Intentional pin changes: test/autoplan-answer-invalidation.test.ts around lines 79, 96 and 140 assert the old meaning of `all` and are rewritten to the new contract with equal or stronger assertions.
**B2c. Scope keys on what changes, and flags stick (item 11).**
- UI detection reads the Implementation plan only (the same input as DX).
- `ui=yes` needs a rendered surface: a path in the Implementation plan matching `\.(tsx|jsx|vue|svelte|css|scss|html|erb|hbs)\b` or `/(components|pages|views|styles)/`, or at least three distinct UI terms; fewer is weak evidence that keeps today's conservative section fallback but does not add the Design phase. `next` prints the evidence: `SCOPE: ui=no (terms: component×1; files: none) dx=yes (detected: CLI×1, flag×2)`.
- Per-axis provenance: `sources: { ui: 'flag'|'detected'|'marker', dx: … }`, printed `source=ui:flag,dx:detected`; `--no-ui` no longer labels a detected DX as flag-forced.
- New `--no-dx` (additive; without it the existing "flags only enable DX" behavior is unchanged).
- Sticky per plan: `initRun` reads `<!-- gstack-autoplan-scope: --no-ui --developer-tool -->` from the source plan (outside the Implementation plan, so the snapshot is unchanged); CLI flags win. When flags come from the CLI and the plan has no marker, init appends the marker after the plan's first heading in the source plan file and prints `SCOPE_PINNED: <marker>`.
- The interactive path's scope comes from the same detector (`snapshot scope` returns `uiRequired` with matches; autoplan/SKILL.md.tmpl uses it instead of a prose grep).
- Tests (new test/autoplan-scope.test.ts): UI words only in the Review record give `ui=false`; component/form/screen with no UI files give no Design phase; `src/components/Settings.tsx` gives `ui=true`; `{ui:false}` reports `dx:detected`; a marker with no flags gives `ui=false` from `marker`; a run with CLI flags writes the marker. Intentional pin change: test/review-checklists.test.ts:92-105 (the `source` line shape).
**B2d. PLAN_REALITY requires the rule text and a receipt per number (item 12).** Reproduced: one receipt covered five numbers, and "the decision rule was written first" passed with no rule text (`verdict=complete`, exit 0). Side bug: receipts on dot paths (`.gstack/ship-policy.json:1`, `.github/…`) are not recognized (lib/plan-reality.ts:160). `unreceipted=` works and must not regress.
- Each clause containing a number needs its own receipt (file:line, a script path or a command in backticks within the clause); misses print `numbers_unreceipted=<n>` with the clause text.
- A line claiming a decision rule exists ("rule written first", "decision rule", "preregistered rule") must quote or link the rule text (a quoted block, a fenced block, or a file:line); misses print `unquoted=<n>`.
- Either miss makes the verdict `incomplete` (exit 1) with a fix line; `unreceipted=` keeps its meaning and its text token.
- The receipt regex accepts dot paths.
- `gstack-autoplan submit` runs the check on the active plan once per run and records its verdict in run.json (report only, never refuses).
- Tests (test/plan-reality.test.ts): five numbers sharing one receipt give `numbers_unreceipted=4`; "the decision rule was written first" without the rule gives `unquoted=1`; a `.gstack/ship-policy.json:1` receipt counts; the existing `unreceipted=` case asserts the literal text token as well as the JSON (a keep-what-worked pin).
**B2e. A compact Capy-native driver doc (item 13).** A Capy parent on Claude running ceo+dx+eng reads about 1,800 lines (about 28k tokens: SKILL.md 995 lines, 521 of them preamble, plus phase sections and three phase-close reads); on GPT, one 1,923-line file (about 33k tokens). What the runner needs is about 106 lines (docs/unattended.md:135-240).
- New generated `autoplan/DRIVER.md` (template `autoplan/DRIVER.md.tmpl`, resolvers that read the code constants: `PHASES`, `FILES`, the RESULT FORMAT, the gate grammar, the result codes the loop can see, the `submit --record` and `revise` commands). Contents: phase order and scope flags, the next/submit/answer/revise/export loop, the artifact contract, the gate protocol and the coordinator contract. No hook, guard, preamble or AskUserQuestion prose.
- `gen:skill-docs` writes it; the existing freshness check covers it; a new test caps it at 160 lines and bans hook terms (`PreToolUse`, `hook`, `AskUserQuestion`, `Skill tool`). The vendored Capy skill and docs/capy-install.md point Capy at it.
### Lane CD: plan to gate, queue-aware shipping, sealed output (items 14–17)
**CD1. Repo guards run in pregate before a lane reports done (item 14).** Reproduced in a gbrain clone: a new serial test with no manifest row passed `check-test-isolation` and gstack-pregate (which ran only the new file), and `test/scripts/serial-files.test.ts` then failed. After rewording an op description, pregate selected 3,598 tests but not `test/cli-goldens.test.ts`, which spawns the CLI as a subprocess, so the import graph cannot see it. Pregate silently ignores unknown flags (`--sealed`, `--against` ran a normal pregate, bin/gstack-pregate:55,62-99). The lanes check runs whichever Bun is first on PATH (lanes.ts:26) and runs serial files in the unit process (:54).
- New `guards` check (lib/pregate/checks/guards.ts plus config.ts, as the existing pregate design names): config entries `{id, when: [globs], command, fix}`; a guard runs when any changed file matches `when`, and its failure fails pregate with the guard id and fix. `dependencies` entries already map globs to extra tests; guards cover manifest and golden scripts.
- Unknown flags exit 2 with usage. The lanes check runs Bun from `gstack_bun_launch_dir` when present (A3) and serial-pattern files (`serial_patterns` config, default `**/*.serial.test.*`) in their own process.
- The coordinator contract and docs/unattended.md say a lane runs `gstack-pregate` (unit tier on changed files plus the repo's guards) before reporting done, and its handoff carries the `PREGATE:` line.
- docs/pregate.md (or the existing pregate doc) gains a worked gbrain config: guards for `scripts/serial-files.tsv` (`test/scripts/serial-files.test.ts`), CLI goldens (`test/cli-goldens.test.ts` when `src/core/operations.ts` or `src/commands/**` change) and `bun run verify`. gbrain commits it after gstack ships (its .gitignore line 16 blocks `.gstack/` today; that change is gbrain's).
- Tests: a scratch repo with a guard on `test/**/*.serial.test.ts` fails pregate naming the guard when a serial test lands without its manifest row (today: pass); `gstack-pregate run --bogus` exits 2 (today: 0).
**CD2. Paid gates are dry-run and priced before plan approval (item 15).** eval-plan models one call per row per arm (lib/eval-plan.ts:140-171) and has no verb that takes a gate command. lib/pricing.ts has no `claude-sonnet-5-5`, so the newest Sonnet fails with `EVAL_PRICE_MISSING`.
- New `gstack-eval-plan gate --command "<exact gate command>" [--smoke-questions 2] [--cap-usd <n>]`: runs the command with every provider key stripped from the environment (keyless dry run), with `GSTACK_EVAL_DRY_RUN=1` and `GSTACK_EVAL_SMOKE_QUESTIONS=<n>` set, and reads `GSTACK_EVAL_CALLS: purpose=<p> model=<id> calls=<n> in_tokens=<n> out_tokens=<n>` lines from its output. It prices each line, prints `GATE_PRICE: total=<usd> calls=<n> purposes=<list>` plus every precondition the command printed (`GSTACK_EVAL_PRECONDITION: …`), and fails with `EVAL_GATE_UNPRICED` when the command emits no call lines (a gate whose cost is unknown cannot be approved), `EVAL_PRICE_MISSING` for an unpriced model, or `EVAL_GATE_OVER_CAP` above `--cap-usd`.
- New eval-plan check `reservation-fits-cap`: one call's input plus maximum output, priced, must fit under the smoke cap (the field's silently refused reservations).
- `claude-sonnet-5-5` pricing added with its source URL and date in the table comment, after checking the provider's current price page at build time; no number without a source.
- The /eval-plan template step that approves a plan runs `gate` for every paid gate the plan names (template text through the existing resolver; parity budget respected). Emitting the call lines is the consuming repo's change (gbrain's synopsis builds), reported to the coordinator, not made here.
- Tests (test/pricing-eval-plan.test.ts): a stub gate command that prints two call lines and one precondition is priced and listed; a stub that prints nothing gives `EVAL_GATE_UNPRICED`; the stub sees no `ANTHROPIC_API_KEY`/`OPENAI_API_KEY`; a reservation above the smoke cap fails `reservation-fits-cap`.
**CD3. Restamp delegates to the repo's command and stamp-only is proved by machine (item 16).** Reproduced: on a pure gbrain restamp from 0.60.188.0 to 0.60.189.0, tree-receipt says `TREE: changed` for 13 of 16 files, because `rewriteStamps` (lib/restamp.ts:276-291) knows only `(was X)`, unquoted `since: X` and `vX`, and misses `since: '…'`, `"version": "…"` and `<!-- …-stamp -->` comments. Setting `release_tool: bun run release:restamp` fails with `RESTAMP_RELEASE_TOOL_FAILED` because gbrain's tool refuses a dirty tree (release-restamp.ts:530-533) and gstack writes VERSION first (lib/restamp.ts:314-321). Two branches each adding migration v240 give `LANE_CHECK: clear` and a pregate pass.
- Ship policy gains `stamp_patterns` (regexes with one version capture group, shared by the writer and the tree-receipt classifier; the default stays today's three forms) and `restamp_command` (the repo's own restamp, run in place on a clean tree before gstack writes anything; gstack then only runs the receipt). Running a repo command is gated by the existing `--allow-repo-commands` and read from the base branch's policy, as `release_tool` is.
- `gstack-pregate --against <gated-head>` runs the tree receipt between that head and HEAD under the policy's stamp patterns and prints `STAMP_ONLY: yes|no files=<n>` (each non-stamp difference listed with file and line), `MIGRATION_COLLISION: none|<number> <file> vs <PR #n file>` (migration directories from `migrations_dir` policy key; open PRs through `gh pr list --json number,files`, cached for the run; `gh` unavailable prints `unknown (gh unavailable)`, never `none`), and `REGATE: needed|not-needed` (not-needed only when stamp-only and no collision). It exits 1 when a regate is needed.
- Which files count as stamp files is the repo's rule: the worked gbrain policy lists exactly the files gbrain's tree rule names, so doc `vX` mentions outside them are differences (the repo's rule wins over gstack's default).
- Tests (test/gstack-restamp-queue.test.ts, test/tree-receipt*.test.ts, new pregate test): a restamp touching `since: '0.60.188.0'`, a package.json version and a stamp comment is `STAMP_ONLY: yes` under configured patterns (today: changed); a code edit is `no` naming the file; two branches adding the same migration number report the collision (today: clear); `restamp_command` runs on a clean tree before any gstack write (today: `RESTAMP_RELEASE_TOOL_FAILED`). The negative fixture at test/tree-receipt-stamp-sensitive.test.ts:58-69 stays as is.
**CD4. A sealed-output guard runs before every push (item 17).** Reproduced: with the managed pre-push hook installed, a push carrying a per-cell transcript, a quoted sealed answer, a seed and a `/home/user` path exited 0 with no output; the home path rates LOW `internal.user_path`, silent in pre-push. gbrain-evals is public.
- New lib/sealed-guard.ts, called from bin/gstack-redact-prepush after `addedLinesFor` (the existing hook, no reinstall) and exposed as `gstack-pregate --sealed [--range <a..b>]`. Config key `sealed` in the repo's `.gstack/pregate.json` read from the pushed commit: `{paths: [globs that may never be pushed], transcripts: [globs], answer_markers: [regexes], seed_patterns: [regexes], local_paths: true}`. In a public repo every match refuses the push, naming the file, line and rule id (`sealed.path`, `sealed.transcript`, `sealed.answer`, `sealed.seed`, `sealed.local_path`); in a private repo it warns. Repos without a `sealed` key see no change.
- Visibility: config override, then a 24-hour cache under the state root, then `gh repo view --json visibility` on the push target with a 5 s timeout; `unknown` counts as public.
- Capy's PR tool may not run local hooks, so the vendored Capy skill and the coordinator contract say to run `gstack-pregate --sealed` before any push or PR creation in a repo with a `sealed` config.
- Custody is the primary control: sealed held-out sets stay in owner custody (never on Capy Drive or a Capy machine that can push to the public repo); the guard is defense in depth. Whether Capy's PR tool runs local pre-push hooks is not measured in this wave; PR C's body says so and the coordinator records the measurement. Server-side push protection on the public repo is the owner's call and is listed in the rollout tranche.
- Tests: a scratch public repo (visibility override) with a `sealed` config refuses a push of each kind, naming file and rule (today: exit 0, no output); a private override warns and passes; a repo without the key passes silently.
### Keep what worked (regression pins)
- `install.sh --check` FAIL rows carry exact `fix:` lines: pinned by test/gstack-doctor.test.ts; A3 adds the `found at … not on PATH` row and A1 the static "every named bin exists" test.
- `gstack-autoplan next` prints phase order and the snapshot hash: pinned at test/autoplan-run-recovery.test.ts:207; B1's end-to-end test also asserts every voice's bound result carries the phase's snapshot hash.
- Accepted-obligation blocks that replace earlier ones: pinned at test/autoplan-obligations.test.ts:265, :326, :382; untouched.
- Exact-token gate answers with `REOPENED:` reruns: grammar pinned in test/gate-list.test.ts:50-117; reruns pinned only through JSON, so B2a adds an assertion on the literal `REOPENED: <phases>` line.
- PLAN_REALITY `unreceipted=`: pinned through JSON only; B2d pins the text token.
- Pluggable outside voice: pinned by test/outside-runner-contract.test.ts; B1b's `--unavailable` and `--record` keep the runner labels.
### Not in this wave
- Changing gbrain itself (its `.gstack/` gitignore line, its pregate and ship-policy config, emitting `GSTACK_EVAL_CALLS` lines, `since:` handling in its restamp tool): reported to the coordinator with the worked configs.
- `bun install --production` for the planning-only install (17.2 s to 3.7 s measured), because `build.sh` and `lib/conductor-env-shim.ts` load devDependencies; needs an audit first.
- Writing Garry's Drive skill or his user-scope AGENTS.md: the coordinator copies `capy/skill/gstack/` when Garry asks.
### Validation
1. Each lane: its fail-first tests fail on `dbf400e` and pass on the lane head, plus the focused suites it touches, run on this simulated host (PATH Bun 1.3.14 first for host-shape tests, Bun 1.4.2 for running the suite).
2. Integrator: `bun run gen:skill-docs`, `bun run typecheck`, the context-budget, parity and module-size ratchets, then `bun run test` (full free suite) once on the integrated tree. Paid PR evals (`eval:bg:pr`) on the pushed head for the touched templates.
3. Live acceptance on this machine: a real 1.91.12 checkout upgrades itself at session start through the vendored wrapper (A2); a 3-rev unattended run of this plan's own autoplan output validates (B1a); `submit --record` closes a run reviewed by subagents (B1b).
### Effort
Four lanes in parallel, each about 2–4 hours of agent time; integration and the full suite about 1 hour; no paid spend beyond the PR eval tier and this plan's reviews (host subagents, spend unknown).
### Gate decisions (gate_rev 1, 2026-10-13 00:29 UTC; answered `d1b d3b d4b d5b p1b` by the wave thread under Garry's standing rule: take the recommendation, escalate one-way doors)
Six reviewer passes (CEO, DX and Eng; native claude-opus-5-5 and outside gpt-6.1-sol, both as Capy host subagents) produced 111 findings: 3 Critical, 37 High. The gate offered five direction changes, every one defaulted to "keep your direction", which is item 10 in the field.
- **d1b, accepted: the Drive skill becomes a frozen bootstrapper.** Old checkouts run only the Drive copy, so P1 depends on it. Lane A ships `capy/skill/gstack/` with a short bootstrapper whose only job is clone-or-fetch, fast-forward under the lock when below the remote minimum, and exec the checkout's own wrapper. Copying it into Garry's Drive once is a release step owned by the coordinator (GBRA-40 was already asked to update the skill when this lands); the PR body lists it first under "after merge".
- **d2a, kept: agreeing user challenges recommend the change.** Garry's wave brief asks for exactly that ("When the voices agree, recommend their change, and mark the default's basis"), so it is not an unasked reversal. Amended per outside-ceo F2 and eng F13: "agree" means both voices flagged `user_challenge` on the same finding (reconciled `confirmed`) **and** carry the same `changes` value; anything weaker keeps `a` with the basis shown. The gate grammar gains `all as-is` (keep every direction in one token), and p1's recommended text names the changes it would apply. Reported to the coordinator as a default change.
- **d3b, accepted: three PRs, not one.** PR A (install and upgrade, lane A), PR B (autoplan, lanes B-core and B-aux), PR C (pregate, paid gates, restamp, sealed guard, lane CD). Each takes its own PATCH slot from `bin/gstack-next-version` at ship time; later PRs merge main and restamp. A dispute over PR B's gate defaults cannot hold back the install fix.
- **d4b, accepted: a coordinator-owned rollout tranche.** The field promises need steps outside gstack: the Drive bootstrapper copy, the one-sentence amendment to Garry's user-scope AGENTS.md (lazy install; trailer rule), gbrain's `.gstack/` config (pregate guards, ship policy stamp patterns, sealed paths), gbrain emitting `GSTACK_EVAL_CALLS` lines, and a post-release rerun of the 12-machine survey (each machine reports its `gstack:` trailer after one session start). The wave hands this list to GBRA-40 with worked configs; it does not claim the field promise met until the survey rerun reports.
- **d5b, accepted: paid gates are priced from a non-executing call plan.** CD2 no longer runs the gate command to learn its cost. The repo declares a plan command (`price_command` in `.gstack/pregate.json`, or `--plan-command`) that prints the call-plan lines without making provider calls; gstack runs it under an environment allowlist (`env -i` plus PATH, HOME, TMPDIR, LANG and names passed with `--keep-env`), with network denied when `unshare -rn` is available (and says `network: not contained` when it is not), with a timeout. A repo whose gates declare no plan command gets `EVAL_GATE_UNPRICED` as a recorded warning that needs `--allow-unpriced <reason>` to approve, never a silent pass.
- **p1b: approved with the overrides above.** On 1.91.95 the reopened CEO and Eng phases cannot see a revised plan (item 9, observed in this run: `REOPENED: ceo,eng` then the rerun would reuse snapshot `7ce72ff6…`). Instead of rerunning on stale bytes, the amendments below were verified by one outside pass (Review record), and PR B's own `revise` is exercised on this run as live acceptance.
### Accepted review amendments (replace conflicting text above)
**Delivery (replaces "Delivery shape" ownership).** Four builders, three PRs. Lane A → PR A. Lane B-core (items 6–10: `lib/autoplan-run.ts`, `lib/autoplan-gate.ts`, `lib/autoplan-export.ts`, `lib/autoplan-state.ts`, `lib/autoplan-reconcile.ts`, `lib/gate-list.ts`, `lib/headless-artifacts.ts`, `bin/gstack-autoplan`, `bin/gstack-artifact`, narrow hooks in `bin/gstack-autoplan-snapshot.ts`) and lane B-aux (items 11–13: `detectScope` and scope printing in `lib/autoplan-prompts.ts`, `lib/plan-reality.ts`, `bin/gstack-plan-reality`, `autoplan/DRIVER.md.tmpl` and its resolvers, `autoplan/SKILL.md.tmpl` scope step) → PR B; B-core owns every state transition, B-aux exposes pure functions B-core calls (`checkPlanReality`, `detectScope`) and writes DRIVER.md from the command spec in this plan. Lane CD → PR C. `lib/coordinator-contract.ts` and the vendored Capy skill text have one writer each (CD and A); other lanes send their sentences to the integrator. Shared append-only files are resolved by the integrator, and the generated `bin/gstack-doctor-components.sh` is regenerated, never hand-merged. All lanes use `test/helpers/capy-host.ts` (committed on the base before dispatch).
**Lane A amendments.**
- `--check` stays read-only for the checkout: it may fetch (refs only) to learn the remote minimum, and reports `gstack: fail revision — installed <v> is below the supported minimum <m> (<cause>); fix: bash <wrapper> --for <skill> (fast-forwards) (REVISION_BELOW_MINIMUM)`. Install mode performs the fast-forward. Session start in the vendored skill: run `--check`; when it fails on `revision` or `runtime` with a checkout present, run install mode (about 1 s when nothing changed, thanks to the setup skip). With no checkout and no workflow planned, record the `gstack: fail not-installed` trailer and continue (one trailer grammar, no third shape).
- Never pass `--depth` when fetching into an existing checkout: `git fetch origin <default branch>`, read `git show FETCH_HEAD:capy/support.env`; on "refusing to merge unrelated histories" run `git fetch --unshallow` once and retry. Fixtures clone from `file://` URLs and assert `git rev-parse --is-shallow-repository` is `true`; two consecutive session starts (below, then at, the minimum) are tested.
- The fast-forward, install and check run as one transaction under `.gstack-capy-install.lock`; the installer accepts the held lock through `GSTACK_CAPY_LOCK_HELD=1` instead of deadlocking. Any dirty tracked byte refuses the upgrade (no discarding of a "render footprint"; an installed checkout is clean after setup, verified on this machine). An interrupted setup after the fast-forward leaves the trailer `fail` and the next install run completes it (tested with a setup stub that fails once).
- Kill switch `GSTACK_CAPY_AUTO_UPGRADE=0`: no fast-forward, trailer `fail revision … (auto-upgrade disabled)`. docs/RELEASING.md gains when `GSTACK_MIN_REVISION` may be raised (install-honesty or security fixes only, set at stamp time from VERSION) and a test pins `GSTACK_MIN_REVISION <= VERSION`.
- `bin/gstack-upgrade` reuses `gstack-session-update`'s fast-forward logic and lock (extract the shared part rather than adding a fourth copy), autodetects a Capy install (the marker `gstack-capy-install` writes) and then runs the installer; `--check` offline exits 3 with `UPGRADE_FETCH_FAILED`.
- The trailer and the launch line come from every successful wrapper run, `--check` included: `GSTACK_LAUNCH: PATH=<dir>:$PATH GSTACK_SESSION_KIND=unattended GSTACK_STATE_ROOT=<p> GSTACK_EPHEMERAL=1 GSTACK_CAPY=1` plus `GSTACK_LAUNCH_PATH: <dir>` for callers that do not evaluate shell syntax.
- Workflow refusal is keyed on a verdict file, not a tautology: the wrapper writes `$GSTACK_STATE_ROOT/install-verdict` (trailer, HEAD sha, remote minimum, time; state root through `gstack-paths`). `gstack-skill-start` (after printing `SESSION_KIND`) refuses in `unattended` sessions and in explicit `GSTACK_SESSION_KIND=spawned` sessions with `GSTACK_CAPY=1` when the verdict is missing, `fail`, or for another HEAD, or when the PATH Bun is below the minimum; the preamble gains one rule (regenerated within budget) to stop on `status=refused`. `bin/gstack-autoplan` checks the same verdict at its mutating verbs (`next`, `submit`, `answer`, `revise`, `export`), leaving `status` and `resume` available. `GSTACK_ALLOW_UNSUPPORTED_RUNTIME=1` overrides and is recorded as a run deviation. Ambient OpenClaw `spawned` sessions without `GSTACK_CAPY` only warn.
- Stale wrapper: the bootstrapper reads `GSTACK_CAPY_MIN_WRAPPER` from `FETCH_HEAD` and, when stale, execs the checkout's vendored wrapper with the same arguments and prints `gstack-capy-skill: owner action — Drive copy is wrapper <n>, minimum <m>; copy <root>/capy/skill/gstack/ (CAPY_WRAPPER_STALE)`. It refuses only when no vendored wrapper exists.
- Pins: installing the verified Bun comes first when no Bun at or above the floor exists; pins are then read with it. An installed Bun that satisfies every applicable constraint is reused, not treated as a conflict. For planning-only workflows (`--for autoplan`, the plan reviews, office-hours) a repo test-pin mismatch is reported as `SKIP pins (planning-only; repo pins bun 1.4.2, launch bun 1.4.3)` instead of failing; workflows that run the repo's tests (`ship`, `review`, `qa`, pregate) keep `pins` required.
- The setup skip keys on the source HEAD sha plus host and model recorded after a successful setup, never on VERSION alone; it does not apply after a fast-forward.
- New bash result codes source `gstack_result_anchor` from the generated components file (no private table in gstack-capy-install); a test resolves every `(CODE)` printed by bash bins to an anchor.
- The handoff line goes after the identifiers: `GSTACK_RESULT` first, then `gstack: ok|fail <rev>`; CD writes it into the coordinator contract.
- Live acceptance runs through the bootstrapper as Drive would hold it, against a real 1.91.12 checkout.
**Lane B-core amendments.**
- B1a: one idempotent `migrateLegacyDecisionIds` under the run lock at state load (journal event once when n > 0), not inside every `exportRun`. run.json records `gstack_version`; a command on a newer gstack prints `GSTACK_VERSION_CHANGED` and runs journaled forward migrations; a downgrade refuses.
- B1b: `submit --record` accepts only: the canonical fence, an exact `No findings.` line, or exactly one findings-shaped markdown table (severity and title columns) or list; more than one candidate shape refuses. A record without `INPUT:` refuses with `RESULT_RECEIPT_MISSING` unless `--attest "<reason>"` is passed; an `INPUT:` that is present but wrong always refuses. Attested binds require `--attempt` and are refused when a `plan_revised` event follows that attempt's dispatch. Validate prints `attested=<n>`; the voice keeps `receipt: attested` and the attest reason. `--attempt` stays optional on the canonical path when exactly one attempt is open.
- `submit --voice outside --unavailable|--failed "<reason>"`; one shared terminal-voice predicate is used by `next`, `submit`, `resume` and close, and terminal outcomes are journaled with their attempt id before the manifest is written, so `next` and `resume` never redispatch or resurrect an unavailable voice.
- B1c `revise`: drift is detected on the Implementation-plan hash, not file bytes. Revise reopens every closed or open phase whose reviewed hash differs from v2 (in practice all reviewed phases plus Eng), resets newly in-scope phases from their skipped stub and updates `required_phases`, stores `plan_heading` in run.json, publishes active plan, state, archives and journal as one recoverable transaction (journal intent first; `resume` completes or rolls back), keeps `.r<rev>` archive names and adds `.v<k>` only on collision (test/autoplan-answer-invalidation.test.ts:106 stays). Revise at `gate_pending` is its own journal event, not p1d; p1d's text points at `revise`.
- Amendment interlock (owns B2a's `APPLY:` lines): accepted plan changes are persisted as pending amendments keyed by decision id; `answer` prints `NEXT: gstack-autoplan revise --out <dir> --plan <source>` instead of `next` when any exist, `next` refuses with `PLAN_AMEND_PENDING` until `revise` binds a changed Implementation-plan hash or `--withdraw <decision id>` drops one, and `export` refuses `complete` with pending amendments.
- Partial replies resolve against persisted decisions: an earlier override is never overwritten by a later `all`, only unresolved items default, and the gate stays pending until every approval settles; validate rejects `complete` with a pending approval.
- Carry-forward uses a stable origin key (phase, finding id, snapshot hash, option text hash), not the display label; only unchanged proposals from phases that did not rerun carry forward.
- `changes` is wired through the result parser, canonical rows, the findings schema and decisions; `recommended_basis` is stored on gate items and decisions.
- `submit` records `checkPlanReality` on the active Implementation-plan hash, cached by hash and checker version and invalidated by `revise`.
**Lane B-aux amendments.**
- B2c: never write the source plan. Flags persist in run.json for the run (revise inherits); a `<!-- gstack-autoplan-scope: … -->` marker in the plan, if the author puts one there (first line, before any heading), is read; `next` prints `SCOPE_PIN: add <marker> as the first line of <file> to keep these flags`. UI evidence is visual context (rendered file paths, or a UI term next to a visual noun: page, layout, style, pixel, view, browser, CSS), not a three-term threshold; tests cover non-visual CLI prose, quoted paths and real rendered changes.
- B2d: `unreceipted_numbers=<n>` with one `NUMBER_UNRECEIPTED: <clause>` line each, scoped to measured quantities (a number with a unit: s, ms, %, $, USD, tokens, ×, MB, GB, or after measured/costs/takes/took); versions, file:line, ids and sha receipts are excluded. The sha alternative in `RECEIPT_RE` needs at least one a–f letter or a commit/backtick context. The verdict keeps the existing session-kind mapping (`incomplete` unattended, `repair` otherwise). A false-positive test runs the checker over this plan and pins a small count.
- B2e: DRIVER.md is the documented unattended entry for every host, printed as `DRIVER: <root>/autoplan/DRIVER.md` in `gstack-autoplan --help` and on the first `next`; installed-layout asset resolution covers it (runtime asset helper), and it is registered in the generator's normal, out-dir and dry-run inventories. The banned-terms test bans harness terms (`PreToolUse`, `SessionStart`, `AskUserQuestion`, `Skill tool`), not the bare word hook. The vendored Capy SKILL.md gets a line cap in its test.
**Lane CD amendments.**
- CD1: lanes run `gstack-pregate run --allow-repo-commands` (named in the contract, docs/unattended.md and DRIVER.md); required guards cannot be downgraded, missing permission is `incomplete`, guards are read from the base policy, each runs in its own process with a timeout. New check ids are added to the pinned stage list (test/pregate-selection.test.ts:101).
- CD2 per d5b. The call-plan wire format is versioned and documented (`GSTACK_EVAL_CALLS v1: purpose= model= calls= in_tokens= out_tokens= max_out_tokens= scope=full|smoke`), aggregate totals are separate from per-call reservations, a receipt binds the price to the command, head and inputs, and malformed, partial or failed producers refuse. Pricing rows for the newest Sonnet, embedding and Gemini models are added only with a source URL and date; an unpriced model makes the total `unknown`, never zero.
- CD3: verbs `gstack-pregate regate --against <head>` (bare `--against` kept as an alias) and `gstack-pregate sealed`. `REGATE: not-needed` requires stamp-only, no migration collision, and gate evidence for `<head>` (an evidence bundle or tree receipt naming it with the gate lanes passed); missing evidence prints `REGATE: needed (no gate evidence for <head>)`. Stamp eligibility is an explicit file allowlist from the policy (`stamp_files`) before any pattern applies; `stamp_patterns` are literal templates with one `{version}` slot (no free regex), bounded in count and length. Migration collisions use `migrations` policy (`dir`, `id` pattern), `gh pr list --state open --limit 1000 --json number,headRefOid,files`, treat a 100-file list as truncated (fetch that PR's files separately), exclude the PR itself, compare with the base branch's additions, and print `unknown` (which blocks `not-needed`) on any query failure.
- CD3 restamp: `restamp_command` runs as the first stage inside restamp's staging worktree (a clean checkout of HEAD), receives the allocated target version as an argument and in `GSTACK_RESTAMP_VERSION`, the generic version writer is skipped when it is set, the output-confinement check still runs, and a failure or a wrong-version output writes nothing to the branch.
- CD4: the guard enumerates every commit the push publishes relative to the destination (`git rev-list <remote sha or merge-base>..<local sha>`) and evaluates path rules on every path added, renamed or copied in those commits, independent of text (binary and empty files included; add-then-delete is caught). Text rules run on added lines of each commit. The effective policy is the union of the destination base branch's `sealed` config and the pushed one (a push can tighten, never relax); waivers come only from a base-branch allow file (rule, path, sha, reason). Refusals end with `(SEALED_OUTPUT_REFUSED)` and name the visibility override key. `gstack-pregate sealed --range <a..b>` shares the core and is what the Capy skill tells agents to run before any push or PR creation; whether Capy's PR tool runs local hooks is checked on this machine and the result recorded in the PR body. A required server-side check for public eval repos is in the coordinator tranche (d4b).
- State-root paths in new bash bins resolve through `gstack-paths` with the exact guard line; TypeScript uses `resolveStateRoot`.
**New result codes (one table; each gets a troubleshooting anchor).** `UPGRADE_REFUSED`, `UPGRADE_FETCH_FAILED`, `REVISION_BELOW_MINIMUM`, `CAPY_WRAPPER_STALE`, `PLAN_SECTIONS_INVALID`, `PLAN_AMEND_PENDING`, `GSTACK_VERSION_CHANGED` (notice), `EVAL_GATE_UNPRICED`, `EVAL_GATE_OVER_CAP`, `EVAL_DRY_RUN_FAILED`, `PREGATE_GUARD_FAILED`, `SEALED_OUTPUT_REFUSED`. Existing codes are reused where they fit (`RUNTIME_BELOW_MINIMUM`, `RESULT_RECEIPT_MISSING`, `RESULT_FINDINGS_MISSING`).
**Behavior changes (for each PR body's `## Behavior changes`).**
| Change | Old | New | Opt-out |
|---|---|---|---|
| Below-minimum checkout at session start (Capy wrapper install mode) | never pulled | fast-forward-only, then install and check | `GSTACK_CAPY_AUTO_UPGRADE=0`; `--revision` pins |
| Unattended or Capy-spawned workflow with a failing verdict | runs anyway | `status=refused` (skill-start and `gstack-autoplan` mutating verbs) | `GSTACK_ALLOW_UNSUPPORTED_RUNTIME=1` (recorded) |
| `GSTACK_LAUNCH` line | no PATH; installer only | carries the verified Bun dir and `GSTACK_CAPY=1`; printed by `--check` too | none needed |
| Planning-only `--check` with a repo test-pin mismatch | `fail pins` | `SKIP pins (planning-only …)` | `--require pins` |
| Gate `all` on a UC both voices agree on with the same `changes` | keeps direction | applies the change (`APPLY:` line, revise required) | `all as-is` |
| Rerun user challenges without `changes` | approval items that reopen Eng | auto agent decisions, no reopen | none |
| UI detection | whole file, any two UI words | Implementation plan, visual context | `--ui` |
| PLAN_REALITY | one receipt covers a line | each measured number needs one; rule claims need the rule | none (report-only in `submit`) |
| `gstack-pregate` unknown flags | ignored | exit 2 | none |
| Pre-push hook in repos with a `sealed` config | no sealed check | refuses in public repos | base-branch allow file |
## Review record
Autoplan run `ap-20261012235347` (gstack 1.91.95.0, Capy cloud machine, no Claude Code harness; native claude-opus-5-5 and outside gpt-6.1-sol as host subagents; scope `--no-ui --developer-tool`, order ceo,dx,eng, snapshot `7ce72ff6…0f09d9` for all three phases). CEO: 15 + 10 findings (confirmed 1). DX: 30 + 16 (confirmed 0). Eng: 18 + 22 (confirmed 1). Gate rev 1 answered as recorded above; `REOPENED: ceo,eng`. Full results, consensus files and the gate are kept with the run; the amendments above are the accepted outcome.
### Amendment verification (outside voice gpt-6.1-sol, after gate rev 1)
One outside pass checked the gate decisions and amendments against every Critical and High finding in the six results (4 Critical and 53 High by its count of the fences; the gate summary above undercounted). 35 resolved, 1 declined, 21 open or partial; it ranked 15 builder corrections. Sent to the builders as replacements:
- **CD2:** price from a versioned call-plan data file (`price-gate --plan-file`, or base-policy `price_file`) bound to the gate command, head and inputs. No repository command runs before approval, and `--allow-unpriced` is a recorded owner override that never authorizes spend.
- **CD4 boundary:** published commits are `git rev-list <local> --not --remotes=<remote>`, not a local merge-base. Malformed policy fails closed. Unknown visibility counts as public, and a cached private verdict older than an hour is re-queried before it can downgrade a refusal. P3 promises a local check against accidental publication; hosted CI only detects bypasses after upload.
- **CD3:** `regate` derives reuse only from a freshly evaluated tree receipt, with per-lane verdicts and stamp-sensitive reruns. A delegated restamp requires a clean caller tree.
- **A:** the bootstrapper and the full wrapper live at distinct paths with a lock handoff. The verdict is keyed by checkout realpath and HEAD, and its path is carried in `GSTACK_LAUNCH`. It is required only under `GSTACK_CAPY=1`, so other hosts get the local runtime check. Support metadata resolves through `.source-path`.
- **B-core:** `revise --incorporates <ids>` clears only the named amendments. The UC option b renders both voices' remedies verbatim. One integrated conformance test covers the whole loop.
Declined, with reasons:
- **Fully semantic agreement detection (correction 3).** It cannot be decided mechanically. Rendering both remedies plus `all as-is` leaves the call visible to the owner.
- **Pinning the upgrade to a qualified release sha (correction 9).** Every gstack main commit is a stamped, CI-green release, so the minimum is the policy lever. The plan no longer claims that a checkout of an old sha rolls back migrations.
PR order: A, then B stacked on A (it reads A's verdict helper), and C independent.
### Gate revisions 2 and 3 (on PR B's code)
The run moved to PR B (`capy/gstack-autoplan-wave`). It printed `GSTACK_VERSION_CHANGED` and migrated. `revise --plan` with the amended plan reopened CEO and DX on snapshot `b5d68733…` (Eng was already reopened by the rev 1 answer). `revise --incorporates d1b,…` named rev 1 ids that 1.91.95 never recorded as amendments; it now refuses with `AMENDMENT_UNKNOWN` instead of `PLAN_AMEND_PENDING` (fixed in PR B, with a fail-first test). All three phases reran through `submit` with verified receipts.
- **Gate rev 2:** 75 findings, 3 items. `d1b` accepted: P3 now makes owner custody the primary control for sealed output, and the CD4 notes say that whether Capy's PR tool runs local hooks is unmeasured. `d2a` kept: every phase snapshots the whole plan, so any edit reopens each phase. `p1b`. The change was bound with `revise --incorporates ap-20261012235347-r2-d1` (snapshot `5ad91c4b…`), and CEO, DX and Eng reran with the `DECIDED AT THE GATE` block.
- **Gate rev 3:** 2 items. `all` (approved as-is). The open item `d1` (one voice: High findings that are not user challenges leave no disposition) is recorded as a follow-up, not a change to this wave.
- `export`, then `gstack-artifact validate run.json`: `ARTIFACT_VALID: run=ap-20261012235347 status=complete`, with 3 gate revisions, 2 plan revisions, 1 amendment incorporated, 76 findings, 29 tasks, 11 decisions and none pending.
- Across 9 phase closes, `confirmed=0`: the consensus matcher never paired a native finding with an outside one, so item 10's agreeing-voices default did not fire on a real run. This is a follow-up.
+86 -8
View File
@@ -1126,21 +1126,25 @@ constraint with its source), or change the project's pin.
revision (a VERSION string, tag, branch or commit) and the checkout is at
another. An unmet revision is never `ok`.
**Fix.** Move the checkout: `/gstack-upgrade`, or
**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 gstack-upgrade to move)`.
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 upgrades Bun only when gstack's supported minimum
and the target project's pin agree on a version. Here they do not (for
example the project pins `bun@1.3.14` exactly), so the installer changed
nothing and stopped before `./setup`.
**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.** Raise the project's pin to gstack's supported minimum, or install
gstack on a machine whose Bun the project accepts. `--project <path>` names
**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>
@@ -1170,6 +1174,80 @@ 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
+13
View File
@@ -282,3 +282,16 @@ line, on an ephemeral state root.
`/autoplan` run (the one that produced this wave's plan) and validates clean.
`test/fixtures/multi-agent-wave/INCIDENTS.md` records what went wrong while it
was produced by hand and which validator rule now catches each incident.
## Install admission
An unattended run, and a `GSTACK_SESSION_KIND=spawned` run with `GSTACK_CAPY=1`,
starts only on a supported Bun and, on a Capy machine (`GSTACK_CAPY=1`, set by
the installer's `GSTACK_LAUNCH` line), with a passing install verdict for this
checkout, its HEAD and the workflow. Otherwise `gstack-skill-start` prints
`INSTALL: fail runtime|verdict — <detail>; fix: <command> (<CODE>)` and
`GSTACK_RESULT: skill=<s> status=refused run=-`, and exits 3; stop and run the
fix. `GSTACK_ALLOW_UNSUPPORTED_RUNTIME=1` admits anyway (`INSTALL_VERDICT:
overridden`), recorded as a deviation. TypeScript entrypoints use
`checkInstallAdmission` from `lib/install-verdict.ts`; the Bash side is
`bin/gstack-install-verdict.sh`. See [capy-install.md](capy-install.md#the-launch-line).