diff --git a/ship/DRIVER.md b/ship/DRIVER.md index 33a9f442b..3986969cc 100644 --- a/ship/DRIVER.md +++ b/ship/DRIVER.md @@ -29,7 +29,7 @@ file, unchanged; its findings reach `merged.jsonl` only via `collect`; `judge` r small model at low effort; their findings go to the PR body. **Calls.** One pass with no fixes is 9 calls: Phases 1-8 and the report; at most 12 with a STOP's -section read or a fix-check, plus a keep-alive per 200 s waited. A fix repeats Phases 3-4. +section read or a fix-check, plus a keep-alive per 200 s waited. **Budget: observe.** `gstack-ship-budget start --mode observe` records, never admits: each program ends with `gstack-ship-budget note --step --reading R --credits C --record `, R and C read in that program @@ -89,8 +89,8 @@ Steps 8.1 and 8.2 (`Scope Check:`) on the todos and commits files, then the core `JUDGMENT: ok` before any fix), fixes and persistence in one program. Prints `Pre-Landing Review:`, `Gate edits:`. `NEXT: stop` stops; `full` or `scoped`: one program runs the failing test, fix, `--tests-only` and Phase 3, each gated; `done`: the same program runs Step 11 from -`sections/adversarial.md` as written (above), only the voices the last full-pass plan lists (neither -prints `ADVERSARIAL: skipped (review plan)`). +`sections/adversarial.md` as written (above), only the voices the last full-pass plan lists (none: +`ADVERSARIAL: skipped (review plan)`). ## 5. Voices, bind and version: Steps 11, 11.5, 12 @@ -102,7 +102,7 @@ approvals; `VERSION: skipped|kept`: no write. ## 6. Notes and documentation: Steps 13, 14, 14.5 -Step 13: one user-facing entry from the commits file, where `NEXT:` says; +Step 13: one user-facing entry from the commits file where `NEXT:` says; `gstack-prose-lint --base origin/ --gate` until `PROSE_LINT: ok`. Step 14: TODOS.md deferrals and completions, edited from the todos file. Step 14.5, every ship: read `sections/documentation.md` and run its audit as written, as one program (above), then `gstack-ship-receipt docs-caps --base `; print `Documentation:`. @@ -119,7 +119,7 @@ STALE or MISSING reruns once), `PREGATE:`; print them, the last `Gate edits:` an ## 8. Publish: Steps 18, 19, 19-C, 20, 21, 22 - Step 18: the open-PR lookup; a title file starting `v `. `UNATTENDED_SHIP:` with no open PR at HEAD takes Step 18-H. -- Step 19 body: the receipt block, then the sections `sections/pr-body.md` lists (Pre-Landing Review: `REVIEW_LOOP:`, `Gate edits:`, deferred, notes), led under `CAPY_PR: on` (Step 19-C) by What changed, Why, Blast radius and Approvals (`gstack-approvals render`). `gstack-prose-lint --sections 'Why,Summary,Blast radius' --gate`; `gstack-issue-links`; `gstack-review-scope summary --since "$REVIEW_SINCE" && gstack-ship-receipt write --pr --base --gate @ --pregate --issues @ --review-loop @ --docs @ --body `; `gstack-post pr-body` and `pr-title` (a new PR via the host's PR tool; a draft while a gate finding is `open`). `SHIP_RECEIPT:` opens your report. +- Step 19 body: the receipt block, then the sections `sections/pr-body.md` lists (Pre-Landing Review: `REVIEW_LOOP:`, `Gate edits:`, deferred, notes), led under `CAPY_PR: on` (Step 19-C) by What changed, Why, Blast radius and Approvals (`gstack-approvals render`). `gstack-prose-lint --sections 'Why,Summary,Blast radius' --gate`; `gstack-issue-links`; `gstack-review-scope summary --since "$REVIEW_SINCE" && gstack-ship-receipt write --pr --base --gate @ --pregate --issues @ --review-loop @ --docs @ [--review-targeted @.gstack/tmp/targeted.json] --body `; `gstack-post pr-body` and `pr-title` (new PR: the host's PR tool; draft while a gate finding is `open`). `SHIP_RECEIPT:` opens your report. - Step 20: the ship metrics row via `gstack-review-log`. - Step 22: `gstack-ship-budget close --reading R --credits C` (`BUDGET_FINAL:` and `BUDGET_TIMELINE:` carry tokens and credits; report both), then `gstack-ship-receipt result --pr `: `GSTACK_RESULT: skill=ship status=complete run=`. Under a merge queue, `gstack-ship-queue ready --pr [--screen|--no-screen]` at the pushed head; paste its output. diff --git a/ship/DRIVER.md.tmpl b/ship/DRIVER.md.tmpl index 5922c4599..8cfd77499 100644 --- a/ship/DRIVER.md.tmpl +++ b/ship/DRIVER.md.tmpl @@ -27,7 +27,7 @@ file, unchanged; its findings reach `merged.jsonl` only via `collect`; `judge` r small model at low effort; their findings go to the PR body. **Calls.** One pass with no fixes is 9 calls: Phases 1-8 and the report; at most 12 with a STOP's -section read or a fix-check, plus a keep-alive per 200 s waited. A fix repeats Phases 3-4. +section read or a fix-check, plus a keep-alive per 200 s waited. **Budget: observe.** `gstack-ship-budget start --mode observe` records, never admits: each program ends with `gstack-ship-budget note --step --reading R --credits C --record `, R and C read in that program @@ -87,8 +87,8 @@ Steps 8.1 and 8.2 (`Scope Check:`) on the todos and commits files, then the core `JUDGMENT: ok` before any fix), fixes and persistence in one program. Prints `Pre-Landing Review:`, `Gate edits:`. `NEXT: stop` stops; `full` or `scoped`: one program runs the failing test, fix, `--tests-only` and Phase 3, each gated; `done`: the same program runs Step 11 from -`sections/adversarial.md` as written (above), only the voices the last full-pass plan lists (neither -prints `ADVERSARIAL: skipped (review plan)`). +`sections/adversarial.md` as written (above), only the voices the last full-pass plan lists (none: +`ADVERSARIAL: skipped (review plan)`). ## 5. Voices, bind and version: Steps 11, 11.5, 12 @@ -100,7 +100,7 @@ approvals; `VERSION: skipped|kept`: no write. ## 6. Notes and documentation: Steps 13, 14, 14.5 -Step 13: one user-facing entry from the commits file, where `NEXT:` says; +Step 13: one user-facing entry from the commits file where `NEXT:` says; `gstack-prose-lint --base origin/ --gate` until `PROSE_LINT: ok`. Step 14: TODOS.md deferrals and completions, edited from the todos file. Step 14.5, every ship: read `sections/documentation.md` and run its audit as written, as one program (above), then `gstack-ship-receipt docs-caps --base `; print `Documentation:`. @@ -117,7 +117,7 @@ STALE or MISSING reruns once), `PREGATE:`; print them, the last `Gate edits:` an ## 8. Publish: Steps 18, 19, 19-C, 20, 21, 22 - Step 18: the open-PR lookup; a title file starting `v `. `UNATTENDED_SHIP:` with no open PR at HEAD takes Step 18-H. -- Step 19 body: the receipt block, then the sections `sections/pr-body.md` lists (Pre-Landing Review: `REVIEW_LOOP:`, `Gate edits:`, deferred, notes), led under `CAPY_PR: on` (Step 19-C) by What changed, Why, Blast radius and Approvals (`gstack-approvals render`). `gstack-prose-lint --sections 'Why,Summary,Blast radius' --gate`; `gstack-issue-links`; `gstack-review-scope summary --since "$REVIEW_SINCE" && gstack-ship-receipt write --pr --base --gate @ --pregate --issues @ --review-loop @ --docs @ --body `; `gstack-post pr-body` and `pr-title` (a new PR via the host's PR tool; a draft while a gate finding is `open`). `SHIP_RECEIPT:` opens your report. +- Step 19 body: the receipt block, then the sections `sections/pr-body.md` lists (Pre-Landing Review: `REVIEW_LOOP:`, `Gate edits:`, deferred, notes), led under `CAPY_PR: on` (Step 19-C) by What changed, Why, Blast radius and Approvals (`gstack-approvals render`). `gstack-prose-lint --sections 'Why,Summary,Blast radius' --gate`; `gstack-issue-links`; `gstack-review-scope summary --since "$REVIEW_SINCE" && gstack-ship-receipt write --pr --base --gate @ --pregate --issues @ --review-loop @ --docs @ [--review-targeted @.gstack/tmp/targeted.json] --body `; `gstack-post pr-body` and `pr-title` (new PR: the host's PR tool; draft while a gate finding is `open`). `SHIP_RECEIPT:` opens your report. - Step 20: the ship metrics row via `gstack-review-log`. - Step 22: `gstack-ship-budget close --reading R --credits C` (`BUDGET_FINAL:` and `BUDGET_TIMELINE:` carry tokens and credits; report both), then `gstack-ship-receipt result --pr `: `GSTACK_RESULT: skill=ship status=complete run=`. Under a merge queue, `gstack-ship-queue ready --pr [--screen|--no-screen]` at the pushed head; paste its output. diff --git a/ship/sections/adversarial.md b/ship/sections/adversarial.md index 0bc8600b3..374905515 100644 --- a/ship/sections/adversarial.md +++ b/ship/sections/adversarial.md @@ -5,7 +5,7 @@ `DISPATCH: outside-voice`. Without the native line (light depth at runtime 0; above 0 the plan lists it beside the batched specialist), print `ADVERSARIAL: skipped (review plan)`; with neither line, take decision 3 of Finish the adversarial phase. On `DISPATCH: targeted`, also run the targeted reader -(`gstack-review-dispatch render --role targeted --classes --base --diff-base `, the merge base on every pass, never RANGE_BASE) like the voices; `verify --targeted-out` feeds the receipt's `--review-targeted`. Under a token cap, +(`gstack-review-dispatch render --role targeted --classes --base --diff-base `, the merge base on every pass, never RANGE_BASE) like the voices; `verify --targeted-out .gstack/tmp/targeted.json` feeds the receipt's `--review-targeted`. Under a token cap, reserve the planned voices as one batch (`gstack-ship-budget check --next dispatch:`) and add `--reservation ` to the `--start adversarial-review` call below. diff --git a/ship/sections/adversarial.md.tmpl b/ship/sections/adversarial.md.tmpl index ea99ddfdd..c437eeb37 100644 --- a/ship/sections/adversarial.md.tmpl +++ b/ship/sections/adversarial.md.tmpl @@ -3,7 +3,7 @@ `DISPATCH: outside-voice`. Without the native line (light depth at runtime 0; above 0 the plan lists it beside the batched specialist), print `ADVERSARIAL: skipped (review plan)`; with neither line, take decision 3 of Finish the adversarial phase. On `DISPATCH: targeted`, also run the targeted reader -(`gstack-review-dispatch render --role targeted --classes --base --diff-base `, the merge base on every pass, never RANGE_BASE) like the voices; `verify --targeted-out` feeds the receipt's `--review-targeted`. Under a token cap, +(`gstack-review-dispatch render --role targeted --classes --base --diff-base `, the merge base on every pass, never RANGE_BASE) like the voices; `verify --targeted-out .gstack/tmp/targeted.json` feeds the receipt's `--review-targeted`. Under a token cap, reserve the planned voices as one batch (`gstack-ship-budget check --next dispatch:`) and add `--reservation ` to the `--start adversarial-review` call below. diff --git a/ship/sections/handoff.md b/ship/sections/handoff.md index 55ea33fba..83f85320f 100644 --- a/ship/sections/handoff.md +++ b/ship/sections/handoff.md @@ -13,6 +13,7 @@ unattended run may do is docs/unattended.md#what-an-unattended-session-may-do. ~/.claude/skills/gstack/bin/gstack-issue-links --base --out .gstack/tmp/issues.json ~/.claude/skills/gstack/bin/gstack-ship-receipt write --base --gate @.gstack/tmp/ship-gate.json --issues @.gstack/tmp/issues.json --out .gstack/tmp/ship-receipt.md ``` + When Step 11 saved `.gstack/tmp/targeted.json`, add `--review-targeted @.gstack/tmp/targeted.json` to that write. 2. Send the completion message, identifiers first, in this order: the `SHIP_RECEIPT:` line and receipt block; `branch:` and `sha:` (`git rev-parse HEAD`), pushed or `not pushed`; `inputs:` the plan or issue, the base SHA and the spend diff --git a/ship/sections/handoff.md.tmpl b/ship/sections/handoff.md.tmpl index 273bad052..2c38144fc 100644 --- a/ship/sections/handoff.md.tmpl +++ b/ship/sections/handoff.md.tmpl @@ -11,6 +11,7 @@ unattended run may do is docs/unattended.md#what-an-unattended-session-may-do. ~/.claude/skills/gstack/bin/gstack-issue-links --base --out .gstack/tmp/issues.json ~/.claude/skills/gstack/bin/gstack-ship-receipt write --base --gate @.gstack/tmp/ship-gate.json --issues @.gstack/tmp/issues.json --out .gstack/tmp/ship-receipt.md ``` + When Step 11 saved `.gstack/tmp/targeted.json`, add `--review-targeted @.gstack/tmp/targeted.json` to that write. 2. Send the completion message, identifiers first, in this order: the `SHIP_RECEIPT:` line and receipt block; `branch:` and `sha:` (`git rev-parse HEAD`), pushed or `not pushed`; `inputs:` the plan or issue, the base SHA and the spend diff --git a/ship/sections/pr-body.md b/ship/sections/pr-body.md index c25c35999..f994062b7 100644 --- a/ship/sections/pr-body.md +++ b/ship/sections/pr-body.md @@ -221,7 +221,7 @@ per-lane counts (`{"":{"pass":N,"fail":N,"skip":N}}`) to ```bash rm -f .gstack/tmp/review-loop.json ~/.claude/skills/gstack/bin/gstack-review-scope summary --since "$REVIEW_SINCE" --out .gstack/tmp/review-loop.json && -~/.claude/skills/gstack/bin/gstack-ship-receipt write --pr --base --gate @.gstack/tmp/ship-gate.json --pregate .gstack/tmp/pregate.json --issues @.gstack/tmp/issues.json --review-loop @.gstack/tmp/review-loop.json --docs @.gstack/tmp/docs-audit.json --body "$PR_BODY_FILE" +~/.claude/skills/gstack/bin/gstack-ship-receipt write --pr --base --gate @.gstack/tmp/ship-gate.json --pregate .gstack/tmp/pregate.json --issues @.gstack/tmp/issues.json --review-loop @.gstack/tmp/review-loop.json --docs @.gstack/tmp/docs-audit.json $([ -s .gstack/tmp/targeted.json ] && echo --review-targeted @.gstack/tmp/targeted.json) --body "$PR_BODY_FILE" ``` `--docs` reads the record Step 14.5 saved (`{status, scope[, exception]}`): the diff --git a/ship/sections/pr-body.md.tmpl b/ship/sections/pr-body.md.tmpl index 46b2a7d3c..26800bbf0 100644 --- a/ship/sections/pr-body.md.tmpl +++ b/ship/sections/pr-body.md.tmpl @@ -204,7 +204,7 @@ per-lane counts (`{"":{"pass":N,"fail":N,"skip":N}}`) to ```bash rm -f .gstack/tmp/review-loop.json ~/.claude/skills/gstack/bin/gstack-review-scope summary --since "$REVIEW_SINCE" --out .gstack/tmp/review-loop.json && -~/.claude/skills/gstack/bin/gstack-ship-receipt write --pr --base --gate @.gstack/tmp/ship-gate.json --pregate .gstack/tmp/pregate.json --issues @.gstack/tmp/issues.json --review-loop @.gstack/tmp/review-loop.json --docs @.gstack/tmp/docs-audit.json --body "$PR_BODY_FILE" +~/.claude/skills/gstack/bin/gstack-ship-receipt write --pr --base --gate @.gstack/tmp/ship-gate.json --pregate .gstack/tmp/pregate.json --issues @.gstack/tmp/issues.json --review-loop @.gstack/tmp/review-loop.json --docs @.gstack/tmp/docs-audit.json $([ -s .gstack/tmp/targeted.json ] && echo --review-targeted @.gstack/tmp/targeted.json) --body "$PR_BODY_FILE" ``` `--docs` reads the record Step 14.5 saved (`{status, scope[, exception]}`): the diff --git a/ship/sections/review-army.md b/ship/sections/review-army.md index 450f3adae..d9f26e3a8 100644 --- a/ship/sections/review-army.md +++ b/ship/sections/review-army.md @@ -3,7 +3,7 @@ ## Step 9: Pre-Landing Review Set CYCLES to 0 on first entry only, with PASS set to 1, -`REVIEW_SINCE=$(date -u +%Y-%m-%dT%H:%M:%SZ)` saved and the `ship: review_scope` +`REVIEW_SINCE=$(date -u +%Y-%m-%dT%H:%M:%SZ)` saved, `.gstack/tmp/targeted.json` removed, and the `ship: review_scope` policy line (Step 3) kept. Every entry saves `PASS_HEAD=$(git rev-parse HEAD)`; from pass 2 on, `RANGE_BASE` is the previous pass's PASS_HEAD. Keep existing approvals; changed finding scope needs a new decision. Order: checklist/design, specialists (9.1), diff --git a/ship/sections/review-army.md.tmpl b/ship/sections/review-army.md.tmpl index 8b349c4e9..04f048ca8 100644 --- a/ship/sections/review-army.md.tmpl +++ b/ship/sections/review-army.md.tmpl @@ -1,7 +1,7 @@ ## Step 9: Pre-Landing Review Set CYCLES to 0 on first entry only, with PASS set to 1, -`REVIEW_SINCE=$(date -u +%Y-%m-%dT%H:%M:%SZ)` saved and the `ship: review_scope` +`REVIEW_SINCE=$(date -u +%Y-%m-%dT%H:%M:%SZ)` saved, `.gstack/tmp/targeted.json` removed, and the `ship: review_scope` policy line (Step 3) kept. Every entry saves `PASS_HEAD=$(git rev-parse HEAD)`; from pass 2 on, `RANGE_BASE` is the previous pass's PASS_HEAD. Keep existing approvals; changed finding scope needs a new decision. Order: checklist/design, specialists (9.1), diff --git a/test/review-targeted.test.ts b/test/review-targeted.test.ts index 3f0f69859..33ed9cd3b 100644 --- a/test/review-targeted.test.ts +++ b/test/review-targeted.test.ts @@ -320,6 +320,41 @@ describe('the targeted role end to end on a fixture repository', () => { }, 180_000); }); +describe('the /ship commands carry the targeted record to the receipt (GST-464)', () => { + // Value: protects=when the targeted reader ran, the receipt /ship writes carries its record, so ready prints verdict=ok instead + // of receipt=legacy (gstack-evals#95, #3682, #3612); fails_when=Step 11's verify writes the record nowhere Step 19 reads, the + // Step 19 or handoff write command omits --review-targeted, or a record from an earlier run survives Step 9; seam=the generated + // sections' own command text, expanded by bash and passed to gstack-ship-receipt write; no_claim=enforcement (report-only) + test('Step 11 verify writes the record where Step 19 write reads it, a run without the reader writes none, and Step 9 clears it', () => { + const section = (name: string) => fs.readFileSync(path.join(ROOT, 'ship', 'sections', `${name}.md`), 'utf8'); + const recordPath = /verify --targeted-out (\S+?)`/.exec(section('adversarial'))?.[1]; + expect(recordPath).toBe('.gstack/tmp/targeted.json'); + const writeLine = section('pr-body').split('\n').find(l => l.includes('gstack-ship-receipt write --pr'))!; + const optional = /\$\(\[ -s (\S+) \] && echo (--review-targeted @\S+)\)/.exec(writeLine); + expect(optional?.slice(1)).toEqual([recordPath!, `--review-targeted @${recordPath}`]); + expect(section('handoff')).toContain(`When Step 11 saved \`${recordPath}\`, add \`--review-targeted @${recordPath}\` to that write.`); + expect(fs.readFileSync(path.join(ROOT, 'ship', 'DRIVER.md'), 'utf8')).toContain(`[--review-targeted @${recordPath}]`); + expect(section('review-army')).toContain(`\`${recordPath}\` removed`); + + const repo = makeFixtureRepo({ prefix: 'targeted-wire-' }); + repos.push(repo); + repo.git('checkout', '-q', '-b', 'feat'); + fs.mkdirSync(path.join(repo.work, 'src'), { recursive: true }); + fs.writeFileSync(path.join(repo.work, 'src', 'charge.ts'), 'export const spend_cap_usd = 5;\n'); + repo.git('add', '-A'); repo.git('commit', '-q', '-m', 'charge'); + const expand = () => spawnSync('bash', ['-c', `printf '%s\\n' ${optional![0]}`], { cwd: repo.work, encoding: 'utf8', timeout: 10_000 }).stdout.split('\n').filter(Boolean); + expect(expand()).toEqual([]); + fs.mkdirSync(path.join(repo.work, '.gstack', 'tmp'), { recursive: true }); + fs.writeFileSync(path.join(repo.work, recordPath!), JSON.stringify({ classes: ['money'], rules: TARGETED_RULES, brief_sha: targetedBriefSha(), status: 'done', findings: 0, raised_by: 'targeted' }) + '\n'); + const args = expand(); + expect(args).toEqual(['--review-targeted', `@${recordPath}`]); + const write = runBin(repo, 'gstack-ship-receipt', ['write', '--base', 'main', '--session-kind', 'unattended', ...args, '--json']); + expect(write.status, write.err).toBe(0); + const receipt = JSON.parse(write.out) as ShipReceipt; + expect(receiptTargetedLine(['money'], receipt)).toBe('REVIEW_TARGETED: required=money receipt=money verdict=ok'); + }); +}); + describe('the receipt record and the queue line', () => { const record = { classes: ['gate', 'money'], rules: TARGETED_RULES, brief_sha: 'a'.repeat(64), status: 'done', findings: 0, raised_by: 'targeted' }; test('parseReviewTargeted binds the receipt\'s product patch, orders the classes and refuses a malformed record', () => { diff --git a/test/size-ledger/ship/gst-464-receipt-targeted-base.json b/test/size-ledger/ship/gst-464-receipt-targeted-base.json new file mode 100644 index 000000000..8a501cb6f --- /dev/null +++ b/test/size-ledger/ship/gst-464-receipt-targeted-base.json @@ -0,0 +1,4 @@ +{ + "bytes": 270, + "why": "GST-464: Step 11 saves the targeted record and the Step 19 and handoff receipt writes pass it; Step 9 clears it" +}