diff --git a/capy/skill/gstack/SKILL.md b/capy/skill/gstack/SKILL.md index adba16ee2..3a2348712 100644 --- a/capy/skill/gstack/SKILL.md +++ b/capy/skill/gstack/SKILL.md @@ -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 `. /ship reads `cost.tokens` and `cost.credits` from `tools.thread.get({ thread_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 --credits `, 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`). A review pass, Step 11 and the Step 14.5 audit each run inside one program, in order: (1) `gstack-review-plan --base ` with the pass's flags (its `DISPATCH:` lines); (2) `gstack-review-log --start ` once per row the step logs (under ship/SKILL.md's admission mode, add `--reservation ` from check's `RESERVATION_ID:` line); (3) `BEFORE`, the counter's tokens and credits; (4) `tools.subagent.create`, one reader each, `machine: "shared"`, its brief copied verbatim from the section that dispatches it; audit readers (Steps 7, 8, 9.2.1, 14.5) cannot block, so they add `model: { model_id: "anthropic/claude-sonnet-5-5", reasoning_mode: "low" }` (`anthropic/claude-haiku-5-5` also fits); (5) a poll of `tools.subagent.get` with `transcript: "none"` every ~20 s for at most 240 s; (6) `gstack-ship-budget note --step --reading --credits --dispatched-at --dispatched-credits --readers `; (7) return only each reader's final output, or what still runs. The parent never walks the dispatch steps call by call. Waits: never let 5 minutes pass between calls. During tests, pregate, readers and CI, re-issue `wait` with `duration_seconds: 240` and the operation ids: a call after a longer gap bills the whole context fresh (about 40x a cached call), and a ping costs a few percent of that. A small waiting child saves nothing: the parent's cache expires during its wait too. +/ship on Capy is about a dozen calls, each one execute program (`ship/DRIVER.md`). A review pass, Step 11 and the Step 14.5 audit each run inside one program, in order: (1) `gstack-review-plan --base ` with the pass's flags (its `DISPATCH:` lines); (2) `gstack-review-log --start ` once per row the step logs (under ship/SKILL.md's admission mode, add `--reservation ` from check's `RESERVATION_ID:` line); (3) `BEFORE`, the counter's tokens and credits; (4) `tools.subagent.create`, one reader each, `machine: "shared"`, its brief copied verbatim from the section that dispatches it; audit readers (Steps 7, 8, 9.2.1, 14.5) cannot block, so they add `model: { model_id: "anthropic/claude-sonnet-5-5", reasoning_mode: "low" }` (`anthropic/claude-haiku-5-5` also fits); (5) a poll of `tools.subagent.get` with `transcript: "none"` every ~20 s for at most 240 s, where a reader is settled only at status `done` or `failed` or with a `summary` (a just-created `idle` reader has not run); (6) `gstack-ship-budget note --step --reading --credits --dispatched-at --dispatched-credits --readers `; (7) return only each reader's final output, or what still runs. The parent never walks the dispatch steps call by call. Waits: never let 5 minutes pass between calls. During tests, pregate, readers and CI, re-issue `wait` with `duration_seconds: 240` and the operation ids: a call after a longer gap bills the whole context fresh (about 40x a cached call), and a ping costs a few percent of that. A small waiting child saves nothing: the parent's cache expires during its wait too. ## Plan, approve, ship diff --git a/lib/ship-handoff.ts b/lib/ship-handoff.ts index 9d07a707a..5ecc277f4 100644 --- a/lib/ship-handoff.ts +++ b/lib/ship-handoff.ts @@ -26,7 +26,7 @@ import { wtreeFingerprint } from './script-invocation'; import { resultLine } from './result-codes'; import { MAX_CHAIN_DEPTH, SHIP_STEPS, budgetError, budgetLine, ledgerFileFor, loadCalibration, mintRunId, mutateLedger, parseStep, pointMarker, - projector, readLedger, reserveIn, runDirFor, segmentEstimate, tally, usageError, writeLedger, type Handoff, type Ledger, + projector, readLedger, reserveIn, runDirFor, segmentEstimate, settleIn, tally, usageError, writeLedger, type Handoff, type Ledger, } from './ship-budget'; import { loadMeterState, saveMeterState } from './claude-usage'; import { parseArgs, readMeter, stopLines, str, tokensOpt, writeReturnFile, type Ctx, type Outcome } from './ship-budget-cli'; @@ -218,6 +218,7 @@ export function cmdReturn(argv: string[], ctx: Ctx): Outcome { return mutateLedger(ledgerFileFor({ run: str(o, 'run') }, ctx.env), ctx.env, l => { if (l.role !== 'child' || !l.parent) throw usageError(`run ${l.run} is not a child run`); readMeter(l, o, false); + for (const r of l.reservations) if (r.kind === 'step' && r.state === 'open') settleIn(l, r); const head = git(ctx.cwd, ['rev-parse', 'HEAD']); const tree = pinsNow(ctx.cwd, 'HEAD', ctx.env).wtree; const accepted = new Set((l as any).accepted_skips as string[] | undefined ?? []); diff --git a/ship/DRIVER.md b/ship/DRIVER.md index 293cc8b6f..77395ec84 100644 --- a/ship/DRIVER.md +++ b/ship/DRIVER.md @@ -46,7 +46,8 @@ waiting child saves nothing: the parent's cache expires during its wait too. Pas parent makes no other call while it runs: (1) `gstack-review-plan --base ` with the pass's flags (its `DISPATCH:` lines; Step 14.5 has one reader, `docs`); (2) `gstack-review-log --start ` once per row the step logs (Step 14.5 takes none); (3) `BEFORE`; (4) one subagent per reader; -(5) a poll until every reader settles; (6) `note` with the reading pair; a later batch (the Red Team, +(5) a poll until every reader settles (status `done` or `failed`, or a final summary; a fresh `idle` +has not run); (6) `note` with the reading pair; a later batch (the Red Team, given the specialists' collected lines) repeats (3)-(6); (7) the collect: return only each reader's final output and status. The next call applies the section's merge, judgment, fixes and persistence; it never walks the dispatch steps call by call. diff --git a/ship/DRIVER.md.tmpl b/ship/DRIVER.md.tmpl index 334897a86..5f25501b0 100644 --- a/ship/DRIVER.md.tmpl +++ b/ship/DRIVER.md.tmpl @@ -44,7 +44,8 @@ waiting child saves nothing: the parent's cache expires during its wait too. Pas parent makes no other call while it runs: (1) `gstack-review-plan --base ` with the pass's flags (its `DISPATCH:` lines; Step 14.5 has one reader, `docs`); (2) `gstack-review-log --start ` once per row the step logs (Step 14.5 takes none); (3) `BEFORE`; (4) one subagent per reader; -(5) a poll until every reader settles; (6) `note` with the reading pair; a later batch (the Red Team, +(5) a poll until every reader settles (status `done` or `failed`, or a final summary; a fresh `idle` +has not run); (6) `note` with the reading pair; a later batch (the Red Team, given the specialists' collected lines) repeats (3)-(6); (7) the collect: return only each reader's final output and status. The next call applies the section's merge, judgment, fixes and persistence; it never walks the dispatch steps call by call. diff --git a/ship/sections/budget-handoff.md b/ship/sections/budget-handoff.md index 32fa68996..c561b04ab 100644 --- a/ship/sections/budget-handoff.md +++ b/ship/sections/budget-handoff.md @@ -36,7 +36,8 @@ after they settle; `close` flags roles never re-checked. `--finish` settles the the same order): `gstack-review-plan` (its `DISPATCH:` lines), `check --step --next dispatch:`, `gstack-review-log --start --reservation ` once per row the step logs, the reading `BEFORE`, the readers exactly as the step dispatches them (their -prompts unchanged), the wait until the last settles, the reading `AFTER`, `gstack-ship-budget settle`; +prompts unchanged), the wait until the last settles (`done`, `failed` or a final summary; a fresh `idle` +has not run), the reading `AFTER`, `gstack-ship-budget settle`; a later batch of the same pass (the Red Team or an ADMIT remainder) takes no start and repeats the rest (`--finish` settles it); last the collect, returning only the readers' final outputs. Step 11's voices and Step 14.5's audit (`dispatch:docs`, no start) are one program each. diff --git a/ship/sections/budget-handoff.md.tmpl b/ship/sections/budget-handoff.md.tmpl index 448139325..1a6df789b 100644 --- a/ship/sections/budget-handoff.md.tmpl +++ b/ship/sections/budget-handoff.md.tmpl @@ -34,7 +34,8 @@ after they settle; `close` flags roles never re-checked. `--finish` settles the the same order): `gstack-review-plan` (its `DISPATCH:` lines), `check --step --next dispatch:`, `gstack-review-log --start --reservation ` once per row the step logs, the reading `BEFORE`, the readers exactly as the step dispatches them (their -prompts unchanged), the wait until the last settles, the reading `AFTER`, `gstack-ship-budget settle`; +prompts unchanged), the wait until the last settles (`done`, `failed` or a final summary; a fresh `idle` +has not run), the reading `AFTER`, `gstack-ship-budget settle`; a later batch of the same pass (the Red Team or an ADMIT remainder) takes no start and repeats the rest (`--finish` settles it); last the collect, returning only the readers' final outputs. Step 11's voices and Step 14.5's audit (`dispatch:docs`, no start) are one program each. diff --git a/test/ship-budget-meter-contract.test.ts b/test/ship-budget-meter-contract.test.ts index 548e76043..f3b0cf702 100644 --- a/test/ship-budget-meter-contract.test.ts +++ b/test/ship-budget-meter-contract.test.ts @@ -83,6 +83,25 @@ describe('the Capy meter is the agent\'s own jam counter', () => { expect(budget).toContain('On Capy pass no --reading or --credits: the parent charges child_used from its own counter around your work'); }); + // Value: protects=a child's return settles its open step reservation, so no step reservation stays open in the child ledger after it hands back; fails_when=return leaves the last step's reservation open (replay 7e #36: return left the Step 17 reservation r8, 742,152 tokens, open); + // why_new=replay 7e; seam=none; no_claim=the settled amount's accuracy + test('return settles the child\'s open Step 17 reservation', () => { + const h = fresh(); + const s = h.sb(['start', '--record', h.record(), '--token-cap', '400M', '--reading', '1000000']); + const ho = h.sb(['handoff', '--run', h.field(s.out, 'BUDGET_RUN', 'id'), '--from', '12', '--to', '22', '--base', 'origin/main', '--reading', '1500000']); + const v = h.sb(['verify-handoff', '--in', ho.out.match(/^HANDOFF: (\S+) /m)![1]!, '--reading', '200000']); + expect(v.code, v.out + v.err).toBe(0); + const child = h.field(v.out, 'BUDGET_RUN', 'id'); + const c = h.sb(['check', '--run', child, '--step', '16', '--next', '17', '--reading', '900000']); + expect(c.code, c.out + c.err).toBe(0); + const id = h.field(c.out, 'RESERVATION', 'id'); + const ret = h.sb(['return', '--run', child, '--to', '22', '--reading', '1600000']); + expect(ret.code, ret.out + ret.err).toBe(0); + const ledger = h.ledger(h.field(v.out, 'BUDGET_RUN', 'ledger')); + expect(ledger.reservations.find((r: any) => r.id === id).state).toBe('settled'); + expect(ledger.reservations.filter((r: any) => r.state === 'open')).toEqual([]); + }); + // Value: protects=a Capy child with no jam id of its own verifies, checks, returns and is reconciled from the parent's counter delta; fails_when=verify-handoff or a child check refuses with BUDGET_READING_MISSING, an unknown charge delegates the child onward, or reconcile charges the child's own BUDGET used=0 instead of the parent's delta; // why_new=replay 7e #36 (handoffs/GSTA-167/replay7e-36.md); seam=none; no_claim=that the parent's counter excludes its own pings test('a child handoff passes verify-handoff with no jam id of its own and the parent measures it', () => { diff --git a/test/ship-driver.test.ts b/test/ship-driver.test.ts index 68abc24df..a94632396 100644 --- a/test/ship-driver.test.ts +++ b/test/ship-driver.test.ts @@ -187,6 +187,14 @@ describe('variant G: the call plan, observe budget, waits and audit readers (GST for (const text of [doc, capy]) expect(text).not.toMatch(/\b270 ?s\b/); }); + // Value: protects=a reader counts as settled only once it has run (status done or failed, or a final summary), in the driver, the Capy skill and the budget section, so a poll never closes a batch on a subagent that was just created; fails_when=the settled rule is dropped or lets a bare idle status through (replay 7e #36: the Red Team reservation settled actual=0 because a just-created subagent read idle); + // why_new=replay 7e; seam=none; no_claim=the host's status semantics beyond done, failed, idle and summary + test('a poll settles a reader only at done, failed or a summary, never on a fresh idle', () => { + expect(flat(doc)).toContain('(5) a poll until every reader settles (status `done` or `failed`, or a final summary; a fresh `idle` has not run)'); + expect(read('capy/skill/gstack/SKILL.md')).toContain('a reader is settled only at status `done` or `failed` or with a `summary` (a just-created `idle` reader has not run)'); + expect(flat(read('ship/sections/budget-handoff.md.tmpl'))).toContain('the wait until the last settles (`done`, `failed` or a final summary; a fresh `idle` has not run)'); + }); + // Value: protects=audits that cannot block (Steps 7, 8, 9.2.1, 14.5) run on a small model at low effort with their sections' own briefs, and skip their reader when --audit decides them; fails_when=an audit reader inherits the lane's model and effort (#36: the coverage audit alone was 2.45M at Opus), or an audit brief is paraphrased; // why_new=audit model routing is new; seam=none; no_claim=the audits' recall test('audit readers run on a small model at low effort, named for Capy, and Phase 3 dispatches only what READER names', () => { diff --git a/test/size-ledger/ship/gsta-167-turns.json b/test/size-ledger/ship/gsta-167-turns.json index d09349075..dfed07655 100644 --- a/test/size-ledger/ship/gsta-167-turns.json +++ b/test/size-ledger/ship/gsta-167-turns.json @@ -1,4 +1,4 @@ { - "bytes": 1548, - "why": "GSTA-167: ship preflight/release bins and the compact driver (+841); variant E: budget-handoff.md runs one program per review pass and hands Steps 9-11.5 to the review child (+362); variant G: budget-handoff.md points driver runs at observe mode, reads credits beside tokens and verifies a Capy child with no reading (+345)" + "bytes": 1614, + "why": "GSTA-167: ship preflight/release bins and the compact driver (+841); variant E: budget-handoff.md runs one program per review pass and hands Steps 9-11.5 to the review child (+362); variant G: budget-handoff.md points driver runs at observe mode, reads credits beside tokens, verifies a Capy child with no reading and settles a reader only once it has run (+411)" }