Files
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

51 KiB

INPUT: eng d7365d9256d2c956908fce530731115816ab6b477152e78602fc69d51fd14b17

Read receipt: /workspace/gstack/.gstack/tmp/autoplan/autoplan-eng-JwMe0e/native-prompt.md, 230 lines, 67384 bytes, SHA-256 77ee24a4…3511f (matches the dispatch); the six lines over 2000 chars (54, 58, 65, 74, 100, 102) were re-read in full via sed. Every file:line below was opened on origin/main 91bbd9e (v1.91.69.0) in /workspace/gstack.

Prior-phase decisions treated as binding: UC1 (tiered scope, P7 gate), B0 (one artifact contract + validator), B1 (unattended as a fourth kind), C1 (repo commands opt-in), C3/D11 (gate reuse on identity), B2 (one runner), A1 (one doctor table), A2 (Bun preflight), C2 (stamp-at-merge, restamp journal), hunk-level receipts, GSTACK_RESULT grammar, gate-list ids/pagination/gate_rev, two-stage pre-gate. Where the plan's own premise table is wrong I say so with the receipt; the plan's D1 "premise table" row would have caught two of these.

Scope Challenge

What already exists (verified). bin/gstack-doctor is 456 lines of Bash 3.2 with one check_* per row, --live, exit 1 on fail / 2 on usage (bin/gstack-doctor:3-16, :35-40, :442-454). bin/gstack-session-kind classifies spawned|headless|interactive with the explicit override only honoring spawned (bin/gstack-session-kind:33-36); bin/gstack-skill-start:93 whitelists exactly those three and coerces anything else to interactive. The preamble's AUQ rule branches on the same three (scripts/resolvers/preamble/generate-ask-user-format.ts:15-29). lib/state-root.ts:22-29 owns the chain and test/state-root-ratchet.test.ts rejects hand-rolled chains and bare ~/.gstack prose. bindReview is the single write choke point and reviewFreshness already downgrades issues_found > 0 at read time (lib/review-evidence.ts:178-186, :238-240); the bash wrapper swallows binding errors as invalid JSON (bin/gstack-review-log:88-96). gstack-version-bump write has one opt-in post-write hook with the exact trust rationale C1 wants to generalize (bin/gstack-version-bump:373-402) and a half-write recovery path (:483-495). gstack-evidence records tree/wtree and compares changed paths against an allow-list by filename (bin/gstack-evidence:388-396, :498-512) and is machine-local by design (:27-30). outsideVoiceFor picks the provider from ctx.host at render time (scripts/resolvers/outside-voice.ts:8-12). The spec-review loop stops on PASS / repeats / third launch and "a missing score alone does not require another review" (scripts/resolvers/spec-review.ts:186-192). The autoplan publication hook renders only for host === 'claude' and denies when the file is absent (scripts/resolvers/composition.ts:11, :16-23); the shell backstop denies when the TS half fails (autoplan/bin/phase-publication-hook:4-9). lib/gate-outcomes.ts:1-18 is the reason-code contract with a docs/troubleshooting.md anchor test (test/troubleshooting-anchors.test.ts). setup honors GSTACK_SKIP_PLAYWRIGHT=1 (setup:2071-2072) and GSTACK_SKIP_FONTS=1 (setup:1512), prints a checksum-verified Bun install recipe when Bun is missing (setup:296-305), and --status always exits 0 (setup:136-146). Section manifests exist for 21 skills and are documented as PASSIVE registries with no machine predicate (autoplan/sections/manifest.json:5). investigate/ has no sections/ dir (278-line monolith; parity ratio 0.787 of a 1.12 cap, so it has room).

What the plan rebuilds vs. extends. PR A extends doctor/setup (right call). PR B's B7/B8/B9/B12 extend existing choke points (right call). PR B's B2/B3 are new subsystems disguised as "thin": a prompt-assembling, dispatching, resumable state machine (B2) and a runtime outside-voice runner (B3) where today only render-time prose exists. PR C is almost entirely new code (restamp, receipt, regen, pregate, ship-policy, ship-receipt, evidence bundle): seven libs and six bins is a realistic count, and the queue-replay fixture alone is a multi-repo harness. PR D is template + resolver work with budget consequences (Section 4). PR E is tier-3 except E1 tier 1 and the E2 bundle, which C3 already pulls forward.

Complexity check (my estimate, [estimated], counting what the text implies rather than the table's "3/1/8").

PR New bins New libs Template/resolver edits Fixtures Est. LOC Fits one review sitting?
A gstack-capy-install, gstack-browser-ensure (+ doctor --check/--for/--require/--json/--project) runtime-pins.ts, component table (see F5 on language) setup --no-browser, --status exit, browse preflight ~10 (healthy, each missing, wrong host, no auth, range vs exact pin, Windows lane pin) 900-1200 yes
B gstack-artifact, gstack-autoplan, gstack-gate, gstack-codex-login, (+ gstack-outside-voice, F2) headless-artifacts.ts, autoplan-export.ts, autoplan-run.ts, gate-list.ts, outside-voice-api.ts preamble (unattended), skill-start, session-kind, autoplan skeleton + sections/unattended, 4 checklists, spec-review, review-evidence, review-log, phase-close ~20 3500-5000 no — B2+B3 alone are a PR
C gstack-restamp, gstack-tree-receipt, gstack-regen, gstack-pregate, gstack-ship-receipt, gstack-hermetic-run ship-policy.ts, restamp.ts, changelog-check.ts, tree-receipt.ts, regen.ts, pregate/* (≥6 files), md-links.ts, evidence bundle ship Steps 3/5/12/16/18, land-and-deploy, tests.md.tmpl ~16 + a 3-PR multi-repo replay 4000-6000 no — pregate and restamp are separate review sittings
D — plan-reality.ts resolver 6 template files ~12 600-900 yes, but every edit lands on a zero-headroom skill (Section 4)
E gstack-lane-check, gstack-banned-terms, (gstack-evidence verify) lane-ownership.ts, spend-ledger.ts, pricing.ts, eval-plan sections new skill + qa/ship sections ~10 1500-2500 yes (tier 1 only)

PR count vs the repo's own constraints. The repo has no hard PR-count rule, but it has three byte-level ratchets that bite per PR (parity, context-budget, skill-size) and a release process where every PR is a full gate. Five PRs is the right shape; two of them are the wrong size. Recommended split (keeps D1(a) in spirit, seven PRs, not ten): B → B-core (B0, B1, B7, B9, B12, B10) and B-runner (B2, B3, B4, B8, B11); C → C-stamp (C1, C2, C3, C5, C7) and C-pregate (C4, C8, C6). Each half is still shippable alone and the queue cost is two extra gates, which the pre-gate PR is designed to make cheaper. Record as decision E1 below.

Section 1 Architecture

 parent agent (Capy thread / coordinator)
   | GSTACK_LAUNCH: GSTACK_SESSION_KIND=unattended GSTACK_STATE_ROOT=<p> GSTACK_EPHEMERAL=1
   v
 bin/gstack-capy-install ──> ./setup --host claude|codex (--no-browser) ──> bin/gstack-doctor --check --for <skill>
   |                                                                          ^ table: doctor-components (F5: bash, not TS)
   |                                                                          ├─ lib/runtime-pins.ts  <── <project>/package.json, .tool-versions, .github/workflows/*.yml
   |                                                                          └─ bin/gstack-browser-ensure (lazy Chromium)
   v
 bin/gstack-autoplan {run|next|submit|status|answer|resume} ── lib/autoplan-run.ts (state machine; F1)
   |   reads: autoplan/sections/*.md (+ scope field, F9), snapshot.json (nativeDispatchPrompt), gate list
   |   writes: --out/<run>/{plan.md, review-record.md, tasks.jsonl, decisions.jsonl, findings.jsonl, timing.json, run.json}
   |           via lib/headless-artifacts.ts  <── bin/gstack-artifact {validate|schema|ack}
   |   outside voice: bin/gstack-outside-voice --runner codex-cli|api|host-subagent (F2) ── lib/outside-voice-api.ts
   |   review log:    bin/gstack-review-log --findings findings.jsonl ── lib/review-evidence.ts bindReview (B7)
   |   gate:          bin/gstack-gate {render|parse} ── lib/gate-list.ts (ids, gate_rev, pages)
   |   state root:    $GSTACK_STATE_ROOT/analytics/autoplan-timing.jsonl   (EPHEMERAL on Capy — F3)
   v
 /ship (Step 5)  bin/gstack-pregate ── lib/pregate/{index,regen,secrets,strays,literals,lanes,import-graph,...}.ts
   |                 pins: .gstack/pregate.json  .gstack/generated.json (read from origin/<base>, F12)
   |                 writes: pregate.json (schema via headless-artifacts)
 /ship (Step 12/16/18)
   bin/gstack-restamp --queue-mode stamp-at-merge --next|--version|--after <pr> ── lib/restamp.ts, lib/changelog-check.ts
   |     pins: .gstack/ship-policy.json (lib/ship-policy.ts)   journal: .gstack/tmp/restamp-journal.json   gate-ahead: .gstack/tmp/gate-ahead.json
   |     calls: bin/gstack-version-bump write, <release_tool> (opt-in), bin/gstack-regen --write, mirror
   bin/gstack-tree-receipt --from gate-ahead.json ── lib/tree-receipt.ts  <── bin/gstack-evidence bundle (portable), gstack-wtree
   |     verdict: TREE: same-modulo-stamps | changed (<paths>)   gate-reuse: eligible | not-eligible (<what>)
   bin/gstack-ship-receipt ── fenced ```gstack-ship-receipt``` (first PR-body section) ── /land-and-deploy read --json

Module-size ratchet risks (corrected). The plan's premise that bin/gstack-next-version (822), bin/gstack-autoplan-snapshot.ts (865) and setup (4662) are capped by test/module-size-ratchet.test.ts is false: the fixture caps 47 newModules and 4 residualFiles, none of which are those three (test/fixtures/module-size-ratchet.json; the residual list is scripts/test-free-shards.ts, scripts/test-paid-shards.ts, browse/src/server.ts, test/helpers/claude-pty-runner.ts). Keeping those bins thin is still good hygiene, but the real ratchet exposure is on files the plan edits and does not name: scripts/resolvers/plan-gates.ts (571/800, D2 adds the Undeclared behavior changes line), scripts/resolvers/spec-review.ts (288/800, B8), autoplan/bin/phase-publication-hook.ts (B1), scripts/lib/measure-bar.ts (E1), scripts/resolvers/outside-voice-steps.ts (B3). All five are in newModules, so the 150-line function cap applies to every function touched. New lib/* files are not ratcheted unless added to the fixture; the plan's "each under 150 lines" for lib/pregate/* is unenforced until it adds them.

State-root compliance. Every new path in the plan goes through GSTACK_STATE_ROOT or a parent-chosen --out dir, which is correct. Two traps: (1) templates and resolvers must use the gstack-paths --get GSTACK_STATE_ROOT form with the :? guard or test/state-root-ratchet.test.ts fails on the first fence (test/state-root-ratchet.test.ts:18-25); (2) docs/unattended.md must not write ~/.gstack in prose (PROSE_ROOT regex, :20) — write $GSTACK_STATE_ROOT. The --out directory is outside the chain by design; run.json should record state_root and state_root_durable so the validator can say which artifact class landed where.

Generated-template compliance. All prose edits are .tmpl + bun run gen:skill-docs --host all (scripts/gen-skill-docs.ts:34, :116 supports all). The new {{HOST_ADAPTATION:unattended}} and {{PLAN_REALITY_ROWS}} and {{COORDINATOR_CONTRACT}} resolvers must be registered in scripts/resolvers/index.ts:52 and will be rendered into every host, so each costs bytes on every skill that includes it (Section 4). B4's scope field on sections/manifest.json reverses the documented passive-registry design (autoplan/sections/manifest.json:5 "No machine predicate here — see docs/designs/v2_PLAN.md:663"); test/qa-lazy-sections.test.ts:127 asserts passive resolution without inlining. The reversal is defensible for an unattended runner that assembles prompts itself, but the plan must update the note, the $schema, and that test rather than add a field nobody reads.

Section 2 Edge cases and failure modes

Registry (critical gap = no test AND no handling AND silent, marked ★):

# Mode Detection Handling in plan Test in plan Gap
1 --check piped through tail, caller sees pipe status trailer line + non-zero exit A1 prints trailer on every path; SKILL.md says never pipe doctor fixtures on trailer tokens ok
2 Bun absent on the target machine, doctor table in TS bun missing none — A1 puts the table in lib/doctor-components.ts, doctor is Bash 3.2 and promises to report without Bun (bin/gstack-doctor:22) none ★ F5
3 bun upgrade --version <pin> unsupported on Bun 1.3.14 bun upgrade --help lists only --canary A2 assumes the flag none ★ F11
4 Project pin and gstack floor conflict both constraints printed, installer stops yes partial (runtime mismatch fixture) add conflict fixture
5 unattended set but older gstack-skill-start coerces to interactive (bin/gstack-skill-start:93) nothing not mentioned not mentioned ★ F7
6 GSTACK_SESSION_KIND=unattended and GSTACK_HEADLESS=1 both set (eval harness + parent) override wins at step 0 implied, not stated negative control for HEADLESS only state precedence; add both-set test
7 Capy host: hook file present, hooks never executed nothing B1 detects file absence only (composition.ts:21-23) guard fixtures on absence ★ F4
8 Claude host: hook file absent (broken install) → B1 turns deny into allow prints guard: not installed weakens the guard where it ships — F4
9 P7 counter written to ephemeral state root; telemetry off in unattended — A3 says learnings skipped; B0/B10 still write analytics none ★ F3
10 Unattended runner needs a native reviewer the host must dispatch (this review) run blocks or shells to Codex for both voices B2 has no submit/next verb acceptance says "produces the files" ★ F1
11 Outside runner api returns truncated/empty output; host-subagent result file missing OUTSIDE_STATUS B3 "never degraded" for completed; missing-file path unspecified — add refused + reason code
12 Same-family check when --outside-model is a Claude id and native is Claude model-family table stated as a rule none add fixture
13 findings.jsonl count matches, content omitted hash binding in run.json yes (B0) omitted-finding fixture ok
14 answer against stale gate_rev refused with current rev yes (B9) parse fixtures ok
15 d10 yes (id + bare yes) ambiguous for multi-option rejects bare yes/no; id+yes unspecified fixture quoted specify: yes after an id = recommended option; print the mapping
16 Interrupted run with half-written decisions.jsonl validate fails resume from first incomplete gate interrupted fixture ok; require atomic rename on every writer
17 B7 mismatch exit code exit 2 vs 1 contradiction inside B7; usage today exits 1 (bin/gstack-review-log:81-84) — — F8
18 origin/<base> absent (shallow clone on gate VM) when reading policy git fails not stated none ★ F12
19 --after <pr> throwaway worktree writes evidence under a synthetic branch key gstack-evidence check on real branch finds nothing not stated queue replay doesn't cover ★ F13
20 Old VERSION string equals an unrelated dependency version in a lockfile or doc permitted-transform regex rewrites it hunk classification per path, patterns "exact" — add fixture (F14)
21 Two restamps race (two coordinators) on one branch journal journal is per-run injected failures per stage add lock on journal path
22 Predecessor head moves between gate and merge PREDECESSOR MOVED refusal yes (C3) replay includes it ok
23 Pre-gate lanes for a touched bash bin (no importer) import graph finds nothing → PASS not stated — ★ F15
24 gstack-hermetic-run imports test/helpers/hermetic-env.ts from a shipped bin product typecheck scope not stated — F16
25 regen --check scratch copy of a large repo with node_modules time "two-minute promise" measured measured on gstack and gbrain ok if scratch copy uses git worktree, not cp -r
26 .gstack/ship-policy.json mirror or stamp_paths escapes repo (.., symlink) containment check C1 says containment-checked none named add escape fixtures
27 Pre-gate downgraded check still fails hard on a first run in a drifted repo warn map yes (D5) — ok
28 10x: 33-decision gate list, pagination ids stable ids yes approve-all fixture ok
29 10x: findings.jsonl with 176 open across 4 voices, validate referential integrity O(n) yes — ok
30 Ratchet fixture refresh forgotten → CI red on parity/context budget on every PR B-E ratchet tests plan says "clear parity"; no per-PR receipt — F6

Section 3 Test plan

 codepath                                   test file (free unless marked)                      fixture
 ─────────────────────────────────────────  ──────────────────────────────────────────────────  ─────────────────────────────
 A  doctor --check/--for/--require/--json   test/gstack-doctor.test.ts (extend)                 healthy; each missing; wrong host;
                                                                                               no auth; bun range vs exact; win lane pin
    lib/runtime-pins.ts                     test/runtime-pins.test.ts (new)                     package.json engines, .tool-versions, nvmrc,
                                                                                               workflows w/ per-lane pins
    gstack-capy-install                     test/gstack-capy-install.test.ts (new, PATH-stubbed) no-bun; old-bun; conflict; --with-browser; --revision mismatch
    gstack-browser-ensure                   test/gstack-browser-ensure.test.ts (new)             stub playwright dir; lazy marker
    setup --no-browser / --status exit 1    test/setup-status.test.ts (extend)                   broken section link registry
 B  session-kind unattended                 test/gstack-session-kind.test.ts (extend)            override; both-set; unknown value
    skill-start unattended                  test/gstack-skill-start.test.ts (extend)             no telemetry prompt; UPGRADE_AVAILABLE one-liner
    preamble AUQ rule                       test/spawned-consent-rule.test.ts (extend)           unattended never records consent
    headless-artifacts + gstack-artifact    test/headless-artifacts.test.ts (new)                valid; interrupted; missing reviewer; omitted finding;
                                                                                               count/hash mismatch; stale artifact; schema print
    autoplan-run state machine              test/gstack-autoplan-run.test.ts (new)               next→submit→close per phase; answer stale rev; resume
    autoplan export                         test/autoplan-snapshot.test.ts (extend)              plan.md >30KB warn; custom heading
    outside-voice runner                    test/gstack-outside-voice.test.ts (new)              codex-cli stub; api stub; host-subagent result; same-family refusal
    review-log --findings                   test/review-log.test.ts (extend)                     clean/176-open mismatch; resolved-all → clean
    gate-list                               test/gate-list.test.ts (new)                         `all`, `d3b uc1a`, `all except d9b`, `yes`, `d10 yes`, 33 items/pages
    spec-review cap                         test/spec-review*.test.ts (extend)                   cap → cheap pass vs unconfirmed decision
    URGENT block order                      test/autoplan-report-order.test.ts (new)             block precedes summary
    E2E unattended run (paid)               test/skill-e2e-autoplan-unattended.test.ts (new)     this plan; PR B merge gate (Validation 2)
 C  ship-policy init/validate/show          test/ship-policy.test.ts (new)                       containment escapes; head vs base
    restamp                                 test/gstack-restamp.test.ts (new)                    --version; --next; --after; idempotent; dry-run; journal injected failures
    queue replay                            test/gstack-restamp-queue.test.ts (new)              3 PRs, mirror, CHANGELOG, (was X), since:, reorder x2
    tree-receipt                            test/gstack-tree-receipt.test.ts (new)               same-modulo-stamps; changed; lockfile-only; predecessor moved; dep==old version
    regen                                   test/gstack-regen.test.ts (new)                      stale golden; --write; command not authorized (exit 3)
    pregate (per check)                     test/pregate-<check>.test.ts (new, one per lib file)  INCIDENTS.md map: stale goldens, scratch, heavy-only const, win-only change, dropped hunk, |tail
    ship-receipt                            test/gstack-ship-receipt.test.ts (new)               render; read --json; truncation keeps identifiers
    ship/sections/tests.md.tmpl pipefail     test/ship-tests-section.test.ts (structural)         fence contains pipefail + wait $pid
 D  plan-reality resolver                   test/plan-reality-rows.test.ts (structural)          every row id rendered where declared; unattended → incomplete
    plan-gates undeclared-changes           test/plan-gates*.test.ts (extend)                    fixture diff with omitted behavior
    investigate sections + manifest         test/investigate-lazy-sections.test.ts (new)         manifest passive + scope; CARVE_GUARDS entry
    eval cases (paid)                       eval:bg:pr on plan-eng-review/plan-ceo-review/review/investigate
 E  spend-ledger                            test/spend-ledger.test.ts (new)                      spent+reserved>=cap under lock; unknown charges
    lane-check                              test/gstack-lane-check.test.ts (new)                 two branches intersecting
    banned-terms                            test/gstack-banned-terms.test.ts (new)               allowed_paths
    eval-plan skill                         test/context-budget-ratchet (new ceiling), parity (new invariant), skill-size-budget

Free vs paid. Everything above is free except the unattended E2E (Validation 2), the eval:bg:pr prompt evals for changed templates (Validation 5), and B4's checklist quality (needs a judge). The plan correctly routes paid work after cheap checks; the pregate PR should be the one that proves it.

Gaps. (1) No test for the mixed-version case (new gstack-session-kind + old gstack-skill-start), which is exactly a fleet-upgrade state. (2) No test that gstack-doctor --check works with Bun absent (the P1 scenario). (3) No --help coverage test exists today (no test/help-coverage*.test.ts), so B0's "with a --help coverage test" is a new test, name it: test/bin-help-exit-table.test.ts, asserting every new bin prints the exit table and exits 0. (4) The lanes check needs a fixture where a touched bash bin maps to its spawning test via path literal (F15). (5) The queue replay has no concurrent-restamp case. (6) gstack-artifact ack idempotency and the P7 census have no test because the collection path is undefined (F3). (7) Windows: every new bin that is bash (gstack-capy-install, gstack-codex-login, doctor) must be excluded from or made safe for the curated Windows subset (scripts/test-free-shards.ts --windows-only); plan is silent. (8) New test files need durations entries or the shard packer uses a default; check scripts/free-test-durations.json handling in the first PR and document it in INCIDENTS.md.

Section 4 Performance/size

Context budget impact (measured now, bun over test/helpers/parity-harness.ts invariants against test/fixtures/parity-baseline-v1.64.1.0.json):

skill union bytes ratio maxSizeRatio headroom
ship 288 804 1.539 1.539 ~0 B
review 133 923 1.234 1.2345 ~60 B
autoplan 115 143 1.129 1.1305 ~150 B
qa 94 736 1.127 1.1272 ~20 B
plan-eng-review 145 423 1.167 1.187 ~2.5 KB
plan-ceo-review 160 085 1.055 1.081 ~4 KB
plan-devex-review 129 397 1.033 1.08 ~6 KB
investigate (monolith) 47 702 0.787 1.12 ~20 KB
cso 33 508 0.374 1.08 large

Every template edit the plan makes to ship (C5, C6, C7, C8 Step 5, Step 16 stage 1, E2 handoff), review (D2, E3 contributor mode), autoplan (B1 guard line, B2 unattended section, B12, E3 brief, B4 checklist) and qa (E1 soak) will fail test/parity-suite.test.ts on the first byte and require a CARVE_GUARDS maxSizeRatio bump plus a capture-context-budget refresh in the same commit. The plan says "every template edit must clear parity" but budgets no bytes and names no receipt; that is a per-PR hidden cost of a few hours and, more importantly, a design pressure: prefer bins that print the text (gstack-gate render, gstack-ship-receipt, gstack-pregate table) over prose that describes it, and put new prose in lazy sections (they count in the union ratio but not in the eager context-budget ceiling). B4's four 200-line checklists are ~30-40 KB of new union bytes across the plan-review skills; plan-eng-review has 2.5 KB of headroom. B4 should be budgeted as "checklist replaces, never adds": the deep section shrinks by what the checklist takes, with the union byte delta recorded in the PR body. eval-plan is a new skill and test/context-budget-ratchet.test.ts fails when "a skill exists with no ceiling at all" — PR E must add the fixture entry.

Parity-suite risk. Low for mechanics (it ran green in 269 ms here), high for byte discipline per the table. Also test/skill-size-budget.test.ts is a shrink floor; B4 moving methodology out of eager text could trip it on plan-* skills if the skeleton shrinks past the floor — check minBytes before carving.

Two-minute pre-gate plausibility. Stage 1 (regen --check, secrets, strays, literals, links, lint, patches, ratchets) is plausible on gstack only if regen --check uses a git worktree scratch (not cp -r of a repo with node_modules) and gen:skill-docs --host all fits — today that generation plus the agents digest is tens of seconds; ratchets (module-size, state-root, parity, context-budget) is a few seconds each. literals is a grep over the test tree (fast). lint depends on semgrep being absent (prints not installed). So ~60-100 s on gstack, measured [estimated]; on gbrain unknown. Stage 2 (lanes, guards, hermetic) is unbounded by design (it runs tests) and the plan correctly keeps its own clock. Risk: lanes import-graph closure over scripts/ and test/ (~2000-line files) is a few seconds with a cached graph; without caching it reparses on every run — cache keyed by HEAD^{tree} under .gstack/tmp.

Findings

F1 — Critical. The unattended runner has no verb for the host-dispatched reviewer. B2 defines run|status|answer|resume and claims run "owns the unattended loop", yet the native voice on a Capy machine is a subagent the parent dispatches from a prompt file (exactly how this review ran; nativeDispatchPrompt lives in snapshot.json, bin/gstack-autoplan-snapshot.ts). Either run blocks forever waiting for a result nobody can hand it, or it shells both voices to Codex and violates B3's same-family rule. Evidence: B3 names runners for the outside voice only; nothing names how the native voice is produced without Claude Code. Change: make lib/autoplan-run.ts an explicit state machine with next (prints the phase, the voice, the prompt path and the expected result path, then exits gate_pending-style with GSTACK_RESULT: status=awaiting_result) and submit --phase <p> --voice native|outside --result <file> --model <id> (validates, binds hash into run.json, runs B11 reconciliation and phase-close). run becomes next in a loop only when a runner can execute both voices locally (api for both with two families).

F2 — High. B3 puts a runtime decision in a render-time resolver. outsideVoiceFor(ctx) picks the provider from ctx.host when gen:skill-docs renders (scripts/resolvers/outside-voice.ts:8-12); the rendered prose shells out to codex directly (scripts/resolvers/outside-voice-steps.ts:177, :327). A --runner codex-cli|api|host-subagent flag chosen per run cannot live there without rendering all three branches into every host's SKILL.md (bytes the zero-headroom skills do not have). Change: new bin/gstack-outside-voice run --runner <r> --prompt <file> --out <file> [--model <id>] [--result <file>] over lib/outside-voice-runner.ts; the resolver names the bin and the runner table once; outsideVoiceFailurePolicy() keeps owning the failure prose and adds codex-cli unavailable; using <runner>.

F3 — Critical. P7's adoption number has no durable collection path. B0 ack, B2 and B10 append to $GSTACK_STATE_ROOT/analytics/autoplan-timing.jsonl; A3 declares the Capy state root ephemeral by default and B1 makes unanswered telemetry consent off. UC1(a) gates PRs D/E tier 3 on "six of ten threads … a parent consumed", which this design cannot count. Change: make the durable carriers the ones that already leave the machine — run.json.consumed_by in the parent-chosen --out dir and the gstack-ship-receipt block (session_kind, artifacts_consumed: <n>/<m>) in PR bodies — and add gstack-ship-receipt census --repo <r> --since <tag> that computes the P7 number from merged PR bodies via gh. Analytics on the state root stay as a bonus, labeled durable=no.

F4 — High. B1's guard clause detects the wrong condition and loosens the right one. The publication hook renders only for host === 'claude' (scripts/resolvers/composition.ts:11) and the file-absent branch denies (composition.ts:16-23). On Capy the file is present (installed from the repo) and hooks are never executed, so "hook file absent" never fires and nothing reports the guard as unenforced; on Claude Code, absence means a broken install, where turning deny into allow removes the only backstop (autoplan/bin/phase-publication-hook:4-9 exists precisely so a broken install cannot pass as absent). Change: keep deny-on-absent for the Claude render; emit autoplan guard: not enforced by this host; publication order is unverified (GUARD_NOT_INSTALLED) from gstack-skill-start when SESSION_KIND=unattended (and on every non-claude host render), and have B0's validator require that line in review-record.md for unattended runs. The owner's direction ("allow guarded tool calls when no guard is installed and say so") is satisfied because unattended hosts never ran the hook anyway.

F5 — High. A1's component table in TypeScript breaks the doctor's "reports without Bun" contract. bin/gstack-doctor is Bash 3.2 + POSIX "so it still reports when bun or python3 is missing" (bin/gstack-doctor:22), and P1's scenario is a machine with nothing installed, Bun possibly below floor or absent (A2 says the Capy image ships 1.3.14; setup exits at :296 without Bun). A lib/doctor-components.ts table is unreadable in that state. Change: the table is bin/gstack-doctor-components.sh (sourced, one row per component: id, label, probe fn, fix, needed_by); if --json or --for resolution wants TS, generate the bash table from lib/doctor-components.ts at build time with a freshness test, the way agents-digest is generated (bin/gstack-version-bump:373-385 describes that pattern). --check must be exercised in a fixture with bun removed from PATH.

F6 — High. Byte budgets are unaccounted on four skills with zero headroom. See Section 4 table: ship 1.539/1.539, review 1.234/1.2345, autoplan 1.129/1.1305, qa 1.127/1.1272 (test/helpers/parity-harness.ts:247 default plus per-skill CARVE_GUARDS). Every PR B-E lands prose on at least one of them. Change: each PR's acceptance lists the union byte delta per skill and the CARVE_GUARDS/capture-context-budget refresh commit; B4 becomes "replace, never add"; any text a bin can print is printed by the bin.

F7 — Medium. unattended must be whitelisted in gstack-skill-start or it silently becomes interactive. bin/gstack-skill-start:93 case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive". B1 names session-kind, the preamble and skill-start but not this line, and the failure is the inverse of the intent (prompts return). Change: add unattended to the case; add a test that an unknown kind prints SESSION_KIND: interactive (unknown kind '<x>' from gstack-session-kind) so a mixed-version fleet is loud; state that the step-0 override outranks GSTACK_HEADLESS and that the eval harness never sets it.

F8 — Medium. B7's exit codes contradict themselves and the code. The text says a status mismatch is "exit 2" and three sentences later "a rejected row exits 1 … usage stays 2"; today the wrapper's usage exits 1 (bin/gstack-review-log:81-84) and swallows binding errors into invalid JSON (:88-96). Change: adopt B0's table — usage 2, rejected row 1 with review-log: status mismatch: claimed clean, findings.jsonl has 176 open (REVIEW_STATUS_MISMATCH) on stderr — and fix the wrapper to pass stderr through (drop the 2>/dev/null on :95).

F9 — Medium. New reason codes do not fit lib/gate-outcomes.ts's contract. GateOutcome.state is ran|not_run|unavailable with snake_case keys and one anchor each, checked by test/troubleshooting-anchors.test.ts (lib/gate-outcomes.ts:1-18). QUEUE_STALE, GATE_REUSE_NOT_ELIGIBLE, REPO_COMMANDS_NOT_AUTHORIZED are not gate states, and ARTIFACT_INVALID_* is a wildcard that cannot have one anchor. Change: a sibling lib/result-codes.ts with the same {anchor, summary, fix} shape and an enumerated artifact-error list; extend the anchor test to both tables; keep snake_case or document the SCREAMING_CASE convention for CLI-visible codes.

F10 — Medium. B4's scope field reverses a documented design without touching its guards. autoplan/sections/manifest.json:5 says the manifest is PASSIVE with "No machine predicate here"; test/qa-lazy-sections.test.ts:127 asserts passive resolution. Change: update the note and $schema, make scope optional with always default, and have the runner (F1) be the only consumer; interactive renders ignore it, as the plan says.

F11 — Medium. A2 relies on bun upgrade --version <pin>, which Bun 1.3.14 does not offer. bun upgrade --help on this machine lists only --canary. setup:296-305 already prints the checksum-verified BUN_VERSION=… bash install recipe. Change: the installer runs that recipe (download, print sha256, install to ~/.bun, re-exec), and A1's gstack runtime fix line quotes it.

F12 — Medium. Reading policy from origin/<base> has no shallow-clone or offline path. C1 reads pins from the committed base copy; gate VMs often have shallow or detached checkouts. Change: when origin/<base> is unresolvable, exit 3 with policy source unavailable — fix: git fetch origin <base> --depth=1 (POLICY_SOURCE_UNAVAILABLE); never fall back to the working tree silently. Also say plainly in docs/ship-policy.md that the base read protects the command string only (bun run release:restamp executes the PR's script), as C1 already concedes.

F13 — Medium. Gate-ahead worktree evidence is keyed by the wrong branch. Review and evidence ledgers are keyed by slug and branch (bin/gstack-review-log:18-22, :99; gstack-evidence ledger path, bin/gstack-evidence:13). A throwaway merge worktree with a synthetic branch writes under that name, so the receipt's gstack-evidence check on the real branch finds nothing and grades MISSING. Change: gate-ahead.json records the evidence branch key and gstack-tree-receipt --from reads it; or export GSTACK_REVIEW_BRANCH/evidence branch to the real PR branch inside the worktree.

F14 — Medium. Permitted version transforms need per-file-type rules and a negative fixture. C3 classifies hunks; the risk is an old VERSION (or its npm form) coinciding with an unrelated dependency version in package-lock.json/bun.lock or a doc. Change: lockfile rules match only the root package's version keys (syncNpmLockfiles already knows where they are, bin/gstack-version-bump:486); stamp_paths rules match (was X), since: X, vX only when X equals the old VERSION and the replacement equals the new one; fixture where a dependency pin equals the old version must classify as changed.

F15 — Medium. lanes cannot map bash bins to their tests through an import graph. test/gstack-doctor.test.ts spawns bin/gstack-doctor; no TS imports it. The precedent (test/helpers/touchfile-closure.ts:1-6, :20) already adds string-literal path matching. Change: lib/pregate/import-graph.ts unions static imports with repo-path literals and the bin/<name> → test/<name>*.test.ts naming convention, and reports "no lane found" as FAIL lanes — <file> has no test lane rather than passing.

F16 — Low. bin/gstack-hermetic-run would import test code. buildHermeticEnv lives in test/helpers/hermetic-env.ts:112 (554 lines); a shipped bin importing it crosses the product/test typecheck boundary (bun run typecheck vs typecheck:test). Change: move the env builder to lib/hermetic-env.ts, re-export from the helper.

F17 — Low. Validation 2 ties PR B's merge gate to Codex CLI. "a Codex login from OPENAI_API_KEY only" contradicts B3's acceptance of a Claude subagent as a full outside voice. Change: the gate accepts either runner and records which ran.

F18 — Low. Security surface of new pin files is named but not fixtured. mirror, stamp_paths, generated.json commands, pregate.json guards patterns and banned-terms.json are all repo-controlled inputs; containment (no .., no symlink escape, globs confined to the repo), command execution only under --allow-repo-commands, and regex-size caps for banned-terms/guards patterns need named fixtures. gstack-regen --check is still code execution even in a scratch copy and must sit behind the same opt-in.

F19 — Low. Windows subset. New bash bins and tests must be classified for scripts/test-free-shards.ts --windows-only; the plan is silent, and the windows lane is exactly the "unrun platform lane" incident C8 targets.

Required outputs

NOT in scope (confirmed and extended). The plan's "Not in this wave" list stands (no capy host, no semgrep/gitleaks install, no native Windows /cso, no issue closing, no stamping on main, no running platform lanes locally). Add: no telemetry transport for P7 (receipts carry it, F3); no change to headless BLOCK semantics or any eval case meaning; no Greptile-tier vocabulary (D8a); no automatic CARVE_GUARDS bumps (each is a human-visible commit).

What already exists (receipts). Doctor rows + exit codes (bin/gstack-doctor:3-16); session kinds and override (bin/gstack-session-kind:33-66); skill-start kind whitelist (bin/gstack-skill-start:93); AUQ branch rule (generate-ask-user-format.ts:15-29); state-root chain and ratchet (lib/state-root.ts:22-29, test/state-root-ratchet.test.ts); bindReview/reviewFreshness (lib/review-evidence.ts:178-243); review-log wrapper (bin/gstack-review-log:81-99); version-bump opt-in regen and half-write path (bin/gstack-version-bump:373-402, :483-495); evidence fingerprints and allow-list compare (bin/gstack-evidence:388-396, :498-512); render-time outside voice (outside-voice.ts:8-12, :217-230); spec-review stop rules (spec-review.ts:186-192); publication hook render + backstop (composition.ts:7-32, phase-publication-hook:1-15); gate-outcome contract (lib/gate-outcomes.ts:1-18); setup skip flags and status exit (setup:1512, :2071-2072, :136-146); Bun floor/tested (bin/gstack-bun-version.sh:16-17; package.json:80-81 >=1.4.2; windows-free-tests.yml:85-89); passive manifests (autoplan/sections/manifest.json:5); parity/context/skill-size ratchets (test/parity-suite.test.ts, test/context-budget-ratchet.test.ts, test/skill-size-budget.test.ts); module-size fixture (test/fixtures/module-size-ratchet.json: 47 + 4 entries, none of the three bins the plan names).

Completion Summary. The plan is well grounded (most receipts check out) and its decisions are right; it is under-specified where it matters most for P2 (how a native reviewer's output gets back into the run) and over-confident where the repo's ratchets are strictest (ship/review/autoplan/qa bytes). Two premises are wrong (module-size caps on the three bins; bun upgrade --version), one promise is unmeasurable as designed (P7), one safety change targets the wrong host (B1 guard), and one component sits in the wrong language for its own promise (doctor table). None of these change the decisions; they change PR B's shape and every PR's acceptance list. Unverified claims in the plan I could not check on this machine: the 86 s Chromium number, the 3.7 s Codex probe, gbrain's pre-gate timing, and the per-PR effort table (all [estimated]; the plan labels them so).

Dependencies and parallelization.

  • A is independent and lands first (D1). F5 and F11 are inside A.
  • B-core (B0, B1, B7, B9, B10, B12) depends only on A's GSTACK_LAUNCH line; B-runner (B2, B3, B4, B8, B11) depends on B-core's schemas and on F1/F2's bins. These two halves can be developed in parallel on fresh machines with B-runner stacked on B-core's branch.
  • C-stamp (C1, C2, C3, C5, C7) depends on B0 (receipt schema) only; C-pregate (C4, C8, C6) depends on C1 (pin trust boundary) and B0 (pregate.json). C-stamp and B-runner are disjoint and can run in parallel; C-pregate stacks on C-stamp.
  • D depends on B4 (scope) for tier 2 only; tier-1 rows can start after B-core. D's byte budget work (F6) is the long pole, not the code.
  • E tier 1 (E1 ledger + pilot) depends on nothing in B-D except lib/pricing.ts extraction; E2 bundle moved into C3. E tier 3 waits for P7 via F3's census.
  • Shared files with 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, serialized), test/fixtures/carve-guards/CARVE_GUARDS (one commit per PR, the PR's author).

Scores

  • Architecture: 6/10 — right layering (lib behind bin, pins, one validator) but F1/F2/F5 put three components in the wrong place or language.
  • Edge cases: 6/10 — queue and receipt edges are strong; session-kind, shallow-clone, worktree-key and bash-lane edges are missing.
  • Test plan: 7/10 — incident fixtures and the queue replay are the right instruments; missing the no-Bun doctor case, mixed-version kinds, concurrent restamp, Windows classification, and any P7 test.
  • Right-sized diff: 4/10 — A, D, E are sized; B and C are each two review sittings and the byte budget is unbudgeted.
  • Reversibility: 8/10 — every new behavior is behind a new kind, a new pin file, or a new flag; headless and interactive paths are untouched; guard change (F4) is the one irreversible-feeling edit and it should not ship as written.
  • Maintainability under ratchets: 5/10 — the plan names the ratchets but misidentifies which files they cap and does not budget the four zero-headroom skills.

Tasks

[
  {"id":"T1","priority":"P1","component":"autoplan-runner","title":"Add next/submit verbs to gstack-autoplan so a host-dispatched native reviewer can return results; run becomes a loop over next only when both voices are local","files":["bin/gstack-autoplan","lib/autoplan-run.ts","lib/headless-artifacts.ts","test/gstack-autoplan-run.test.ts"],"effort_human":"4d","effort_cc":"0.5d","source_finding":"F1"},
  {"id":"T2","priority":"P1","component":"outside-voice","title":"Create bin/gstack-outside-voice (codex-cli|api|host-subagent) and point the resolver prose at it instead of rendering runner branches","files":["bin/gstack-outside-voice","lib/outside-voice-runner.ts","lib/outside-voice-api.ts","scripts/resolvers/outside-voice.ts","scripts/resolvers/outside-voice-steps.ts","test/gstack-outside-voice.test.ts"],"effort_human":"3d","effort_cc":"0.5d","source_finding":"F2"},
  {"id":"T3","priority":"P1","component":"adoption","title":"Carry P7 in run.json.consumed_by and the ship receipt; add gstack-ship-receipt census over merged PR bodies; label state-root analytics durable=no","files":["lib/headless-artifacts.ts","bin/gstack-artifact","bin/gstack-ship-receipt","docs/unattended.md","test/gstack-ship-receipt.test.ts"],"effort_human":"2d","effort_cc":"0.25d","source_finding":"F3"},
  {"id":"T4","priority":"P1","component":"autoplan-guard","title":"Keep deny-on-absent in the Claude render; emit GUARD_NOT_INSTALLED from gstack-skill-start for unattended/non-hook hosts and require it in review-record.md","files":["scripts/resolvers/composition.ts","bin/gstack-skill-start","lib/headless-artifacts.ts","test/autoplan-publication-guard.test.ts","test/gstack-skill-start.test.ts"],"effort_human":"1d","effort_cc":"0.15d","source_finding":"F4"},
  {"id":"T5","priority":"P1","component":"doctor","title":"Component table as sourced bash (or build-time generated from TS with a freshness test); --check fixture with bun removed from PATH","files":["bin/gstack-doctor","bin/gstack-doctor-components.sh","lib/doctor-components.ts","test/gstack-doctor.test.ts"],"effort_human":"2d","effort_cc":"0.25d","source_finding":"F5"},
  {"id":"T6","priority":"P1","component":"budgets","title":"Per-PR union-byte delta and CARVE_GUARDS/capture-context-budget refresh in acceptance; B4 checklists replace rather than add; new eval-plan ceiling","files":["test/helpers/carve-guards.ts","test/fixtures/context-budget*.json","docs/designs/multi-agent-wave-2026-10-10/ACCEPTANCE.md"],"effort_human":"1d per PR","effort_cc":"0.1d per PR","source_finding":"F6"},
  {"id":"T7","priority":"P1","component":"session-kind","title":"Whitelist unattended in gstack-skill-start; loud fallback for unknown kinds; state override-vs-HEADLESS precedence and test both-set","files":["bin/gstack-skill-start","bin/gstack-session-kind","test/gstack-skill-start.test.ts","test/gstack-session-kind.test.ts"],"effort_human":"0.5d","effort_cc":"0.1d","source_finding":"F7"},
  {"id":"T8","priority":"P2","component":"review-log","title":"Adopt exit table (usage 2, rejected 1 with REVIEW_STATUS_MISMATCH); pass bindReview stderr through the wrapper","files":["bin/gstack-review-log","lib/review-evidence.ts","test/review-log.test.ts"],"effort_human":"0.5d","effort_cc":"0.1d","source_finding":"F8"},
  {"id":"T9","priority":"P2","component":"reason-codes","title":"lib/result-codes.ts with enumerated artifact/queue/policy codes and anchors; extend troubleshooting-anchors test","files":["lib/result-codes.ts","lib/gate-outcomes.ts","docs/troubleshooting.md","test/troubleshooting-anchors.test.ts"],"effort_human":"0.5d","effort_cc":"0.1d","source_finding":"F9"},
  {"id":"T10","priority":"P2","component":"sections-manifest","title":"Optional scope field with updated passive-registry note and schema; runner is the only consumer; update qa-lazy-sections test","files":["autoplan/sections/manifest.json","plan-*-review/sections/manifest.json","scripts/resolvers/sections.ts","test/qa-lazy-sections.test.ts"],"effort_human":"0.5d","effort_cc":"0.1d","source_finding":"F10"},
  {"id":"T11","priority":"P2","component":"installer","title":"Replace bun upgrade --version with setup's checksum-verified BUN_VERSION install recipe; doctor fix line quotes it","files":["bin/gstack-capy-install","bin/gstack-doctor","test/gstack-capy-install.test.ts"],"effort_human":"0.5d","effort_cc":"0.1d","source_finding":"F11"},
  {"id":"T12","priority":"P2","component":"ship-policy","title":"POLICY_SOURCE_UNAVAILABLE (exit 3) when origin/<base> is unresolvable; never fall back to the working tree; document the command-string-only trust boundary","files":["lib/ship-policy.ts","docs/ship-policy.md","test/ship-policy.test.ts"],"effort_human":"0.5d","effort_cc":"0.1d","source_finding":"F12"},
  {"id":"T13","priority":"P2","component":"restamp","title":"Record the evidence branch key in gate-ahead.json and read it in the receipt; concurrent-restamp lock on the journal","files":["lib/restamp.ts","lib/tree-receipt.ts","test/gstack-restamp-queue.test.ts"],"effort_human":"1d","effort_cc":"0.15d","source_finding":"F13"},
  {"id":"T14","priority":"P2","component":"tree-receipt","title":"Per-file-type permitted transforms (root package version keys only in lockfiles) and a dep-equals-old-version negative fixture","files":["lib/tree-receipt.ts","test/gstack-tree-receipt.test.ts","test/fixtures/multi-agent-wave/p0-3-dep-equals-old-version/"],"effort_human":"1d","effort_cc":"0.15d","source_finding":"F14"},
  {"id":"T15","priority":"P2","component":"pregate","title":"Import graph unions repo-path literals and bin→test naming; 'no lane found' is FAIL; cache graph by HEAD tree","files":["lib/pregate/import-graph.ts","lib/pregate/lanes.ts","test/pregate-lanes.test.ts"],"effort_human":"1.5d","effort_cc":"0.2d","source_finding":"F15"},
  {"id":"T16","priority":"P3","component":"pregate","title":"Move buildHermeticEnv to lib/hermetic-env.ts and re-export from the test helper","files":["lib/hermetic-env.ts","test/helpers/hermetic-env.ts","bin/gstack-hermetic-run"],"effort_human":"0.5d","effort_cc":"0.1d","source_finding":"F16"},
  {"id":"T17","priority":"P3","component":"validation","title":"PR B merge gate accepts either outside runner and records it","files":["docs/designs/multi-agent-wave-2026-10-10/ACCEPTANCE.md"],"effort_human":"0.1d","effort_cc":"0.05d","source_finding":"F17"},
  {"id":"T18","priority":"P2","component":"pin-security","title":"Containment, opt-in and regex-size fixtures for ship-policy mirror/stamp_paths, generated.json, pregate.json guards, banned-terms.json","files":["lib/ship-policy.ts","lib/regen.ts","lib/pregate/index.ts","bin/gstack-banned-terms","test/ship-policy.test.ts","test/gstack-regen.test.ts"],"effort_human":"1d","effort_cc":"0.15d","source_finding":"F18"},
  {"id":"T19","priority":"P3","component":"windows-lane","title":"Classify every new bash bin and test for the curated Windows subset; add durations entries","files":["scripts/test-free-shards.ts","scripts/free-test-durations.json","scripts/free-test-durations-windows.json"],"effort_human":"0.5d","effort_cc":"0.1d","source_finding":"F19"},
  {"id":"T20","priority":"P1","component":"delivery","title":"Split B into B-core/B-runner and C into C-stamp/C-pregate (seven PRs, stacked); record as decision E1 with the two-gate cost","files":["docs/designs/multi-agent-wave-2026-10-10/ACCEPTANCE.md"],"effort_human":"0.2d","effort_cc":"0.05d","source_finding":"Scope Challenge"}
]

Auto-decided (recommended options, recorded, no questions asked): UC1(a), D1(a) amended by T20 (seven PRs: A, B-core, B-runner, C-stamp, C-pregate, D, E), D2(a), D3(a), D4(a) with F12's refusal, D5(a), D6(a), D7(a), D8(a), D9(a), D10(a), D11(a) with F14's per-file-type rules. New decision E1 (PR split) recommended (a) as above; (b) keep five PRs and accept two review sittings per PR.

Recommendation: revise-then-proceed because the decisions are sound and most receipts hold, but PR B cannot meet P2 without a submit/next verb and a runtime outside-voice bin (F1, F2), P7 is unmeasurable as designed (F3), the guard change targets the wrong host (F4), the doctor table is in a language the P1 scenario lacks (F5), and four skills have zero parity headroom for the prose every later PR adds (F6).