* 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>
34 KiB
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.
- Parent reads drive
SKILL.md, runsbash scripts/install.sh --check. F1: silent exit 1, or exit 0 through a pipe. No row, no fix hint, no revision. - 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 undocumentedGSTACK_SKIP_PLAYWRIGHT=1onsetup(setup:2071). - 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 aswarn. The parent cannot tell whether a gate on this Bun is sound. No--json(only--liveand-hare parsed,bin/gstack-doctor:37-38). - Dispatches a subagent: "read
~/.claude/skills/autoplan/SKILL.mdand run it on plan.md". F4: the preamble classifies the session asinteractive(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 setsGSTACK_HEADLESS=1, the skill BLOCKs at the first question (generate-ask-user-format.ts:28). If it setsGSTACK_SESSION_KIND=spawned, the final approval gate is auto-approved (:15, exception only for destructive options). - The run ends with a prose record. F5: The parent has to read
ACTIVE_PLANto find decisions;tasks-<phase>-*.jsonlexist per phase but no aggregate, no manifest, no completion check. - F6: Learnings and decisions went to
~/.gstackon the ephemeral box (lib/state-root.ts:22-29chain ends at$HOME/.gstack) and are gone at wake. - F7: Codex outside voice needs
codex login; the plan shows it works fromOPENAI_API_KEYin 3.7 s but nothing in doctor (bin/gstack-doctor:186says "codex login, or export CODEX_API_KEY") oroutsideVoiceFailurePolicy()(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 " 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.
- Coordinator decides PR 3 is next. Runs
gstack-restamp --queue-mode stamp-at-merge --nextin PR 3's worktree. F8:--nextis 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. gstack-restampwants to runbun run release:restampfrom.gstack/ship-policy.json. F9: standalone it exits 3 withrepo 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_pathsstill happens (partial stamp) or nothing happens. A half-stamped tree with the release tool skipped is the worst outcome.- Receipt:
gstack-tree-receiptprintsTREE: same-modulo-stampsandgate-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. - Reorder #1: PR 1 now goes before PR 3. PR 3 needs
--after 1(gate-ahead). F11:--afterandmirrorare 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. - 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). - Merge. F13: the ship receipt (C5) is the first fenced block of the PR body, good, but has no
schema_versionfield listed even though B0 saysship-receiptis in the contract, and there is no reader (gstack-ship-receipt read --pr N) for/land-and-deployto 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 |
| 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 |
| 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,--jsonkeys and--forall read it (Finding 2).gstack-doctor --check:PASS|FAIL|SKIP <id> [— fix: …]rows; trailergstack: <ok|fail> rev=<v> for=<skill|hosts> fail=<csv>;--json;--checknever runs the paid probe without--live(Finding 9)../setup --no-browserdocumented insetup --help; precedence sentence overGSTACK_SKIP_PLAYWRIGHTand--with-browser(Finding 8).GSTACK_RESULT:one grammar for autoplan and cso;run.json.statusenum (Finding 7).bun gstack-autoplan-snapshot answer|resumeand 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-modedefault from policy;--nextignores claims under stamp-at-merge and says so;--dry-run; all-or-nothing when repo commands are refused;--aftertier 1 (Findings 3, 4, 14).gstack-tree-receipt: documented inputs (--gated <sha>,--predecessor <sha>or--from gate-ahead.json),--json, fix clause onPREDECESSOR MOVEDandnot-eligible(Findings 5, F10).gstack-ship-receipt:schema_versionin the block;read --pr Nfor/land-and-deploy(F13).- New reason codes in
lib/gate-outcomes.tswithdocs/troubleshooting.mdanchors for every new error family (Finding 5). initscaffolds for the four pin files plusdocs/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--helpcoverage test for the new bins alongsidetest/setup-help.test.ts.
Magical moments
gstack: ok 1.91.70.0 for=autoplanin 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.gstack-artifact validate run.json && jq 'select(.status=="pending")' decisions.jsonlreturning seven numbered gates with recommendations and costs, no prose read. Thensnapshot answer --reply "all except 3b"and the plan is final. Two commands, zero paid reruns. This moment exists only with Finding 1 fixed.gate-reuse: eligibleafter 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.OUTSIDE_STATUS: completed provider=host-subagent model=gpt-6-astrain 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 jsonwraps the final message in a typed envelope (result,session_id, cost) but the content is still prose. gstack's B0 (run.jsonmanifest +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 execstreams JSONL events and exits with the agent's status; the final message is text. There is no artifact manifest. gstack'sGSTACK_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_agentfinal), atools.subagent.getsummary field, and files attached by path;pr_creategives a PR with checks and findings a parent reads throughtools.pr.getFindingsas 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'sAUTOPLAN_RESULTnames 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
[
{"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.