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

@@ -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.