From dfccf5a7504fbfabec888e87175aad3414924b02 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sat, 10 Oct 2026 16:25:32 -0700 Subject: [PATCH] =?UTF-8?q?docs(designs):=20multi-agent=20wave=20plan=20?= =?UTF-8?q?=E2=80=94=20honest=20install,=20unattended=20autoplan,=20queue-?= =?UTF-8?q?aware=20ship,=20cheap-first=20gates=20(#3113)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs(designs): multi-agent wave plan with autoplan review record and artifacts Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(designs): record owner approval of the multi-agent wave gate (all recommended options) 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> --- docs/designs/MULTI_AGENT_WAVE_2026_10_10.md | 393 ++++++++++++++++++ .../multi-agent-wave-2026-10-10/ceo-native.md | 232 +++++++++++ .../ceo-outside.md | 47 +++ .../decisions.jsonl | 13 + .../multi-agent-wave-2026-10-10/dx-native.md | 165 ++++++++ .../multi-agent-wave-2026-10-10/dx-outside.md | 51 +++ .../multi-agent-wave-2026-10-10/eng-native.md | 263 ++++++++++++ .../eng-outside.md | 162 ++++++++ .../findings.jsonl | 65 +++ .../multi-agent-wave-2026-10-10/run.json | 120 ++++++ .../multi-agent-wave-2026-10-10/tasks.jsonl | 28 ++ .../multi-agent-wave-2026-10-10/timing.json | 46 ++ 12 files changed, 1585 insertions(+) create mode 100644 docs/designs/MULTI_AGENT_WAVE_2026_10_10.md create mode 100644 docs/designs/multi-agent-wave-2026-10-10/ceo-native.md create mode 100644 docs/designs/multi-agent-wave-2026-10-10/ceo-outside.md create mode 100644 docs/designs/multi-agent-wave-2026-10-10/decisions.jsonl create mode 100644 docs/designs/multi-agent-wave-2026-10-10/dx-native.md create mode 100644 docs/designs/multi-agent-wave-2026-10-10/dx-outside.md create mode 100644 docs/designs/multi-agent-wave-2026-10-10/eng-native.md create mode 100644 docs/designs/multi-agent-wave-2026-10-10/eng-outside.md create mode 100644 docs/designs/multi-agent-wave-2026-10-10/findings.jsonl create mode 100644 docs/designs/multi-agent-wave-2026-10-10/run.json create mode 100644 docs/designs/multi-agent-wave-2026-10-10/tasks.jsonl create mode 100644 docs/designs/multi-agent-wave-2026-10-10/timing.json diff --git a/docs/designs/MULTI_AGENT_WAVE_2026_10_10.md b/docs/designs/MULTI_AGENT_WAVE_2026_10_10.md new file mode 100644 index 000000000..da0f3aad0 --- /dev/null +++ b/docs/designs/MULTI_AGENT_WAVE_2026_10_10.md @@ -0,0 +1,393 @@ + +# gstack multi-agent wave (October 10, 2026): honest install, unattended autoplan, queue-aware ship, cheap-first gates + +## URGENT, outside this plan + +None. Six reviewer passes (CEO, DX, Eng; native Claude and outside gpt-6-astra each) raised no security or data-loss finding the plan does not own. The one live defect found during the run, the user-scope installer's `--check` certifying an absent install (`~/.capy/drive/user-garry-tan/skills/gstack/scripts/install.sh:7-16`, `if --check; then check_install; exit 0; fi`), is owned by A2/D3 and is worked around today by running the full install on every Capy machine (86 s). + +## Implementation plan + +### Context + +Base: origin/main `91bbd9e` (v1.91.69.0). Input: feedback from 28 Capy threads that built gbrain and gbrain-evals over a month on cloud machines with no Claude Code harness, a coordinator thread merging PRs one at a time, full test gates on ephemeral Ubicloud VMs, and preregistered paid evals. 13 of the 28 threads ran no gstack workflow beyond the installer; several times the skills were not installed and the check did not say so. The others used each repo's own release tooling because `/ship` does not fit a shared serial merge queue. + +Evidence gathered for this plan (2026-10-10, this machine, a fresh Capy cloud box): +- **The install check lies by omission (P0-1 reproduced).** The user-scope Capy installer's `--check` (`~/.capy/drive/user-garry-tan/skills/gstack/scripts/install.sh`, a bare `test -x` chain) printed nothing and exited 1 in 0.004 s with nothing installed. The cause is `if --check; then check_install; exit 0; fi` under `set -e`: a failed probe exits 1 with no message, and through a pipe (`... | tail`) the caller sees the pipe's status, 0, which is the "exited 0 with nothing installed" report. A full install then took 86 s, most of it Chromium (114.7 MiB headless shell plus fonts). After it, `bin/gstack-doctor` reported `7 ok, 2 warn, 3 not configured, 0 fail` and exited 0 with Bun 1.3.14 against the repo's tested 1.4.2 (`warn`, not `fail`), no Node row, no telemetry row. +- **What exists already is more than the feedback assumes.** `bin/gstack-doctor` (456 lines) has one `check_*` row per component with a fix line and exits 1 on `fail` (bin/gstack-doctor:3-16, :455-456). `bin/gstack-session-kind` already classifies `spawned | headless | interactive` (`GSTACK_HEADLESS`, `CI`, `GSTACK_SESSION_KIND=spawned`), and the preamble's AskUserQuestion rules branch on it (scripts/resolvers/preamble/generate-ask-user-format.ts:15-29). The snapshot tool wraps a raw plan in `## Implementation plan` automatically (bin/gstack-autoplan-snapshot.ts:415), so no literal heading is required of the user. `gstack-wtree` and `gstack-evidence` already compute reviewed-tree fingerprints (bin/gstack-evidence:13, :392). `bin/gstack-next-version` already reads the live PR queue and sibling worktrees (bin/gstack-next-version:80-105). `ship/sections/measure.md.tmpl:3-16` already holds a never-loosen rerun discipline. `codex_reviews` defaults to `enabled` (bin/gstack-config:251), so the outside voice is on by default. +- **Where the gaps actually are.** `headless` means BLOCK on any question (generate-ask-user-format.ts:28); only `spawned` auto-continues, and `spawned` would auto-approve autoplan's final gate. Upgrade, telemetry, lake and proactive prompts are suppressed for `spawned` only (bin/gstack-skill-start:394). Review-log `status`/`unresolved` are model-written free text; nothing derives them from findings (bin/gstack-review-log:92-97, lib/review-evidence.ts:226-243). `bin/gstack-version-bump write` stamps VERSION, package.json and npm lockfiles only, with one hard-coded post-write hook (`scripts/gen-agents-digest.ts`, :374-402); there is no `release:restamp` hook, no `--after `, no tree-equality receipt in the PR body, and no CHANGELOG re-heading tool. Paid evals in `/ship` (Step 6) run before Step 16's cheap checks. `/investigate` has no flake, polluter, race or backlog content (investigate/SKILL.md.tmpl, 278 lines). No user-facing eval workflow exists; preregistration, panels, admission budgets and dev/held-out splits are all gstack-internal (scripts/ship-measure.ts:22-28, scripts/judge-calibration.ts:42, test/helpers/periodic-exclude-data.ts:79-90). Codex outside voice from an API key works today (`printenv OPENAI_API_KEY | codex login --with-api-key` then `gstack-codex-probe role-ready exec` printed `CODEX_SEL: gpt-6-astra`, `AUTH_OK`, `MODEL_OK` in 3.7 s) but no helper, doctor fix line or doc names it (grep: only TODOS.md:1640 and CI tests). +- **Owner input received during review (binding).** Garry's second message adds: accept a subagent on another model family as a full outside voice and never report a missing Codex CLI as degraded; store learnings and telemetry durably on ephemeral machines or say plainly they are skipped; allow guarded tool calls when no publication guard is installed and say so; list the linked issue's deferred asks in the gate template; name the ship mode `--queue-mode stamp-at-merge`. These are folded into A3, B1, B3, C2 and D1 below. +- **Repo constraints that shape every item.** SKILL.md is generated from `.tmpl` (edit templates, run `bun run gen:skill-docs`). New logic goes in `lib/*.ts` behind thin `bin/` entrypoints; `test/module-size-ratchet.test.ts` caps *registered* owner modules at 800 lines and 150 per function (test/helpers/module-size.ts:202), so every new module this wave adds is registered in `test/fixtures/module-size-ratchet.json`, and `bin/gstack-next-version` (822 lines), `bin/gstack-autoplan-snapshot.ts` (865) and `setup` (4662), unregistered today, are not grown. Four skills have no parity headroom at all (ship 1.539/1.539, review 1.234/1.2345, autoplan 1.129/1.1305, qa 1.127/1.1272 union-byte ratios against `test/helpers/parity-harness.ts` guards, measured during this review), so prose added to them must be replaced byte for byte or printed by a bin instead. State paths resolve only through `lib/state-root.ts` / `bin/gstack-paths`. Prose in templates cannot be pinned by sentence (docs/test-value-bar.md); new rows get structural tests plus eval cases. Every template edit must clear `bun test test/parity-suite.test.ts` and the context-budget ratchets. + +The wave has one thesis: **gstack must be a tool a parent agent can drive and verify without reading prose.** Every artifact below is machine-readable first (one grep-able line, a JSONL file, a fenced receipt) and human-readable second. + +### Release promises (the finish line) + +- **P1. The install check cannot lie.** On a machine with nothing installed, one command prints one PASS/FAIL line per component with the exact fix command, ends with `gstack: fail ` and exits non-zero. On a healthy machine it ends with `gstack: ok `. A parent agent confirms a subagent machine by grepping that one line. Planning-only machines skip Chromium. +- **P2. /autoplan, the plan reviews and /cso run unattended on a host with no Claude Code harness.** Under the new `unattended` session kind they need no hooks, no AskUserQuestion, no Skill or Agent tool, prompt for nothing, and write `plan.md`, `review-record.md`, `tasks.jsonl`, `decisions.jsonl`, `findings.jsonl`, `timing.json` and a `run.json` manifest, all under one versioned schema with a validator, that a parent agent consumes without reading prose. The approval gate is never auto-approved: it is written as a gate list and the run ends. +- **P3. "Ship, next in queue at " works.** The stamp happens when the PR is next, from main's VERSION at that moment, through the repo's own release tool when it has one, across every stamp location at once. The PR carries a tree-equality receipt and a handoff record with identifiers first. Zero manual stamp edits; a pure restamp of a gated tree is reported as `gate-reuse: eligible` with the identity it rests on (tree modulo stamp and generated paths, runtime pins, lockfile, CI config), and the coordinator reuses the gate instead of paying for it again. +- **P4. Cheap checks run before any paid or remote gate.** A two-minute pre-gate replays every mechanical-drift incident in the feedback (stale goldens, scratch files, pipe-hidden failures, unrun platform lanes, old constants in nightly-only tests, dropped vendored hunks) and refuses to start a gate while any fails. +- **P5. Plans, reviews and investigations carry the gbrain lessons as required rows,** each one tied to the incident that earned it, each failing the review when missing. +- **P6. Measurement, coordination and the owner's view get first-class support:** a preregistered-eval workflow with a budget ledger, lane-ownership and evidence-bundle tools, and a one-page owner brief with numbered decisions. +- **P7. Adoption is measured, not assumed.** The durable carriers are the ones that already leave the machine: `run.json.consumed_by` in the parent-chosen `--out` directory and the ship receipt's `session_kind` and `artifacts_consumed: /` fields in merged PR bodies; `gstack-ship-receipt census --repo --since ` computes the number from merged PRs through `gh`, deduplicated by run and thread id, against an owner-maintained thread cohort (threads that never invoke gstack count in the denominator); state-root analytics stay a bonus labeled `durable=no`. The tier-3 items in PRs D and E start only after the first ten Capy threads on B-runner show at least six unattended runs whose artifacts a parent consumed (see UC1). + +### Delivery shape + +Seven pull requests, merged in order; each is shippable alone and each later one assumes the earlier one merged. Every item below carries a tier: **tier 1** ships in its PR; **tier 2** ships in the same PR only if green and otherwise moves to the next PR without blocking; **tier 3** waits for the P7 adoption number. The Eng review sized B and C at two review sittings each (3,500–6,000 LOC `[estimated]`), so each is split at its natural seam; the halves can be built in parallel on fresh machines, stacked. Queue cost: six extra full gates over a single PR, two of them made cheaper by the pre-gate they ship. The stack dogfoods itself: C-stamp's restamp tool stamps every PR behind it. (See decisions D1, D12 and UC1.) + +| PR | Items | Theme | Depends on | +|---|---|---|---| +| A | A1–A3 | Honest install (small, lands first) | — | +| B-core | B0, B1, B7, B9, B10, B12 | Artifact contract, `unattended` kind, gate list, derived review log | A (`GSTACK_LAUNCH`) | +| B-runner | B2, B3, B4, B8, B11 | Unattended runner, outside-voice runner, checklists, spend ledger | B-core | +| C-stamp | C1, C2, C3, C5, C7 | Ship policy, restamp, receipt, handoff, history policy | B-core (schemas) | +| C-pregate | C4, C6, C8 | Regen registry, issue links, pre-gate | C-stamp (trust boundary) | +| D | D1–D3 | Plan rows, sideways review, investigate recipes | B-runner (tier 2 rows need `scope`) | +| E | E1–E3 | `/eval-plan`, coordination tools, owner brief | B-runner (ledger), C-stamp (bundle) | + +Every PR's acceptance lists the union byte delta per skill it touches and the `CARVE_GUARDS`/`capture-context-budget` refresh commit, because ship, review, autoplan and qa have zero parity headroom today. Shared files have one owner each: `bin/gstack-skill-start` (B-core), `scripts/resolvers/outside-voice*.ts` (B-runner), `ship/SKILL.md.tmpl` and `ship/sections/*` (C-stamp, then C-pregate), the ratchet fixture (one commit per PR by that PR's author). + +### PR A: honest install (P0-1) + +**A1. `gstack-doctor --check`: the one-line verdict.** Add a `--check` mode to `bin/gstack-doctor` that prints one line per component as `PASS ` or `FAIL — fix: ` and ends with `gstack: ok ` or `gstack: fail `, exit 1 on any FAIL, exit 0 only when every required component passes. Components: Claude skills, Codex skills, the Astra behavioral patch (grep of the generated patch heading, as the Capy installer does today), browse bundle, Chromium launch and render (reuse setup's launch probe plus a `setContent` smoke render through `bin/gstack-render.ts`), runtime pins, privacy state. Readiness is relative to a requested workflow: `--for ` (or `--require `) names the components that skill needs (planning skills: both host profiles and the patch; browser skills: plus the browse bundle and Chromium; `/cso`: plus the native helper) and the trailer names the workflow it certified (`gstack: ok 1.91.70.0 for=autoplan`). Without either flag the required set is the installed hosts, and the trailer says so. Wrong-host, missing-authentication and incompatible-runtime fixtures are tested, not just missing files. One component table feeds the row ids, `--for` resolution, `--require`, the trailer and the `--json` keys, so a parent never maps between vocabularies. The doctor stays Bash 3.2 and reports without Bun (bin/gstack-doctor:22; the P1 scenario is a machine with Bun absent or below floor), so the table's source of truth is `lib/doctor-components.ts` and the file the doctor sources, `bin/gstack-doctor-components.sh`, is generated from it at build time with a freshness test, the way the agents digest is. `--check` is fixtured with `bun` removed from `PATH`, with an empty installed-host set (never `ok`), and with an unmet `--revision` (never `ok`). The `gstack runtime` row distinguishes gstack's security floor (1.3.3) from its supported minimum (1.4.2, bin/gstack-bun-version.sh:15); 1.3.14 is FAIL in `--check`. `--for ` prints `required: claude codex patch` before the rows, and each row is `PASS | FAIL | SKIP ` where SKIP marks a component the selected workflow, host or runner does not need (`not_applicable`) or an optional one that is absent (`optional`). A healthy Capy machine with Claude subagents as the outside voice and no Codex CLI passes `--for autoplan`. `--json` emits the same rows as objects; `--check` never runs the paid Codex probe unless `--live` is also passed. Silent success becomes impossible: every path prints the trailer. `./setup --status` exits 1 when any install row reports broken section links instead of always 0 (setup:136-146). +- New rows: `gstack runtime` (Bun against gstack's own floor and tested version, today's `bun` row) and `project pins`, which reads the *target repository's* (named by `--project `, default the git toplevel of the cwd, never the gstack checkout) `packageManager`, `engines`, `.tool-versions`, `.nvmrc`, `.bun-version` and the CI matrix (`bun-version:` / `node-version:` in `.github/workflows/*.yml`) through new `lib/runtime-pins.ts`, compares with the running Bun and Node, distinguishes a range (`>=1.4.2`) from an exact pin, keeps platform-specific CI pins with their lane (the Windows workflow pins Bun and Node separately, .github/workflows/windows-free-tests.yml:83, and must not block a Linux run), prints each effective constraint with its source, and reports `FAIL project pins — bun 1.3.14 outside engines.bun >=1.4.2 (package.json:80; CI windows-free-tests.yml:89 pins 1.4.2); fix: bun upgrade --version 1.4.2`. A mismatch against the project's pin is FAIL in `--check` (a gate on an unpinned runtime is the incident), `warn` in the dashboard. `privacy` prints `telemetry= artifacts_sync= codex_reviews= update_check=` from `gstack-config get`, values only (doctor's clamp rule). `cores` prints the CPU count and, under 8 cores, the remote-gate recommendation (consumed by E2). +- Tests: `test/gstack-doctor.test.ts` gains the `--check` fixtures (healthy, each missing component, runtime mismatch), pinned on the trailer tokens and exit codes, not prose. + +**A2. The Capy installer lives in the repo.** New `bin/gstack-capy-install` (bash, Capy cloud Ubuntu only, under 200 lines): clone or reuse `~/.local/share/gstack` under the existing lock (never reset, never auto-pulled), `./setup --host claude --no-prefix`, `./setup --host codex --model gpt-6-astra`, then `gstack-doctor --check --for ` (default `--require claude,codex,patch`). Chromium is lazy by default: the installer skips it (`GSTACK_SKIP_PLAYWRIGHT=1 GSTACK_SKIP_FONTS=1`), new `bin/gstack-browser-ensure` installs it on first use from the browse preflight in `scripts/resolvers/browse.ts`, and `--with-browser` installs it up front for machines that want the 86 s now; `--only ` installs the host profiles and checks only that skill's declared dependencies. `./setup` gains the documented `--no-browser` flag (today only the undocumented env var, setup:2072). Runtime preflight runs before `./setup`, because `setup` exits at once when Bun is absent or below its floor (setup:296): the installer checks Bun, Node and `jq`, installs or upgrades Bun through `setup`'s own checksum-verified recipe (`BUN_VERSION= bash install`, setup:296-305; `bun upgrade --version` does not exist on 1.3.14) when gstack's supported minimum or the project's pin requires it and the two agree, and stops with both constraints printed when they conflict. The default Capy image ships 1.3.14, so P1 is unmeetable there otherwise. `--revision ` pins the checkout for a fleet; an existing checkout at a different revision is reused and reported as `gstack: ok (requested ; run gstack-upgrade to move)`, never pulled. Chromium skip precedence is one sentence in `setup --help`: `--no-browser` sets `GSTACK_SKIP_PLAYWRIGHT=1` internally; the installer passes it unless `--with-browser`. Planning-only install time is measured separately from browser bootstrap and printed. The user-scope Capy skill becomes a thin wrapper: clone or reuse the repo under the same lock, then run `bin/gstack-capy-install` from the clone (no raw-content URL), and grep `gstack: ok`. Its SKILL.md tells a parent agent to confirm each subagent machine with that grep and never to pipe `--check` through `tail`. +- Acceptance: a fresh machine with nothing installed fails loudly, naming each missing component; the default install finishes without downloading Chromium and reports `SKIP browser (lazy; gstack-browser-ensure installs on first use)` rather than PASS; the first browser skill run installs it. + +**A3. Durable state on ephemeral machines.** The state-root chain already exists (`GSTACK_STATE_ROOT` > `GSTACK_HOME` > `GSTACK_STATE_DIR` > `CLAUDE_PLUGIN_DATA` > `~/.gstack`, lib/state-root.ts), so learnings, decisions, review logs and timing are a defaults-and-honesty fix: `gstack-skill-start` prints `STATE_ROOT: durable=` once per session, detecting an ephemeral root (a Capy cloud home, a tmpfs, or `GSTACK_EPHEMERAL=1`), and in unattended runs prints `learnings: skipped (state root is ephemeral; set GSTACK_STATE_ROOT)` instead of writing silently to a disk that will vanish. Ephemerality is detected from an environment marker the host sets (`GSTACK_EPHEMERAL=1`, which the Capy installer exports into the skill's launch configuration), not a path heuristic. A durable root is opt-in: the owner sets `GSTACK_STATE_ROOT` (the Capy drive is governed by Capy's own drive policy, which excludes memory and verification evidence, so the installer only prints the option and never writes there by default). Because a child shell's export cannot configure the parent's later commands (lib/state-root.ts:22 reads each process's own environment), the installer prints a machine-readable launch configuration (`GSTACK_LAUNCH: GSTACK_SESSION_KIND=unattended GSTACK_STATE_ROOT= GSTACK_EPHEMERAL=1`) that the parent supplies to every gstack command it launches; the doctor `state root` row shows the durability verdict. + +### PR B (B-core, B-runner): unattended autoplan and the artifact contract (P0-2) + +Item ids are stable across review phases, so B5 and B6 (folded into B2 and B0 during the CEO phase) are not reused. + +**B0. One versioned artifact contract.** New `lib/headless-artifacts.ts` owns every schema this wave writes (`tasks`, `decisions`, `findings`, `timing`, `run` manifest, `pregate`, `ship-receipt`), each with `schema_version`, exported TypeScript types, and `bin/gstack-artifact validate `, which `phase-close`, `gstack-ship-receipt` and `gstack-pregate` call before writing. `run.json` is the manifest: required phases, each reviewer's outcome and model, input hashes, decision and finding counts, artifact paths and hashes; `gstack-artifact validate run.json` is the deterministic completion check a parent runs first, and it fails on an interrupted run, a missing reviewer, a stale artifact or a findings total that disagrees with the file. `findings.jsonl` rows share the review-log row shape so B7's derivation is one function over one type, and every finding carries a run-bound id (`--F`) that decisions, tasks and the URGENT block reference. `tasks.jsonl` rows carry run-bound ids, `depends_on`, `acceptance` (the command or check that proves the task), `findings` (ids) and `blocked_by` (decision ids); `validate` checks referential integrity across the files and binds each reviewer's immutable output file (path and hash) into `run.json`, so a count that merely matches cannot hide an omitted finding. One transport contract for every new command: `--json` means JSON only on stdout and diagnostics on stderr; without it, one terminal line `GSTACK_RESULT: skill= status= run=` where the run directory holds fixed file names; a shared exit-code table (0 ok, 1 fail, 2 usage, 3 refused or needs a flag) printed in every new bin's `--help`, with a `--help` coverage test; every error line carries a reason code with a `docs/troubleshooting.md` anchor and a `fix:` clause from a new sibling table `lib/result-codes.ts` (same `{anchor, summary, fix}` shape as `lib/gate-outcomes.ts`, whose `state` enum is for gate outcomes only; `test/troubleshooting-anchors.test.ts` covers both tables; the artifact errors are an enumerated list, not a wildcard): `GUARD_NOT_INSTALLED`, `REPO_COMMANDS_NOT_AUTHORIZED`, `POLICY_SOURCE_UNAVAILABLE`, `RESTAMP_PREDECESSOR_MOVED`, `GATE_REUSE_NOT_ELIGIBLE`, `QUEUE_STALE`, `REVIEW_STATUS_MISMATCH`, `EXECUTION_UNKNOWN`, and the `ARTIFACT_*` list. The terminal-line rule applies to workflow commands (`run`, `next`, `submit`, `answer`, `restamp`, `pregate`); pure query commands (`schema`, `validate --json`, the doctor trailer) keep their own documented output. Every reviewer returns canonical structured findings (the `findings.jsonl` row shape) beside its prose, and the aggregate reconciles their exact ids, keeping disagreements and resolutions as separate fields; a hash of the prose proves which file was kept, not completeness, so the reconciliation is the completeness check. `validate` also rejects duplicate ids, dependency cycles, malformed JSONL tails, and artifact paths that escape the run directory (symlinks included). Formats are versioned from day one with every reader and writer enumerated (`tasks` v2 adds `depends_on`, `acceptance`, `findings`, `blocked_by` over today's `source_finding` rows, scripts/resolvers/tasks-section.ts:77; v1 rows still aggregate), and `bindReview` validates a `--findings` file before it consumes the start token (lib/review-evidence.ts:188). `gstack-artifact schema ` prints the JSON Schema for the version it validates, so a parent on another machine can read the contract; `gstack-artifact ack --consumer ` is the idempotent consumption report behind P7 (it sets `run.json.consumed_by` and appends to `analytics/autoplan-timing.jsonl`); `validate --json` returns `{valid, errors:[{code, path, message}]}`. + +**B1. A real unattended session kind.** A fourth kind, `unattended`, honored only as the explicit `GSTACK_SESSION_KIND=unattended` override (bin/gstack-session-kind:17-23). `headless` (set by `GSTACK_HEADLESS`, `CI`, `GITHUB_ACTIONS`, and by the eval harness) keeps today's BLOCK semantics, so no paid eval or E2E case changes meaning. `unattended` gets its own semantics in the preamble and `gstack-skill-start`: +- `unattended` is added to the kind whitelist in `gstack-skill-start` (bin/gstack-skill-start:93 coerces any unknown kind to `interactive`, the inverse of the intent), an unknown kind prints `SESSION_KIND: interactive (unknown kind '')` so a mixed-version fleet is loud, the explicit override outranks `GSTACK_HEADLESS`, and the eval harness never sets it. +- No prompts of any kind: update check, upgrade offer, lake intro, telemetry, proactive and routing blocks are suppressed as they are for `spawned` (bin/gstack-skill-start:96, :394). Privacy is enforced at the writers and sync entrypoints, not only at the prompts: the analytics and artifact-sync calls that run outside the onboarding guard (bin/gstack-skill-start:179, :310) and the review-log sync enqueue (bin/gstack-review-log:99) all check the session kind and skip with an explicit line, inherited enabled settings and pending queue work included; the test instruments the egress sinks and asserts no subprocess or network call, not merely the absence of a prompt. The upgrade offer becomes one line at skill end (`UPGRADE_AVAILABLE: ; run /gstack-upgrade`). An unanswered consent is `off`; the preamble never records consent in headless. +- Questions: a `(recommended)` option is taken and the decision appended to `decisions.jsonl` with `kind: "auto"`; a question with no recommendation, a consent, a destructive option or an approval gate is appended with `kind: "gate"` and `status: "pending"`, and the skill stops at its next gate with the pending list printed. Unattended never approves and never publishes or syncs data. `spawned` and `headless` keep today's behavior. +- The Capy adaptation text in the preamble (no Skill tool, no slash commands, plain text is not delivered) moves from the user-scope skill into a `{{HOST_ADAPTATION:unattended}}` resolver so every host's render carries it. +- The autoplan frontmatter `PreToolUse` hooks deny every matched call with `Autoplan publication guard is unavailable` when `autoplan/bin/phase-publication-hook` is absent. A host that honors the frontmatter but lacks the file is blocked outright. The hook renders only for the Claude host (scripts/resolvers/composition.ts:11) and on Capy it is present but never executed, so "file absent" is the wrong condition: on Claude Code, deny-on-absent stays (a broken install must not pass as an absent one, autoplan/bin/phase-publication-hook:4-9); on every host that does not execute hooks, and whenever `SESSION_KIND=unattended`, `gstack-skill-start` prints `autoplan guard: not enforced by this host; publication order is unverified (GUARD_NOT_INSTALLED)` once, and B0's validator requires that line in `review-record.md` for an unattended run. That satisfies the owner's ask (guarded calls proceed and the run says so) without weakening the one host that enforces the guard. +- Tests: `test/gstack-session-kind.test.ts`, `test/gstack-skill-start.test.ts`, `test/spawned-consent-rule.test.ts` gain the unattended cases; negative controls prove unattended never emits the telemetry prompt and that `GSTACK_HEADLESS` still blocks. + +**B2. autoplan unattended profile and the output files.** A new lazy section `autoplan/sections/unattended.md.tmpl` (loaded when `SESSION_KIND: unattended`) and a `bun gstack-autoplan-snapshot export` step implemented in new `lib/autoplan-export.ts` (the snapshot bin stays at 865 lines), writing through B0's schemas. Export writes, beside `ACTIVE_PLAN`: `plan.md` (the `## Implementation plan` section only; warn above 30 KB), `review-record.md` (the `## Review record` section and decision audit trail), `tasks.jsonl` (the aggregated per-phase tasks, same schema as today's `tasks--*.jsonl`), `decisions.jsonl` (every auto, taste, user-challenge and gate item with `id`, `phase`, `kind`, `recommended`, `options`, `cost`, `status`), `findings.jsonl` (every finding with `phase`, `voice`, `model`, `severity`, `file`, `line`, `title`, `resolution`), `timing.json` (per-phase start/end, outside-voice wall time, total; see B10) and `run.json`. One executable owns the unattended loop as an explicit, durable state machine: `bin/gstack-autoplan` (thin, over `lib/autoplan-run.ts`). On a host where the parent dispatches the native reviewer itself (Capy: the native voice is a subagent the parent starts from the prompt file, exactly how this review ran), the loop is `next` and `submit`: `next --out ` prepares the current phase (snapshot, methodology, prompt), persists the dispatch intent with an attempt id, prints the voice, the prompt path, the expected result path and the model family required, and exits with `GSTACK_RESULT: status=awaiting_result`; `submit --out --phase

--voice native|outside --result --model --attempt ` validates the result (full-read receipt, canonical findings, model family), binds its hash into `run.json` atomically, runs the B11 reconciliation and `phase-close`, and advances. `run --plan --out

[--runner ...] [--outside-model ] [--deadline ] [--spend-cap ] [--plan-heading ]` is `next` in a loop and exists only when a runner can execute both voices itself. `status --out ` prints the manifest. `answer --out --gate-rev --reply ""` rejects a stale, conflicting or partially parsed reply atomically with no writes; applies the choices; and when a non-recommended choice changes reviewed content (scope, architecture, test plan), recomputes the affected review-input identities and reopens the dependent phases with Eng last, the same rule today's option D follows, instead of finalizing a plan no reviewer evaluated. Only a reply that changes nothing reviewed finalizes `plan.md` and `run.json.status` without another paid phase. `resume --out ` takes exclusive ownership of the run (a second writer is refused), reconciles any attempt whose dispatch intent exists without a terminal record as `EXECUTION_UNKNOWN` (the provider may have finished and charged; snapshot hashes prove input identity, not completion), and never redispatches an ambiguous attempt automatically. `--deadline` cancels every child the runner started; spend by a host-dispatched subagent is reported as `unknown` in the ledger, not as zero. The minimal shared spend ledger (`lib/spend-ledger.ts`: atomic `spent + reserved + next_estimate <= cap` under a file lock, reservations retained for unsettled attempts after a crash) lands here, not in PR E, because `--spend-cap` needs it. `--out` must be a directory the parent chose (durable or not, reported per artifact class); nothing is written beside `ACTIVE_PLAN` by default. The final approval gate in unattended runs is written as a gate list (B9) into `decisions.jsonl` and printed; the run ends with `GSTACK_RESULT: skill=autoplan status=gate_pending run=`. Lanes read `tasks.jsonl`, not the plan. `docs/unattended.md` is the parent-agent quickstart: install, run, validate, answer, ack, five copy-paste commands, linked from the user-scope Capy skill. Each run also appends `session_kind` and, when the parent reports it, `consumer` to `analytics/autoplan-timing.jsonl` (P7). +- `cso` in unattended runs prints `GSTACK_RESULT: skill=cso status=<...> run=` at exit (the run directory holds `findings.jsonl` and the run id), and `gstack-doctor --check` lists the CSO native helper as an optional component (`SKIP cso (toolchain absent)`, never silent). +- A plan with any layout is accepted (already true: snapshot `init` wraps raw text; document it in the headless section) and `--plan-heading ` names a custom heading when a repo's plans use one. +- Acceptance: this plan, reviewed on this machine with no Claude Code harness, produces the files; a parent agent runs `gstack-artifact validate run.json`, lists pending decisions with `jq 'select(.status=="pending")' decisions.jsonl`, and never reads the record. Interrupted-run, missing-reviewer and omitted-finding fixtures fail validation; `test/autoplan-run-recovery.test.ts` kills the runner before and after result persistence and asserts no automatic duplicate charge; `test/autoplan-run-concurrency.test.ts` covers two writers, revision conflicts and exactly-once `ack`; `test/autoplan-answer-invalidation.test.ts` proves a scope-changing answer reopens only the affected phases and a stale reply writes nothing. New bash bins and their tests are classified for the Windows subset (`scripts/test-free-shards.ts --windows-only`). + +**B3. A pluggable outside voice, with a model parameter.** The outside voice becomes a runtime choice, not a render-time one: `outsideVoiceFor(ctx)` picks the provider when `gen:skill-docs` renders (scripts/resolvers/outside-voice.ts:8-12) and the rendered prose shells out to `codex` directly (scripts/resolvers/outside-voice-steps.ts:177), so a per-run `--runner` cannot live there without rendering three branches into every zero-headroom skill. New `bin/gstack-outside-voice run --runner --prompt --out [--model ] [--result ]` over `lib/outside-voice-runner.ts`; the resolver names the bin and the runner table once. Every runner implements one adapter interface: declared capabilities (repository access or supplied-input only), input digest, actual model metadata, cancellation, terminal result and usage; the `api` runner either runs a bounded repository tool loop or is labeled `supplied-input review` in the record, never silently equal to a repo-reading review. Runners: `codex-cli` (today's path), `api` (a direct call with `OPENAI_API_KEY` or `ANTHROPIC_API_KEY` through new `lib/outside-voice-api.ts`, model from `--outside-model`), or `host-subagent` (the host runs a subagent on another model family from the paste-ready prompt and returns its output file; the parent passes `--outside-result --outside-model `). A completed `host-subagent` or `api` review is a full outside voice: `OUTSIDE_STATUS: completed provider= model=`, never `[single-model]` or degraded, and the model id is recorded in `findings.jsonl` and the voices log row. A missing Codex CLI is reported as `codex-cli unavailable; using ` only when another runner is configured; with none, today's unavailable text stands. New `bin/gstack-codex-login`: `printenv OPENAI_API_KEY | codex login --with-api-key`, then `codex login status`, exit non-zero with the probe's hint on failure; `outsideVoiceFailurePolicy()` (outside-voice.ts:217) and the doctor codex row (bin/gstack-doctor:186) name it as the unattended remedy. `--outside-model ` sets `GSTACK_CODEX_MODEL` for the CLI runner (bin/gstack-codex-probe:144-147 already honors it). The outside voice stays on by default (`codex_reviews: enabled`). The same-family rule holds for every runner: the outside model must differ in family from the native reviewer, and the record says which was which. Runner selection is decoupled from host identity (today `outsideVoiceFor` picks the provider from the harness, scripts/resolvers/outside-voice.ts:8-12), so a Codex-host render can still receive an outside voice from a Claude subagent and a Capy run can use either. The Capy dispatch path (a Capy subagent reading the native prompt file with its own file-reading tool) is the one this plan's own review ran on; the PR body records it. + +**B4. Condensed operative checklists.** Each review skill gets `sections/checklist.md.tmpl`, at most 200 lines, holding the executable steps and required outputs of that phase; the deep methodology stays in `review-sections.md`. In unattended runs and under an explicit `light` mode the phase loads the checklist plus only the deep sections the detected scope needs (the manifest gains a `scope` field per section: `always`, `ui`, `dx`, `db`, `perf`, `security`, `incident`), and prints one index line: `Skipped sections: (scope: )`. Interactive runs are unchanged. `nativeDispatchPrompt` already exists in `snapshot.json`; `bun gstack-autoplan-snapshot prompt ` prints it so a parent can paste it to a subagent. The manifest's `scope` field is optional with `always` as the default, its "passive, no machine predicate" note is updated with the schema, and the only consumer is the runner (interactive renders ignore it; `test/qa-lazy-sections.test.ts:127` keeps asserting passive resolution for them). A checklist is "replace, never add": the methodology bytes it summarizes leave the eager text, and an obligation-to-section map (every required output names the section that produces it) proves nothing required was dropped, with a conservative fallback that loads the deep section whenever scope detection is uncertain and behavioral negative controls for omitted obligations. A test caps every checklist at 200 lines and checks the obligation map, not just the section count. + +**B7. Review-log status derived from findings.** `reviewFreshness` already downgrades a `clean` row with open issues to UNVERIFIED at read time (lib/review-evidence.ts:238-240); the gap is at write time. `bindReview` (lib/review-evidence.ts:178-186, the single choke point) accepts `--findings `: it derives `status` (`clean` when no finding is open, else `issues_open`), `unresolved`, `issues_found` and `critical_gaps` from the file and rejects a row whose claimed values disagree. The row carries separate `findings_total`, `findings_open` and `findings_resolved`; `reviewFreshness` (lib/review-evidence.ts:238) is updated in the same change to read `findings_open`, since today it rejects any row with `issues_found > 0` even when every finding is resolved. A rejected row exits 1 with `review-log: status mismatch: claimed clean, findings.jsonl has 176 open (REVIEW_STATUS_MISMATCH)` on stderr (the wrapper drops the `2>/dev/null` at bin/gstack-review-log:95 and stops converting binding failures into `invalid JSON`); usage becomes 2 (today 1, :81-84); the legacy `critical` and Codex counters keep their conservative reading. Rows written without `--findings` are tagged `status_source: claimed`. Unattended autoplan always logs through `--findings`. Phase subagents that cannot write the JSONL return findings to the parent, who writes `findings.jsonl` and logs from it; the hand-written `status: clean` beside `issues_found: 176` is rejected at write time. + +**B8. Spec-review loop at the cap.** The loop stops today on PASS, repeated issues or the third launch, and a missing score alone does not force another launch (scripts/resolvers/spec-review.ts:182-192); the 1-10 score is reported but not a stop rule. At the cap it now prints the unresolved gaps as a numbered list and takes one of two recorded paths, never a resample of unchanged input: a deterministic issue-verification pass that checks only the listed issues after their fixes are applied and is recorded as `issues verified: k of N`, never promoted to a full-review verdict and never a fourth model launch past the cap (unattended default), or `N fixes unconfirmed` with the approval gate blocked by a `spec not re-verified` decision. An interactive user may raise `--max-rounds`, and each extra launch requires a changed input hash. `DONE_WITH_CONCERNS` always names the missing points next to the score. + +**B9. One gate list instead of four-option sequences.** New `lib/gate-list.ts` with `bin/gstack-gate` (`render`, `parse`): a gate list is numbered items each with a recommended default and lettered options, and one reply grammar: `all` (every item takes its recommendation), or `