fix(ship): a scoped pass keeps the merge base for its pending Step 11 voices (GST-619 model; #3670 merge fix-check)

The driver passed diffBase: RANGE_BASE on scoped passes too, and --diff-base won in pass(), so the voices read only the fix range. pass() now ignores --diff-base on a scoped step, and the driver and Capy skill name diffBase for full repeat passes only.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-11-01 02:56:26 +00:00
1 parent 857c4b6745
commit 7e9d544680
7 files changed
+26 -6

No files matched your search

+1 -1
View File
@@ -188,7 +188,7 @@ function pass(): number {
const rangeBase = step === 'scoped' ? git(['rev-parse', '--verify', '--quiet', `${p.fix_range?.base ?? ''}^{commit}`])?.trim() : undefined;
if (step === 'scoped' && !rangeBase) throw new BriefError('a scoped plan names no readable fix range base');
const mergeBase = forkPoint(base);
const diffBase = flag('--diff-base') ?? (planArgs.includes('--adversarial-ran') ? rangeBase : undefined) ?? mergeBase;
const diffBase = (step === 'scoped' ? undefined : flag('--diff-base')) ?? (planArgs.includes('--adversarial-ran') ? rangeBase : undefined) ?? mergeBase;
if (!diffBase) throw new BriefError(`no merge base between ${base} and HEAD`);
const root = git(['rev-parse', '--show-toplevel'])?.trim() ?? process.cwd();
const extrasFile = flag('--extras');
+1 -1
View File
@@ -46,7 +46,7 @@ Capy checklist for `/autoplan`: drive `gstack-autoplan`, never a hand adaptation
Token caps on /ship: pass a brief's cap as `--token-cap <n>`. /ship reads `cost.tokens` and `cost.credits` from `tools.thread.get({ thread_id: "<your own jam id>" })` (its own jam, subagents included; `"this"` names the root thread, so only a root run may pass it) inside the same execute program as the `gstack-ship-budget` command that takes them as `--reading <n> --credits <c>`, never in a call of its own. `ship/DRIVER.md` starts `--mode observe`: each program ends with `note`, and only a note past the cap stops (`docs/ship-budget.md`).
/ship on Capy is about a dozen calls, each one execute program (`ship/DRIVER.md`). Each review pass and Step 11 is one call to the checked-in pass program (fill only `input`): `const t0 = await now(); const p = (await tools.capy.bash({ command: ". '<launch file>' && gstack-review-dispatch program", description: "Pass" })).output.split("\n"); return p[0] + "\n" + (await new (Object.getPrototypeOf(async () => {}).constructor)("tools", "sleep", "now", "input", JSON.parse(p[1]))(tools, sleep, now, { startedAt: t0, launch: "<launch file>", cwd: "<repo>", base: "<base>", out: "<run dir>/p<n>", pass: "<run>-p<n>", laneModel: "<your model id>", planFlags: [], logStart: ["review"], jam: "<jam id>", record: "<rec>", step: "9.2" }));` It plans, logs `logStart` (`reservation` mode from `RESERVATION_ID:`), sends each brief on `machine: "shared"`, then polls: one `tools.subagent.list` per ~20 s tick, returning 200 s after the program's first line, where a reader is settled only at status `done` or `failed` or with a `summary` (a just-created `idle` reader has not run); it starts readers under `rejected` with `tools.subagent.start` as slots free and stops on one stuck 20 minutes. It ends on `NEXT:` (`poll`: rerun) or `STOP:`. `models` maps roles to models (`outside-voice`: another family); Step 11 adds `stage: "voices"`; a repeat pass, `diffBase: "<RANGE_BASE>"`. Steps 7, 8, 9.2.1 and 14.5 (`step: "14.5"`) use `extras`, JSON `{name, prompt_file, first, model}` (prompts verbatim from their sections, Step 7 `first`); they cannot block, so `model: { model_id: "anthropic/claude-sonnet-5-5", reasoning_mode: "low" }` (`anthropic/claude-haiku-5-5` also fits). Waits: never let 5 minutes pass between calls. While readers run, never end the turn on `wait`; rerun the loader. Otherwise re-issue `wait` with `duration_seconds: 200` and operation ids. At Step 22 report the `CALLS:` line of `gstack-ship-budget calls --events <file>` on your jam's transcript since /ship began. A small waiting child saves nothing.
/ship on Capy is about a dozen calls, each one execute program (`ship/DRIVER.md`). Each review pass and Step 11 is one call to the checked-in pass program (fill only `input`): `const t0 = await now(); const p = (await tools.capy.bash({ command: ". '<launch file>' && gstack-review-dispatch program", description: "Pass" })).output.split("\n"); return p[0] + "\n" + (await new (Object.getPrototypeOf(async () => {}).constructor)("tools", "sleep", "now", "input", JSON.parse(p[1]))(tools, sleep, now, { startedAt: t0, launch: "<launch file>", cwd: "<repo>", base: "<base>", out: "<run dir>/p<n>", pass: "<run>-p<n>", laneModel: "<your model id>", planFlags: [], logStart: ["review"], jam: "<jam id>", record: "<rec>", step: "9.2" }));` It plans, logs `logStart` (`reservation` mode from `RESERVATION_ID:`), sends each brief on `machine: "shared"`, then polls: one `tools.subagent.list` per ~20 s tick, returning 200 s after the program's first line, where a reader is settled only at status `done` or `failed` or with a `summary` (a just-created `idle` reader has not run); it starts readers under `rejected` with `tools.subagent.start` as slots free and stops on one stuck 20 minutes. It ends on `NEXT:` (`poll`: rerun) or `STOP:`. `models` maps roles to models (`outside-voice`: another family); Step 11 adds `stage: "voices"`; a full repeat pass, `diffBase: "<RANGE_BASE>"`. Steps 7, 8, 9.2.1 and 14.5 (`step: "14.5"`) use `extras`, JSON `{name, prompt_file, first, model}` (prompts verbatim from their sections, Step 7 `first`); they cannot block, so `model: { model_id: "anthropic/claude-sonnet-5-5", reasoning_mode: "low" }` (`anthropic/claude-haiku-5-5` also fits). Waits: never let 5 minutes pass between calls. While readers run, never end the turn on `wait`; rerun the loader. Otherwise re-issue `wait` with `duration_seconds: 200` and operation ids. At Step 22 report the `CALLS:` line of `gstack-ship-budget calls --events <file>` on your jam's transcript since /ship began. A small waiting child saves nothing.
## Plan, approve, ship
+1 -1
View File
@@ -13,4 +13,4 @@
- **Step 11 records a start for each row it logs.** Two `logStart` entries for one skill (the native and outside `adversarial-review` rows) now each take their own `gstack-review-log --start` token. Before, the second was dropped as a repeat.
- **The pass finds its fork point against origin/<base>, as preflight does.** With no local `main`, or one behind origin/main, `gstack-review-dispatch pass` used to stop on "no merge base" or review from the stale branch. It now tries `origin/<base>` first, the way `render` and preflight already do (GSTA-198 replay).
- **A section return that never reached the parent can be fetched again.** `gstack-ship-parts next` now marks what it returns as sent, the next call promotes it, and `next --resend` (the pass program's `resendParts`) sends it again. Its `PARTS:` and `NEXT: parts` lines count against `--room`, the run directory in the `NEXT: parts` command is shell-quoted, a corrupt `state.json` stops with its path, and Step 11's sections are due by Phase 4, where Step 11 runs. The pass program keeps its last `NEXT:` line when its return is capped and prints `PARTS: skipped (…)` when it finds no parts state. Before, a lost or capped return dropped section text or the next step without a trace (PR #3670 review).
- **A scoped repeat pass leaves its fix-check to Step 3.5.** After main's GST-619, a scoped pass stages its readers on the fix range and names its fix-check outside the manifest; the pass program takes `fixCheck` files only on a `fix-check` step.
- **A scoped repeat pass leaves its fix-check to Step 3.5.** After main's GST-619, a scoped pass stages its readers on the fix range and names its fix-check outside the manifest; the pass program takes `fixCheck` files only on a `fix-check` step, and on a scoped step it ignores `--diff-base`, so the pending Step 11 voices keep the merge base and only the Red Team reads the fix range.
+1 -1
View File
@@ -87,7 +87,7 @@ Steps 8.1 and 8.2 (`Scope Check:`) on the todos and commits files, then the core
9.3, 9.4 and the repeat decision as written (`gstack-review-plan` included;
`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 (`diffBase`: RANGE_BASE; a scoped fix-check runs as Step 3.5); `fix-check`: the
`scoped`: one program runs the failing test, fix, `--tests-only` and Phase 3, each gated (`diffBase` RANGE_BASE on `full` only; scoped fix-check: Step 3.5); `fix-check`: the
pass (above) with `fixCheck: {fix_list, hunks, tests, callers}` files; `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)`).
+1 -1
View File
@@ -85,7 +85,7 @@ Steps 8.1 and 8.2 (`Scope Check:`) on the todos and commits files, then the core
9.3, 9.4 and the repeat decision as written (`gstack-review-plan` included;
`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 (`diffBase`: RANGE_BASE; a scoped fix-check runs as Step 3.5); `fix-check`: the
`scoped`: one program runs the failing test, fix, `--tests-only` and Phase 3, each gated (`diffBase` RANGE_BASE on `full` only; scoped fix-check: Step 3.5); `fix-check`: the
pass (above) with `fixCheck: {fix_list, hunks, tests, callers}` files; `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)`).
+20
View File
@@ -591,6 +591,26 @@ describe('Capy pass program (fake host, real bins)', () => {
expect(h.creates.map(c => c.title)).toContain('ship x22s1 red-team');
}, 240_000);
// Value: protects=a scoped pass run the old way, with the Capy driver's diffBase set to the fix range, still gives the pending Step 11 voices the merge base and only the Red Team the range (GST-619); fails_when=--diff-base on a scoped step overrides the plan's bases and narrows the voices to the fix range;
// why_new=#3670 merge fix-check (row 454): the driver's diffBase reached the voices on a scoped pass; seam=none; no_claim=a scoped pass after Step 11 ran, which reads the range throughout
test('a scoped pass given diffBase keeps the merge base for the Step 11 voices; only the Red Team reads the fix range', async () => {
const { dir, env } = repo();
const sh = (c: string) => spawnSync('bash', ['-c', c], { cwd: dir, env, encoding: 'utf8', timeout: 60_000 });
const mergeBase = sh('git merge-base main HEAD').stdout.trim();
const rangeBase = sh('git rev-parse HEAD').stdout.trim();
expect(sh('echo "export const fix = 2;" >> src/a.ts && git commit -qam fix').status).toBe(0);
const out = path.join(dir, '.state/run/s2');
const planFlags = ['--pass', '1', '--last', 'full', '--fixed-blocking', '--pass1-depth', 'standard', '--range-base', rangeBase];
const res = await runPass(host(dir, env), { base: 'main', out, pass: 'x22s2', diffBase: rangeBase, planFlags });
expect(res).toContain('next=scoped');
const m = JSON.parse(fs.readFileSync(path.join(out, 'pass-manifest.json'), 'utf8')) as { diff_base: string; readers: { role?: string; stage: string; diff_base?: string }[] };
expect(m.diff_base).toBe(mergeBase);
expect(m.readers.find(r => r.role === 'red-team')!.diff_base).toBe(rangeBase);
const voices = m.readers.filter(r => r.stage === 'voices');
expect(voices.length).toBeGreaterThan(0);
for (const v of voices) expect(v.diff_base ?? m.diff_base).toBe(mergeBase);
}, 240_000);
// Value: protects=the program's final NEXT line survives a return that its reader outputs push to the 32 KB cap, and a pass whose out is not <run dir>/<pass dir> says it skipped the parts; fails_when=finish() cuts the tail line, the room ignores the parts trailer, or parts are skipped without a line;
// why_new=#3670 Step 11 (outside voice 2, adversarial 2 and 6); seam=none; no_claim=what the parent does with a capped return
test('a near-cap return still ends with its NEXT line, and an odd out prints PARTS: skipped', async () => {
+1 -1
View File
@@ -194,7 +194,7 @@ describe('variant G: the call plan, observe budget, waits and audit readers (GST
test('returns stay at or under 32 KB, a fix is one gated program, Step 7 settles first, and unattended push defers the pre-push offer', () => {
const f = flat(doc);
for (const s of ['Returns: ≤32 KB per program; split longer reads at headings, each byte once, before first use',
'one program runs the failing test, fix, `--tests-only` and Phase 3, each gated', 'RANGE_BASE; a scoped fix-check runs as Step 3.5', 'Step 7 may write tests, so it settles first',
'one program runs the failing test, fix, `--tests-only` and Phase 3, each gated', '(`diffBase` RANGE_BASE on `full` only; scoped fix-check: Step 3.5)', 'Step 7 may write tests, so it settles first',
'Unattended, push defers the pre-push offer; else its STOP runs Step 17\'s guard block', 'plus a keep-alive per 200 s waited']) expect(f).toContain(s);
expect(flat(doc.slice(doc.indexOf('**Budget: observe.**')))).toContain('--reading R --credits C --record <rec>');
});