Files
gstack/test/fixtures/multi-agent-wave/reference-run/dx-native.md
Garry Tanandcapy-ai[bot] 45a678323b v1.91.70.0 feat(unattended): artifact contract + validator, unattended session kind, findings-derived review log, gate list (multi-agent wave B-core) (#3115)
* v1.91.70.0 feat(unattended): artifact contract, unattended session kind, findings-derived review log, gate list

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: register test/fixtures/multi-agent-wave as a free fixture (consumer: headless-artifacts.test.ts)

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>
2026-10-10 23:26:08 +00:00

166 lines
34 KiB
Markdown

INPUT: dx ffe831074afb86b6b394d6b0e0383034883c94a61dbce855e29b0738e5f11753
Read: `/workspace/gstack/.gstack/tmp/autoplan/autoplan-dx-U0huJT/native-prompt.md`, lines 1-229, SHA-256 `482ebf2c…e83fe` (verified with `sha256sum`); the embedded implementation plan hashes to `ffe83107…f11753` (verified against `dx-implementation.md`). Repo at `origin/main 91bbd9e`, VERSION `1.91.69.0`. Every claim below marked **confirmed** or **refuted** was checked against a file:line on this checkout. The dispatch asked me to ask questions or stop at the gate; I auto-decided every one with the recommended option and record that in Findings and Tasks.
## Step 0
### Persona A: parent Capy agent installing gstack on a subagent machine, running autoplan unattended, consuming the outputs
**Who they are.** A Capy thread's Capy, running a Claude or GPT model on a cloud Ubuntu box. No Claude Code harness, no Skill tool, no AskUserQuestion, no hooks. Reads files with a native tool, runs bash, and spends the owner's money by the token. It judges a tool by one question: *can I grep the answer?*
**What they do today (confirmed).** They run `scripts/install.sh --check` from the user-scope drive skill. `check_install` is a bare `test -x`/`test -s`/`grep -Fq` chain (`~/.capy/drive/user-garry-tan/skills/gstack/scripts/install.sh:7-14`) under `set -euo pipefail` (`:2`), invoked as `check_install; exit 0` (`:32-35`). A failed probe kills the script with status 1 and *no output*; the only success text is printed from inside the Playwright smoke (`:24`). Through `| tail` the parent sees exit 0 and nothing printed. The plan's P0-1 reproduction stands.
**Empathy narrative.** The parent asks "is gstack ready on machine 3?" and gets silence. Silence is the one thing an agent cannot act on: it isn't PASS, isn't FAIL, and its training says "no output from a check means fine." Then it tells a subagent to "load gstack and run autoplan," the subagent spends 20 minutes of model time improvising a review because the skills are not on disk, and the parent only learns this when the output lacks a `## Review record`. Thirteen of 28 threads did exactly that. The fix the parent wants is not a better doctor dashboard (the dashboard already exists: `bin/gstack-doctor:51-53` prints `%-15s %-15s %s` rows and exits 1 on fail, `:455-456`); it wants one grep-stable line it can put in a `test`.
**Developer journey trace (current), with friction points.**
1. Parent reads drive `SKILL.md`, runs `bash scripts/install.sh --check`. **F1:** silent exit 1, or exit 0 through a pipe. No row, no fix hint, no revision.
2. Runs the installer without flags: clone + `./setup --host claude --no-prefix` + `./setup --host codex --model gpt-6-astra` + `bunx playwright install --with-deps chromium` (`install.sh:43-50`). **F2:** 86 s, 115 MiB of Chromium, on a machine that will only plan. There is no flag to skip it; the only skip is the undocumented `GSTACK_SKIP_PLAYWRIGHT=1` on `setup` (`setup:2071`).
3. Runs `bin/gstack-doctor`. **F3:** 13 rows of prose-ish columns; "7 ok, 2 warn, 3 not configured, 0 fail", exit 0, with Bun 1.3.14 vs tested 1.4.2 as `warn`. The parent cannot tell whether a gate on this Bun is sound. No `--json` (only `--live` and `-h` are parsed, `bin/gstack-doctor:37-38`).
4. Dispatches a subagent: "read `~/.claude/skills/autoplan/SKILL.md` and run it on plan.md". **F4:** the preamble classifies the session as `interactive` (any ambiguity → interactive, `bin/gstack-session-kind:16-18`), so the skill calls AskUserQuestion, which does not exist; the fallback prose brief ends the subagent's turn. If the parent sets `GSTACK_HEADLESS=1`, the skill BLOCKs at the first question (`generate-ask-user-format.ts:28`). If it sets `GSTACK_SESSION_KIND=spawned`, the final approval gate is auto-approved (`:15`, exception only for destructive options).
5. The run ends with a prose record. **F5:** The parent has to read `ACTIVE_PLAN` to find decisions; `tasks-<phase>-*.jsonl` exist per phase but no aggregate, no manifest, no completion check.
6. **F6:** Learnings and decisions went to `~/.gstack` on the ephemeral box (`lib/state-root.ts:22-29` chain ends at `$HOME/.gstack`) and are gone at wake.
7. **F7:** Codex outside voice needs `codex login`; the plan shows it works from `OPENAI_API_KEY` in 3.7 s but nothing in doctor (`bin/gstack-doctor:186` says "codex login, or export CODEX_API_KEY") or `outsideVoiceFailurePolicy()` (`scripts/resolvers/outside-voice.ts:216`) names the API-key path.
**First-time roleplay (post-plan).** "`bin/gstack-capy-install --only autoplan` … `gstack: ok 1.91.70.0 for=autoplan`. Good. `GSTACK_SESSION_KIND=unattended`, run autoplan. … `AUTOPLAN_RESULT: gate_pending run=…/run.json decisions=…`. `gstack-artifact validate run.json` → exit 0. `jq 'select(.status=="pending")' decisions.jsonl` → 7 gates. Now what? I answer them… how? I re-run autoplan? Does it redo three review phases? There is no `resume`." That last question is the biggest hole in the plan (Finding 1).
**TTHW.** Current: zero to a trusted "ready" verdict on a fresh machine is unbounded (the check cannot be trusted), and zero to a consumed unattended review does not exist (needs the harness). Target per plan: ≈ 25 s to `gstack: ok … for=autoplan` without Chromium (clone + two `setup` renders), ≈ one run to `AUTOPLAN_RESULT`, and one `jq` to the gate list. Add the missing resume step and the full loop closes in two commands.
### Persona B: coordinator agent running "ship, next in queue at <version>" for three PRs that reorder twice
**Who they are.** A single thread merging one PR at a time into a repo with its own `release:restamp` script, a CHANGELOG, `(was X)` doc mentions, a mirror repo, and a full test gate that costs 40+ minutes on an Ubicloud VM. It decides order from CI state and risk, so order changes.
**Empathy narrative.** Today `/ship` Step 12 claims a version at PR-open time by reading the live PR queue and sibling worktrees (`bin/gstack-next-version:80-105`, `pickNextSlot` bumps past the highest claim). The moment PR 2 is merged before PR 1, PR 1's claim is wrong, its CHANGELOG heading is under the wrong version, every `(was X)` in docs is stale, and the mirror disagrees. The coordinator hand-edits five files in two repos, reruns the 40-minute gate because "the tree changed," and does it again on the next reorder. The feedback says they stopped using `/ship` and used the repo's own tool. What they want: "stamp when it's my turn, in one command, show me the tree is the same thing I already gated."
**Developer journey trace (plan's target), with friction points.**
1. Coordinator decides PR 3 is next. Runs `gstack-restamp --queue-mode stamp-at-merge --next` in PR 3's worktree. **F8:** `--next` is defined as "main's VERSION at this moment plus the policy bump, through the next-version queue reader" (plan C2). The queue reader's whole job is to read *claims* (`bin/gstack-next-version:80-105`). Under stamp-at-merge nobody claims, so either the reader is bypassed (then say so) or stale claims from pre-wave PRs bump the number unexpectedly. Unspecified.
2. `gstack-restamp` wants to run `bun run release:restamp` from `.gstack/ship-policy.json`. **F9:** standalone it exits 3 with `repo commands not executed (pass --allow-repo-commands)` (C1). Correct posture, but the coordinator, who is not inside `/ship`, has to learn one more flag, and the plan doesn't say whether the stamp of VERSION/CHANGELOG/`stamp_paths` still happens (partial stamp) or nothing happens. A half-stamped tree with the release tool skipped is the worst outcome.
3. Receipt: `gstack-tree-receipt` prints `TREE: same-modulo-stamps` and `gate-reuse: eligible`. **F10:** invocation unspecified. The receipt needs the gated head and the predecessor head; where does the coordinator get them, `gate-ahead.json`? `gstack-evidence`? The plan names outputs, not inputs.
4. Reorder #1: PR 1 now goes before PR 3. PR 3 needs `--after 1` (gate-ahead). **F11:** `--after` and `mirror` are **tier 2** (C2). The P3 promise ("works") is tier 1; the piece that makes reordering cheap can slide to the next PR. The coordinator's headline use case is then half-delivered.
5. Reorder #2: `PREDECESSOR MOVED <old> -> <new>` refusal. Good. **F12:** what does the coordinator do next? The line has no fix clause (criterion 3), while every doctor row has one (`bin/gstack-doctor:53`).
6. Merge. **F13:** the ship receipt (C5) is the first fenced block of the PR body, good, but has no `schema_version` field listed even though B0 says `ship-receipt` is in the contract, and there is no reader (`gstack-ship-receipt read --pr N`) for `/land-and-deploy` to consume it with; the plan says `/land-and-deploy` "reads it" without a command.
**TTHW.** Current: one reorder ≈ 20 minutes of hand edits plus a repeated 40-minute gate. Target: `gstack-restamp --after <pr>` + `gstack-tree-receipt` ≈ 2 minutes, zero manual edits, gate reused when the receipt says `eligible`. Achievable only if `--after` is tier 1 and the receipt's inputs are named.
## Passes 1-8
| # | Dimension | Pre | Post | The change that moves it |
|---|---|---|---|---|
| 1 | Onboarding / TTHW | 2 | 8 | A1+A2 as written, plus one copy-paste "parent agent quickstart" doc with the three commands (install, run, validate) and the resume command (Finding 1). Without resume the loop is open and the score is 6. |
| 2 | Discoverability | 3 | 7 | `gstack-doctor --for <skill>` prints *which* components it required (today the row names and the `--require` ids are different vocabularies). `bun gstack-autoplan-snapshot prompt <phase>` surfaces the existing `nativeDispatchPrompt` (`bin/gstack-autoplan-snapshot.ts:661`). Add `gstack-artifact schema <name>` so a parent can read the contract without the repo. |
| 3 | Error messages | 4 | 7 | Plan has fix clauses for doctor rows and `review-log: status mismatch`; missing for `PREDECESSOR MOVED`, `gate-reuse: not-eligible`, `repo commands not executed`, validator failures, hook `guard unavailable`. Repo convention is a reason code in `lib/gate-outcomes.ts` with a `docs/troubleshooting.md` anchor (`lib/gate-outcomes.ts:69-79`); route every new error family through it. |
| 4 | CLI / flag consistency | 3 | 7 | One component-id vocabulary; one verdict grammar (`PASS|FAIL|SKIP <id>` rows and a key=value trailer); `--for` *or* `--require`, not both; one exit-code table for the five new bins (exit 2 = usage everywhere, as doctor does at `:12`, `:39`); state `--queue-mode` default and whether it is required. |
| 5 | Machine-readability | 5 | 9 | B0 is the right shape. Add `--json` on `gstack-doctor --check` (planned), `gstack-artifact validate` (not planned), `gstack-tree-receipt`, `gstack-pregate` (planned), `gstack-gate parse` (planned); one terminal-status line grammar shared by autoplan and cso (`AUTOPLAN_RESULT:` vs `CSO_RUN:` today are two shapes). Enumerate `run.json.status`. |
| 6 | Docs / examples | 2 | 6 | The plan names fixtures and `INCIDENTS.md` but no user-facing doc for unattended runs, ship policy, or the receipt. Each pin file (`ship-policy.json`, `generated.json`, `pregate.json`, `banned-terms.json`) needs a copy-paste example and a `gstack-<tool> init` that writes it. |
| 7 | Owner gate experience | 4 | 8 | B9 gate list + E3 one-page brief + fixtures from real owner replies (`d10 yes`, `1a 2a`) is strong. The reply grammar is printed with the list. Gap: deferred asks and `URGENT` block come in different PRs (D1 tier 1 vs B12), so the brief is incomplete for one release. |
| 8 | Recoverability | 2 | 7 | Interrupted-run validation fails loudly (B0). Missing: resume from `gate_pending` with answers (Finding 1); resume of an interrupted phase without redoing finished phases (`run.json` has phase outcomes, so this is a flag away); a `--dry-run` on `gstack-restamp` is implied ("shown as a dry-run diff before writing") but not a flag. |
## Findings
**Finding 1 — Critical. Unattended autoplan has an exit (`gate_pending`) but no entry for the answer.** B2 ends the run with `AUTOPLAN_RESULT: gate_pending …` and D6 says the run never approves. B9 gives `gstack-gate parse` which turns `all except 3b` into JSON. Nothing in B2, B9, or E3 says how the parent feeds that JSON back and gets `plan.md` finalized ("every `draft direction stands until the owner decides` line is rewritten to the decided option", E3, is a phase-close operation with no trigger named). The parent's only option is to re-run autoplan, which redoes three paid phases. Evidence: the snapshot CLI has `init` (`bin/gstack-autoplan-snapshot.ts:833`) and the plan adds `export` and `prompt`; no `resume`/`answer`. Fix: `bun gstack-autoplan-snapshot answer --decisions decisions.jsonl --reply "<grammar>"` (or `--answers <json from gstack-gate parse>`) that validates ids against pending gates, appends `kind:"owner"` rows with `status:"decided"`, runs the Phase 4 close operations (rewrite draft lines, write final `plan.md`, update `run.json.status` to `complete`), and prints `AUTOPLAN_RESULT: complete …`. Document it as step 3 of the parent quickstart.
**Finding 2 — Critical. Two vocabularies for the same component.** A1 rows are "Claude skills, Codex skills, the Astra behavioral patch, browse bundle, Chromium…"; A2's default is `--require claude,codex,patch`; the trailer is `gstack: fail <component,component>`. If the trailer prints "Claude skills" the parent cannot map it to a `--require` id, and `--json` keys are a third guess. Today's doctor already has this problem in miniature (`row ok codex …` vs `check_codex`, `bin/gstack-doctor:186,446`). Fix: one `COMPONENTS` table in `lib/doctor-components.ts` (id, human label, probe, fix, needed-by skills), used for row ids, `--require`, the trailer, `--json` keys, and `--for` resolution; `--for <skill>` prints `required: claude codex patch` before the rows.
**Finding 3 — High. `--next` under stamp-at-merge is self-contradictory.** C2: "`--next` (main's VERSION at this moment plus the policy bump, through the next-version queue reader)". The queue reader resolves collisions against *claimed* PR versions (`bin/gstack-next-version:80-105`, `pickNextSlot`). Stamp-at-merge means no PR claims a version until it is next, so either the reader finds stale claims from pre-wave PRs and over-bumps, or it is a no-op. Fix: under `--queue-mode stamp-at-merge`, `--next` reads `origin/<base>:VERSION` + policy bump only and prints `claims: ignored (stamp-at-merge)`; the reader stays for the legacy claim-at-open mode. State the default `--queue-mode` (recommend: read from `ship-policy.json`, else legacy) so the flag is not required on every call.
**Finding 4 — High. The P3 promise depends on tier-2 pieces.** `--after <pr>` and `mirror` are tier 2 in C2; the queue replay test (Validation 3) asserts both. If they slide to a follow-up, PR C ships a restamp that works only when the PR is already next and the gate must rerun after every reorder, which is the incident. Fix: promote `--after` to tier 1 (it is the gate-ahead that makes reorders cheap) and leave `mirror` tier 2; or re-word P3 to "stamp when next" and move the reorder claim to the follow-up. Auto-decided: promote `--after`.
**Finding 5 — High. Error lines without a fix clause or docs anchor.** Criterion 3 asks for problem + cause + fix + link. Plan lines that have none: `PREDECESSOR MOVED <old> -> <new>`, `gate-reuse: not-eligible (<what changed>)`, `repo commands not executed (pass --allow-repo-commands)` (fix, no cause/link), `Autoplan publication guard is unavailable`, `autoplan guard: not installed; publication order is unverified`, every `gstack-artifact validate` failure. The repo already has the mechanism: reason codes in `lib/gate-outcomes.ts` each carrying a `docs/troubleshooting.md#anchor` (`lib/gate-outcomes.ts:69,74,79`; AGENTS.md "Key conventions"). Fix: add `RESTAMP_PREDECESSOR_MOVED`, `GATE_REUSE_NOT_ELIGIBLE`, `REPO_COMMANDS_NOT_AUTHORIZED`, `ARTIFACT_INVALID_*`, `GUARD_NOT_INSTALLED` to gate-outcomes with anchors, and have each bin print `<line> — fix: <text> (<anchor>)`.
**Finding 6 — High. P7's "parent consumed the artifacts" has no command behind it.** B2: "when the parent reports it, `consumer`". Tier 3 of PRs D and E unlock only on that number. Without a one-liner the number is zero forever and the plan's own gate never opens. Fix: `gstack-artifact ack <run.json> --consumer <thread-id>` appends the row to `analytics/autoplan-timing.jsonl` and sets `run.json.consumed_by`; the parent quickstart puts it after `validate`. Pair it with `gstack-artifact validate` returning exit 0 only when consumed? No, keep them separate: validate is a check, ack is a report.
**Finding 7 — Medium. Terminal-status lines use two grammars.** `AUTOPLAN_RESULT: gate_pending run=<path> decisions=<path> …` (B2) and `CSO_RUN: <run id> findings=<path>` (B2 cso bullet). The parent needs one regex. Fix: `GSTACK_RESULT: skill=<name> status=<complete|gate_pending|incomplete|interrupted> run=<path>` from every unattended skill, with the status enum also in `run.json.status`. Keep the old prefixes as aliases for one release if any test pins them.
**Finding 8 — Medium. Three ways to skip Chromium with no stated precedence.** `GSTACK_SKIP_PLAYWRIGHT=1` (env, `setup:2071`), the new `./setup --no-browser` (A2), and lazy-by-default in `gstack-capy-install` with `--with-browser` to override (D9). Fix: `--no-browser` sets the env var internally; the installer passes `--no-browser` unless `--with-browser`; document one sentence of precedence in `setup --help` (there is a test for help coverage, `test/setup-help.test.ts`).
**Finding 9 — Medium. `--for <skill>` and `--require <csv>` are two spellings of one input.** A1 offers both. Pick `--for <skill>` (resolves to ids via the component table) and let `--require <csv>` exist only as the low-level form it resolves to; print the resolved set so the parent learns the ids. Also state how `--check` composes with `--live` (`bin/gstack-doctor:37`): `--check` must never run the paid probe unless `--live` is also passed.
**Finding 10 — Medium. The hook-absent semantics are described as one behavior but are two.** B1: "the hook allows the call when no guard is installed and prints `autoplan guard: not installed`". Confirmed: the shell hook's backstop *denies* when its TS half fails (`autoplan/bin/phase-publication-hook:4-9`), which is the right behavior for a broken install. If the hook *file* is absent, a host that honors frontmatter fails the hook before anything prints; the only place to change that is the `{{AUTOPLAN_PUBLICATION_HOOK}}` render (`autoplan/SKILL.md.tmpl:31`). Fix: split the two cases in the plan text: (a) hook file absent → the resolver renders a one-line inline `command` that prints the "not installed" allow JSON; (b) hook present, `.ts` fails → keep deny. The review record carries line (a).
**Finding 11 — Medium. Durable state on a Capy drive conflicts with drive policy.** A3 sets `GSTACK_STATE_ROOT` to `~/.capy/drive/project-user-*/gstack-state`. Capy's drive guidance for agents says the drive is not for memory, scratch, or verification evidence; learnings and review logs are exactly that. The alternative the plan already names, "say plainly they are skipped", is honest and cheap. Fix: default to `learnings: skipped (ephemeral)` plus the `STATE_ROOT: <path> durable=no` line; make the drive root an explicit opt-in (`GSTACK_STATE_ROOT` set by the owner or `--state-root drive`). Owner decision recorded as: opt-in. Also specify how "a Capy cloud home" is detected (an env marker, not a path heuristic).
**Finding 12 — Medium. `gstack-artifact validate` has no `--json` and no exit-code contract.** B0 calls it "the deterministic completion check a parent runs first" but lists only failure causes. Fix: exit 0 valid, 1 invalid (with one `INVALID <reason-code> <path>` line per problem), 2 usage; `--json` emits `{valid, errors:[{code, path, message}]}`; `gstack-artifact schema <name>` prints the JSON Schema for the version it validates, so a parent on another machine can read the contract.
**Finding 13 — Medium. Pin files have no scaffold.** Four new JSON pin files (`ship-policy.json`, `generated.json`, `pregate.json`, `banned-terms.json`) each with a schema, none with an `init`/example. A coordinator adopting the wave hand-writes JSON against a TypeScript type they must open the repo to read. Fix: `gstack-restamp init-policy`, `gstack-regen init`, `gstack-pregate init` write a commented example (`"$schema"`, every key with its default), and `docs/ship-policy.md` shows the gstack repo's own files.
**Finding 14 — Low. Partial-stamp behavior when repo commands are refused.** C1 exit 3 "lists the commands" standalone. It must also say whether VERSION/CHANGELOG/`stamp_paths` were written. Fix: refuse before any write (all-or-nothing) and say `nothing written`.
**Finding 15 — Low. The "Not in this wave" premise on hosts is slightly off.** It says "no resolver reads `capabilities.questions`". Confirmed no resolver *branches* on it, but `scripts/host-config.ts:178` validates it and every host declares it (`hosts/claude.ts:7`, `hosts/codex.ts:7`). The decision (D2a) still holds; just correct the sentence so a future reader doesn't re-litigate it on a false premise.
**Finding 16 — Low. Review-log mismatch exit code.** B7 uses exit 2 for `status mismatch`; `bin/gstack-review-log:95-97` exits 1 for invalid JSON and doctor uses 2 for usage (`bin/gstack-doctor:12`). Use 1 for a rejected row, keep 2 for usage, and put the table in `--help`.
## Required outputs
### Developer journey map (post-plan, both personas)
| Stage | Parent agent (Persona A) | Coordinator (Persona B) | Artifact / line consumed |
|---|---|---|---|
| Install | `bin/gstack-capy-install --only autoplan` | same, `--only ship` | `gstack: ok <rev> for=autoplan` |
| Preflight | `gstack-doctor --check --for autoplan --json` | `gstack-doctor --check --for ship` | `PASS|FAIL|SKIP <id>` rows, trailer |
| Run | `GSTACK_SESSION_KIND=unattended` + autoplan on plan.md | `gstack-restamp --after <pr>` then `/ship` | `GSTACK_RESULT: …`, stamp-only diff |
| Verify | `gstack-artifact validate run.json` | `gstack-tree-receipt --gated <sha>` | exit 0 / `gate-reuse: eligible` |
| Decide | `jq 'select(.status=="pending")' decisions.jsonl` → owner → **`snapshot answer --reply "all except 3b"`** (missing today) | reuse gate or `--force-gate <reason>` | `decisions.jsonl` `kind:"owner"` |
| Report | `gstack-artifact ack run.json --consumer <thread>` (missing today) | PR body ` ```gstack-ship-receipt ` first | P7 counter / receipt |
| Recover | interrupted → `validate` fails → `snapshot resume` (missing) | `PREDECESSOR MOVED` → rerun `--after <new>` (needs fix clause) | reason codes with anchors |
### DX Implementation Checklist
- [ ] `lib/doctor-components.ts`: single component table (id, label, probe, fix, needed-by); row ids, `--require`, trailer, `--json` keys and `--for` all read it (Finding 2).
- [ ] `gstack-doctor --check`: `PASS|FAIL|SKIP <id> [— fix: …]` rows; trailer `gstack: <ok|fail> rev=<v> for=<skill|hosts> fail=<csv>`; `--json`; `--check` never runs the paid probe without `--live` (Finding 9).
- [ ] `./setup --no-browser` documented in `setup --help`; precedence sentence over `GSTACK_SKIP_PLAYWRIGHT` and `--with-browser` (Finding 8).
- [ ] `GSTACK_RESULT:` one grammar for autoplan and cso; `run.json.status` enum (Finding 7).
- [ ] `bun gstack-autoplan-snapshot answer|resume` and its doc (Finding 1).
- [ ] `gstack-artifact validate --json`, exit table, `schema <name>`, `ack` (Findings 6, 12).
- [ ] Parent-agent quickstart doc: install → run → validate → answer → ack, every command copy-paste, under `docs/unattended.md`; the user-scope skill links it.
- [ ] `gstack-restamp`: `--queue-mode` default from policy; `--next` ignores claims under stamp-at-merge and says so; `--dry-run`; all-or-nothing when repo commands are refused; `--after` tier 1 (Findings 3, 4, 14).
- [ ] `gstack-tree-receipt`: documented inputs (`--gated <sha>`, `--predecessor <sha>` or `--from gate-ahead.json`), `--json`, fix clause on `PREDECESSOR MOVED` and `not-eligible` (Findings 5, F10).
- [ ] `gstack-ship-receipt`: `schema_version` in the block; `read --pr N` for `/land-and-deploy` (F13).
- [ ] New reason codes in `lib/gate-outcomes.ts` with `docs/troubleshooting.md` anchors for every new error family (Finding 5).
- [ ] `init` scaffolds for the four pin files plus `docs/ship-policy.md` (Finding 13).
- [ ] Plan text fixes: hook-absent split (Finding 10), state-root opt-in (Finding 11), hosts sentence (Finding 15), review-log exit code (Finding 16).
- [ ] Exit-code table shared by all new bins (0 ok, 1 fail, 2 usage, 3 refused/needs flag) in each `--help`; a `--help` coverage test for the new bins alongside `test/setup-help.test.ts`.
### Magical moments
1. **`gstack: ok 1.91.70.0 for=autoplan` in under 30 seconds on a fresh box**, with Chromium never downloaded. The parent greps one line and moves on. The first time this fires on a machine that used to take 86 s and lie, the trust problem ends.
2. **`gstack-artifact validate run.json && jq 'select(.status=="pending")' decisions.jsonl`** returning seven numbered gates with recommendations and costs, no prose read. Then `snapshot answer --reply "all except 3b"` and the plan is final. Two commands, zero paid reruns. This moment exists only with Finding 1 fixed.
3. **`gate-reuse: eligible`** after a restamp, with the identity it rests on printed (tree-modulo-stamps, pins, lockfile, CI hashes). A 40-minute gate skipped with a receipt that a reviewer can audit. This is the moment the coordinator stops using the repo's own tool.
4. **`OUTSIDE_STATUS: completed provider=host-subagent model=gpt-6-astra`** in a Capy run with no Codex CLI, never `[single-model]`. The outside voice becomes a model choice, not a CLI install.
### Competitive benchmark: how Claude Code, Codex, and Capy natively expose structured subagent output
- **Claude Code** subagents (Task/Agent tool) return a single free-text result to the parent; structured output exists only when the prompt asks for JSON and the parent parses it, with no schema or validator. `claude -p --output-format json` wraps the final message in a typed envelope (`result`, `session_id`, cost) but the content is still prose. gstack's B0 (`run.json` manifest + `schema_version` + validator) is ahead of the harness: it is the only one of the three that gives a parent a deterministic completion check.
- **Codex CLI** `codex exec` streams JSONL events and exits with the agent's status; the final message is text. There is no artifact manifest. gstack's `GSTACK_RESULT:` line plus files is comparable to Codex's last JSONL event; the gain is files with hashes a parent can validate later.
- **Capy** subagents end with a completion summary (`message_agent` final), a `tools.subagent.get` summary field, and files attached by path; `pr_create` gives a PR with checks and findings a parent reads through `tools.pr.getFindings` as structured rows. The pattern Capy already uses, identifiers first then prose and a typed findings list, is the pattern C5 and B2 adopt. Where gstack should match Capy: Capy's subagent output is addressable without knowing a path (by subagent index); gstack's `AUTOPLAN_RESULT` names paths, so the parent must parse a line to find the files. Printing the run directory once (`run=<dir>`) with fixed file names inside (`run.json`, `decisions.jsonl`, …) matches Capy's "one handle, known shape" and shortens the parent's regex to one capture group.
- **Net:** with Finding 1 (resume) and Finding 7 (one result grammar) fixed, gstack's unattended contract is stricter than any of the three harnesses' native output, which is the right place for a tool whose primary user is another agent.
## Scores
- Initial overall (plan as written, averaged over the eight passes): **3.1 / 10**. The artifact contract and gate list are strong; the loop is open at the answer step, component naming is split, the queue-mode semantics contradict the queue reader, and errors mostly lack fix clauses.
- Target overall (with the tasks below, all P1 and P2): **7.6 / 10**. Remaining gap is docs volume and the tier-2 slide risk on `mirror`.
## Tasks
```json
[
{"id":"DX-1","priority":"P1","component":"autoplan","title":"Add `gstack-autoplan-snapshot answer|resume`: consume gate-list replies, run Phase 4 close operations, finalize plan.md and run.json.status","files":["lib/autoplan-export.ts","bin/gstack-autoplan-snapshot.ts","lib/gate-list.ts","autoplan/sections/unattended.md.tmpl","docs/unattended.md","test/autoplan-unattended.test.ts"],"effort_human":"3 days","effort_cc":"3 hours","source_finding":"Finding 1"},
{"id":"DX-2","priority":"P1","component":"doctor","title":"One component table for row ids, --require, --for resolution, trailer and --json keys; print the resolved required set","files":["lib/doctor-components.ts","bin/gstack-doctor","bin/gstack-capy-install","test/gstack-doctor.test.ts"],"effort_human":"2 days","effort_cc":"2 hours","source_finding":"Finding 2"},
{"id":"DX-3","priority":"P1","component":"restamp","title":"Define --queue-mode default (from ship-policy) and make --next ignore claims under stamp-at-merge with an explicit line","files":["lib/restamp.ts","bin/gstack-restamp","lib/ship-policy.ts","bin/gstack-next-version","test/gstack-restamp-queue.test.ts"],"effort_human":"1 day","effort_cc":"1.5 hours","source_finding":"Finding 3"},
{"id":"DX-4","priority":"P1","component":"restamp","title":"Promote --after <pr> (gate-ahead) to tier 1 so P3's reorder promise ships in PR C","files":["lib/restamp.ts","bin/gstack-restamp","test/gstack-restamp-queue.test.ts"],"effort_human":"2 days","effort_cc":"2 hours","source_finding":"Finding 4"},
{"id":"DX-5","priority":"P1","component":"errors","title":"Reason codes with docs/troubleshooting.md anchors for PREDECESSOR MOVED, gate-reuse not-eligible, repo commands refused, artifact invalid, guard not installed; every line carries fix:","files":["lib/gate-outcomes.ts","docs/troubleshooting.md","lib/tree-receipt.ts","lib/restamp.ts","lib/headless-artifacts.ts","autoplan/bin/phase-publication-hook"],"effort_human":"1.5 days","effort_cc":"1.5 hours","source_finding":"Finding 5"},
{"id":"DX-6","priority":"P1","component":"artifacts","title":"`gstack-artifact ack <run.json> --consumer <id>` so the P7 adoption number is measurable; `validate --json`, exit table, `schema <name>`","files":["lib/headless-artifacts.ts","bin/gstack-artifact","docs/unattended.md","test/gstack-artifact.test.ts"],"effort_human":"1 day","effort_cc":"1.5 hours","source_finding":"Finding 6, Finding 12"},
{"id":"DX-7","priority":"P1","component":"docs","title":"Parent-agent quickstart (install → run → validate → answer → ack) with copy-paste commands; link from the user-scope Capy skill","files":["docs/unattended.md","README.md","agents-digest/gstack-AGENTS.md"],"effort_human":"1 day","effort_cc":"1 hour","source_finding":"Pass 6, Pass 1"},
{"id":"DX-8","priority":"P2","component":"unattended","title":"One terminal-status grammar `GSTACK_RESULT: skill= status= run=` for autoplan and cso; run.json.status enum","files":["lib/headless-artifacts.ts","autoplan/sections/unattended.md.tmpl","cso/SKILL.md.tmpl","test/autoplan-unattended.test.ts"],"effort_human":"0.5 day","effort_cc":"45 min","source_finding":"Finding 7"},
{"id":"DX-9","priority":"P2","component":"tree-receipt","title":"Specify receipt inputs (--gated, --predecessor | --from gate-ahead.json), add --json, fix clause on refusals","files":["lib/tree-receipt.ts","bin/gstack-tree-receipt","ship/SKILL.md.tmpl","land-and-deploy/SKILL.md.tmpl"],"effort_human":"1 day","effort_cc":"1 hour","source_finding":"Journey F10, Finding 5"},
{"id":"DX-10","priority":"P2","component":"ship-receipt","title":"schema_version in the fenced receipt and `gstack-ship-receipt read --pr N` for /land-and-deploy","files":["bin/gstack-ship-receipt","lib/headless-artifacts.ts","land-and-deploy/SKILL.md.tmpl"],"effort_human":"0.5 day","effort_cc":"45 min","source_finding":"Journey F13"},
{"id":"DX-11","priority":"P2","component":"setup","title":"Document ./setup --no-browser and the precedence over GSTACK_SKIP_PLAYWRIGHT and --with-browser in setup --help","files":["setup","bin/gstack-capy-install","test/setup-help.test.ts"],"effort_human":"0.5 day","effort_cc":"30 min","source_finding":"Finding 8"},
{"id":"DX-12","priority":"P2","component":"pin-files","title":"`init` scaffolds for ship-policy.json, generated.json, pregate.json, banned-terms.json plus docs/ship-policy.md with gstack's own examples","files":["bin/gstack-restamp","bin/gstack-regen","bin/gstack-pregate","bin/gstack-banned-terms","docs/ship-policy.md"],"effort_human":"1 day","effort_cc":"1 hour","source_finding":"Finding 13"},
{"id":"DX-13","priority":"P2","component":"plan-text","title":"Split hook-absent vs hook-failed semantics; make drive state root opt-in; correct the hosts capabilities sentence; align review-log exit codes","files":["scripts/resolvers/autoplan-publication-hook.ts","autoplan/bin/phase-publication-hook","bin/gstack-skill-start","bin/gstack-review-log"],"effort_human":"0.5 day","effort_cc":"45 min","source_finding":"Findings 10, 11, 15, 16"},
{"id":"DX-14","priority":"P3","component":"restamp","title":"--dry-run flag and all-or-nothing refusal when repo commands are not authorized (`nothing written`)","files":["lib/restamp.ts","bin/gstack-restamp"],"effort_human":"0.5 day","effort_cc":"30 min","source_finding":"Finding 14"},
{"id":"DX-15","priority":"P3","component":"cli","title":"Shared exit-code table (0/1/2/3) in every new bin's --help and a --help coverage test for the new bins","files":["bin/gstack-artifact","bin/gstack-restamp","bin/gstack-tree-receipt","bin/gstack-pregate","bin/gstack-gate","test/new-bin-help.test.ts"],"effort_human":"0.5 day","effort_cc":"45 min","source_finding":"Pass 4"}
]
```
Recommendation: approve with changes because the artifact contract and gate list are the right shape for an agent-first tool, but the unattended loop has no answer/resume step, the install verdict uses two component vocabularies, and `--next` contradicts the queue reader under stamp-at-merge; fixing DX-1 through DX-7 before PR B and PR C land turns a promising plan into one a parent can drive end to end without reading prose.