diff --git a/capy/skill/gstack/SKILL.md b/capy/skill/gstack/SKILL.md index a505ec214..2cede15c5 100644 --- a/capy/skill/gstack/SKILL.md +++ b/capy/skill/gstack/SKILL.md @@ -42,9 +42,9 @@ Run directories and scratch on Capy live under `~/.capy/work/` (`--out ~/. Capy checklist for `/autoplan`: drive `gstack-autoplan`, never a hand adaptation presented as a run. Relay `gstack-gate render /gate.json --all` with `rec` (every recommendation, plan changes included) as the one-word default; answer with `--by agent: --owner-reply ""`. Set `GSTACK_AUTOPLAN_CHECKPOINT_DIR` to a private project-user Drive folder; after machine replacement, `gstack-autoplan resume --out --from ` restores the newest valid checkpoint. -Token caps on /ship: pass a brief's cap as `--token-cap `. /ship reads `cost.tokens` from `tools.thread.get({ thread_id: "this" })` (one execute program returning only that number) into `gstack-ship-budget --reading ` and obeys `verdict=` (`docs/ship-budget.md`). +Token caps on /ship: pass a brief's cap as `--token-cap `. /ship reads `cost.tokens` from `tools.thread.get({ thread_id: "this" })` inside the same execute program as the `gstack-ship-budget` command that takes it as `--reading `, never in a call of its own, and obeys `verdict=` (`docs/ship-budget.md`). -/ship reviewers on Capy (`docs/review-policy.md#reviewer-packets`): one execute program runs `gstack-review-packet build` with the plan run's flags, creates one subagent per `PACKET:` line (`tools.subagent.create` takes at most 10 per call) with `machine: "shared"` on the lane's model at reasoning `medium` (red-team and adversarial keep the lane's reasoning; a cross-family outside voice stays as it is), and starts at most the free slots of the running-subagent cap, retrying refused starts as slots free. Each brief is the packet brief with that packet's `read=` ranges, plus one more parallel call in turn 1: `install.sh --check --for review-voice` in bash. A reviewer then finishes in about three calls: the parallel reads, at most one turn of extra ranges, one write. Under a token cap, `gstack-ship-budget check --next` takes build's `BUDGET_NEXT:` value (`role@bytes` per packet, so each reader is priced from its packet), and the same program measures the batch: it reads the counter right before it creates the readers (`BEFORE`; a batch check that runs in this program takes this same reading) and again right after the last reader settles (`AFTER`), then runs `gstack-ship-budget settle --run --reservation --dispatched-at --reading --readers ` before collect. The parent makes no model call while the program runs, so only that pair measures the readers; a batch settled from a later program (a rerun poll) omits `--dispatched-at` and stays `UNMEASURED`. The same program polls `tools.subagent.get` with `transcript: "none"` every ~20 s until every reviewer settles, then runs `gstack-review-packet collect --dir ` and returns only its output; keep one poll program under ~8 minutes and run it again until collect has run (a lost program loses no reviewer), and never read a reviewer's transcript. +/ship reviewers on Capy (`docs/review-policy.md#reviewer-packets`): one execute program per review pass, in this order: (1) `gstack-review-packet build` with the pass's plan flags (a later build in the same pass, the Red Team or Step 11's packets, is `build --like `, never hand-copied flags); (2) under a token cap, `gstack-ship-budget check --next "$BUDGET_NEXT"` (build's `BUDGET_NEXT:` value, `role@bytes` per packet, so each reader is priced from its packet), then `gstack-review-log --start --reservation ` once per role on that reservation; (3) the counter reading `BEFORE` (a check in this program takes this same reading); (4) `tools.subagent.create`, one subagent per `PACKET:` line (at most 10 per call), `machine: "shared"`, on the lane's model at reasoning `medium` (red-team and adversarial keep the lane's reasoning; a cross-family outside voice stays as it is), starting at most the free slots of the running-subagent cap and retrying refused starts as slots free; (5) a poll of `tools.subagent.get` with `transcript: "none"` every ~20 s until every reviewer settles; (6) the reading `AFTER`, then `gstack-ship-budget settle --run --reservation --dispatched-at --reading --readers `; (7) `gstack-review-packet collect --dir `, returning only its output. Each brief is the packet brief with that packet's `read=` ranges, plus one more parallel call in turn 1: `install.sh --check --for review-voice` in bash; a reviewer then finishes in about three calls (the parallel reads, at most one turn of extra ranges, one write). The parent makes no model call while the program runs, so only that pair measures the readers; a batch settled from a later program (a rerun poll) omits `--dispatched-at` and stays `UNMEASURED`. Keep one program under ~8 minutes and rerun the poll until collect has run (a lost program loses no reviewer); never read a reviewer's transcript. `gstack-review-log --finish` stays at the step that persists the pass: it records the parent's verdict. ## Plan, approve, ship diff --git a/ship/sections/budget-handoff.md b/ship/sections/budget-handoff.md index b9f228785..457908a28 100644 --- a/ship/sections/budget-handoff.md +++ b/ship/sections/budget-handoff.md @@ -29,6 +29,12 @@ role per packet, never packet names): On `ADMIT: k of n (remaining=)`, dispatch only those k and re-check the remaining roles after they settle; `close` flags roles never re-checked. `--finish` settles the reservation and the pass's other dispatch reservations. + +**One program per review pass.** One execute program (one shell script off Capy) runs, in order: +`gstack-review-packet build` (a later build in the pass is `build --like `), `check --next "$BUDGET_NEXT"`, +`gstack-review-log --start` once per role on the printed reservation, the reading `BEFORE`, the readers +(`tools.subagent.create` on Capy), the poll until the last settles (`tools.subagent.get`), the reading `AFTER`, +`gstack-ship-budget settle`, then `gstack-review-packet collect`, returning only collect's output. Settle each batch in the program that dispatched it: read the counter right before the readers start and right after the last one settles, then `gstack-ship-budget settle --run --reservation --dispatched-at --reading --readers ` (`--unknown` when the meter cannot see it); diff --git a/ship/sections/budget-handoff.md.tmpl b/ship/sections/budget-handoff.md.tmpl index a46dd8e6e..2dfe09e00 100644 --- a/ship/sections/budget-handoff.md.tmpl +++ b/ship/sections/budget-handoff.md.tmpl @@ -27,6 +27,12 @@ role per packet, never packet names): On `ADMIT: k of n (remaining=)`, dispatch only those k and re-check the remaining roles after they settle; `close` flags roles never re-checked. `--finish` settles the reservation and the pass's other dispatch reservations. + +**One program per review pass.** One execute program (one shell script off Capy) runs, in order: +`gstack-review-packet build` (a later build in the pass is `build --like `), `check --next "$BUDGET_NEXT"`, +`gstack-review-log --start` once per role on the printed reservation, the reading `BEFORE`, the readers +(`tools.subagent.create` on Capy), the poll until the last settles (`tools.subagent.get`), the reading `AFTER`, +`gstack-ship-budget settle`, then `gstack-review-packet collect`, returning only collect's output. Settle each batch in the program that dispatched it: read the counter right before the readers start and right after the last one settles, then `gstack-ship-budget settle --run --reservation --dispatched-at --reading --readers ` (`--unknown` when the meter cannot see it); diff --git a/test/ship-budget-reader-drift.test.ts b/test/ship-budget-reader-drift.test.ts index da9b28df2..7660370d5 100644 --- a/test/ship-budget-reader-drift.test.ts +++ b/test/ship-budget-reader-drift.test.ts @@ -236,6 +236,31 @@ describe('the budget section states both contracts', () => { expect(tmpl).toMatch(/--prior-reading[\s\S]{0,200}previous call/); expect(tmpl).toMatch(/older than the run's last reading is ignored/); }); + // Value: protects=no gstack prose tells the agent to read the meter in a call of its own (each such call costs a full context, about 0.2M on #34/#36); fails_when=capy SKILL.md keeps "one execute program returning only that number" or the section asks for a meter-only call; + // why_new=the template test above never read the Capy skill; seam=none; no_claim=how often an agent obeys the prose + test('the Capy skill reads the meter inside the budget command\'s program and names no meter-only call', () => { + const capy = fs.readFileSync(path.join(import.meta.dir, '..', 'capy', 'skill', 'gstack', 'SKILL.md'), 'utf8'); + const caps = capy.slice(capy.indexOf('Token caps on /ship'), capy.indexOf('/ship reviewers on Capy')); + expect(caps.length).toBeGreaterThan(0); + expect(caps).not.toContain('returning only that number'); + expect(caps).toMatch(/inside the same (?:execute )?program as the\s+`gstack-ship-budget` command/); + for (const text of [capy, tmpl]) expect(text).not.toMatch(/read it again in your next call|meter-only call(?!s? (?:is|are) never)/); + }); + // Value: protects=each review pass is one execute program in a fixed order, so the readers' reading pair brackets only them and no build, start or poll costs its own parent call; fails_when=the order breaks (a start after the readers are created, a settle after collect) or later builds hand-copy flags instead of --like; + // why_new=no test reads the pass order; seam=none; no_claim=the model's adherence + test('a review pass is one execute program: build, check --next, review-log --start, BEFORE, create, poll, AFTER and settle, collect', () => { + const capy = fs.readFileSync(path.join(import.meta.dir, '..', 'capy', 'skill', 'gstack', 'SKILL.md'), 'utf8'); + const pass = capy.slice(capy.indexOf('/ship reviewers on Capy'), capy.indexOf('## Plan, approve, ship')); + expect(pass).toMatch(/one execute program per review pass/); + const order = ['gstack-review-packet build', 'check --next', 'gstack-review-log --start', 'BEFORE', 'tools.subagent.create', 'tools.subagent.get', 'AFTER', 'gstack-ship-budget settle', 'gstack-review-packet collect']; + for (const text of [pass, tmpl.slice(tmpl.indexOf('**One program per review pass.**'))]) { + const at = order.map(o => text.indexOf(o)); + expect(at.every(i => i >= 0), order.filter((_, k) => at[k]! < 0).join(',')).toBe(true); + expect([...at].sort((a, b) => a - b)).toEqual(at); + } + expect(pass).toContain('--like'); + expect(pass).not.toContain("with the plan run's flags"); + }); test('a batch names one role per reader and is settled with the dispatch program\'s own reading pair', () => { expect(tmpl).toMatch(/one role per (?:reader|`DISPATCH:` line)/); expect(tmpl).toMatch(/Settle each batch in the program that dispatched it[\s\S]{0,300}--dispatched-at --reading --readers /); diff --git a/test/ship-budget-review-log.test.ts b/test/ship-budget-review-log.test.ts index b8e26506e..5abb3bd7f 100644 --- a/test/ship-budget-review-log.test.ts +++ b/test/ship-budget-review-log.test.ts @@ -113,6 +113,7 @@ describe('gstack-review-log --start under a token cap', () => { expect(h.ledger(run.ledger).reservations.find((r: any) => r.id === id).state).toBe('settled'); const close = h.sb(['close', '--run', run.run, '--reading', '10']); expect(close.out).not.toContain('BUDGET_CHECK_SKIPPED'); + expect(close.out).not.toContain('reason=budget_check_skipped'); expect(close.code, close.out).toBe(0); });