feat(ship): variant E orchestration: one program per review pass and Step 14.5 audit, the review child starts at Step 9, a metered dispatch:docs role, and the driver's record headings (GSTA-167)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-30 15:28:39 +00:00
1 parent 8a1e5bb675
commit 80e5625cfc
18 files changed
+267 -123

No files matched your search

+2 -1
View File
@@ -46,7 +46,8 @@ 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` 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 it as `--reading <n>`, never in a call of its own, and obeys `verdict=` (`docs/ship-budget.md`).
/ship reviewer batches on Capy under a cap: one execute program per batch, in this order: (1) `gstack-ship-budget check --run <id> --step <n> --next dispatch:<roles> --reading <BEFORE>` (one role per `DISPATCH:` line; `BEFORE` is this program's counter reading), then `gstack-review-log --start <skill> --reservation <id>` (`<id>` from check's `RESERVATION_ID:` line) once per row the step logs (a later batch of the same pass takes none); (2) `tools.subagent.create`, one reviewer each, its brief copied verbatim from the section that dispatches it; (3) a poll of `tools.subagent.get` with `transcript: "none"` every ~20 s until every reviewer settles; (4) the reading `AFTER`, then `gstack-ship-budget settle --run <id> --reservation <id> --dispatched-at <BEFORE> --reading <AFTER> --readers <n>`, returning each reviewer's final report. The parent makes no model call while the program runs, so only that pair measures the readers; a batch settled from a later program omits `--dispatched-at` and stays `UNMEASURED`. Keep one program under ~8 minutes and rerun the poll until every reviewer has settled.
/ship reviewers on Capy: one execute program per review pass, per Step 11 and for the Step 14.5 audit (`ship/DRIVER.md`, "One program per pass"), in this order: (1) `gstack-review-plan --base <base>` with the pass's flags (its `DISPATCH:` lines; Step 14.5's one reader is `docs`); (2) `gstack-ship-budget check --run <id> --step <n> --next dispatch:<roles> --reading <BEFORE>` (one role per reader; `BEFORE` is this program's counter reading), then `gstack-review-log --start <skill> --reservation <id>` (`<id>` from check's `RESERVATION_ID:` line) once per row the step logs; (3) `tools.subagent.create`, one reader each, `machine: "shared"`, its brief copied verbatim from the section that dispatches it; (4) a poll of `tools.subagent.get` with `transcript: "none"` every ~20 s until every reader settles; (5) the reading `AFTER`, then `gstack-ship-budget settle --run <id> --reservation <id> --dispatched-at <BEFORE> --reading <AFTER> --readers <n>`; a later batch of the pass (the Red Team, given the specialists' collected lines) repeats (2)-(5) with no start; (6) return only each reader's final output. The parent makes no model call while the program runs, so only that pair measures the readers, and it never walks the dispatch steps call by call. Give the program `timeout_seconds` for the slowest reader; a poll rerun from a later program settles without `--dispatched-at` and stays `UNMEASURED`.
## Plan, approve, ship
+5 -4
View File
@@ -95,7 +95,8 @@ prints `ADMIT: k of n (remaining=<roles>)`: dispatch those k, then re-check the
once they settle. Only a batch whose first reader does not fit stops, and `close` flags remaining
roles no later check requested (`DROPPED:`, status=incomplete until the owner accepts).
Roles are review-depth's: `specialist`, `red-team`, `adversarial-native`, `outside-voice`,
`fix-check`. `--finish <token>` is the pass's completion receipt: it settles that reservation and
`fix-check`, plus `docs`, the Step 14.5 documentation audit, which takes no review-log start and
settles in its program like the others. `--finish <token>` is the pass's completion receipt: it settles that reservation and
every other dispatch reservation started in the same step, so reviewers admitted one at a time
do not stay reserved. The program that dispatches a batch measures it: it reads the counter right
before it starts the readers and again right after the last one settles, then runs `settle
@@ -117,8 +118,8 @@ fresh session with the ship skill loaded, before any /ship work. The floor is `c
unit in `lib/ship-budget-calibration.json` (150k on Capy, where a fresh thread's calls cost
150–230k; 40k on Claude Code; 0 for the estimate meter, whose charge is always unknown), or
`start --call-floor <n>`, which the ledger records and every child inherits. At start, 100k or
more above the floor delegates the whole /ship. Before Step 7 and before Step 12, 150k or more
above the floor (or an unknown charge) delegates the next segment (7–11.5, 12–22). The BUDGET
more above the floor delegates the whole /ship. Before Step 9 and before Step 12, 150k or more
above the floor (or an unknown charge) delegates the next segment (9–11.5, 12–22). The BUDGET
line prints `call`, `call_floor` and `above_floor`, so each verdict can be checked:
1. `gstack-ship-budget handoff --run <id> --from 12 --to 22 --base origin/<base> --reading <n>` writes a
@@ -170,7 +171,7 @@ unknown charge continues there. A parent driving /ship from the driver carries n
child would shed, so a child pays its fixed cost only when the parent's own context grew.
**Small runs keep their segment.** `check --runtime-lines <n>` (runtime_lines from
`gstack-review-plan --json`, which `gstack-ship-preflight` prints) continues at the Step 7 and
`gstack-review-plan --json`, which `gstack-ship-preflight` prints) continues at the Step 9 and
Step 12 boundaries, with a `SMALL_RUN:` line, when the diff is light (at most 50 runtime lines)
and the segment's calibrated calls at this run's per-call charge cost at most 12M. A child costs
a fixed 2–4.5M (gstack-evals #34: boot, install check, verify-handoff, section reads,
+1 -1
View File
@@ -145,7 +145,7 @@ export function decide(l: Ledger, step: string, next: Next, call: number | null,
}
const threshold = startThreshold ? DELEGATE_START_CALL : DELEGATE_SEGMENT_CALL;
const above = aboveFloor(call, p.floor);
const boundary = (startThreshold ? next.step === '1' : next.step === '7' || next.step === '12');
const boundary = (startThreshold ? next.step === '1' : next.step in SEGMENTS && next.step !== '1');
const to = l.parent ? l.parent.to : SEGMENTS[next.step]!;
const small = boundary && !startThreshold ? keepsSmallRun(loadCalibration(l.calibration), { runtimeLines, from: next.step, to, call }) : null;
const wants = boundary && !small && (above === null ? !startThreshold && l.delegate !== 'measured' : above >= threshold);
+4 -3
View File
@@ -28,7 +28,8 @@ import { BIN_DIR, runScriptSync } from './script-invocation';
import { SMALL_DIFF_LINES, type Role } from './review-depth';
export const SHIP_STEPS = ['0.9', '1', '2', '3', '4', '5', '6', '6.5', '7', '8', '9', '10', '11', '11.5', '12', '13', '14', '14.5', '15', '16', '17', '18', '19', '20', '21', '22'] as const;
export const DISPATCH_ROLES = ['specialist', 'red-team', 'adversarial-native', 'outside-voice', 'fix-check'] as const satisfies readonly Role[];
/** Reviewer roles plus `docs`, the Step 14.5 documentation audit: each is one reader priced and reconciled the same way. */
export const DISPATCH_ROLES = ['specialist', 'red-team', 'adversarial-native', 'outside-voice', 'fix-check', 'docs'] as const satisfies readonly (Role | 'docs')[];
type MissingRole = Exclude<Role, typeof DISPATCH_ROLES[number]>;
const ROLES_COMPLETE: [MissingRole] extends [never] ? true : never = true;
void ROLES_COMPLETE;
@@ -40,8 +41,8 @@ export const DELEGATE_START_CALL = 100_000;
export const DELEGATE_SEGMENT_CALL = 150_000;
/** Handoffs between the root run and the deepest child; a run at this depth continues instead of handing off. */
export const MAX_CHAIN_DEPTH = 3;
/** Segment boundaries (before Step 7, before Step 12) and the last step each segment runs. */
export const SEGMENTS: Record<string, string> = { '1': '22', '7': '11.5', '12': '22' };
/** Segment boundaries (before Step 9, before Step 12) and the last step each segment runs: Steps 7-8 stay with the parent, so the review child reads only what dispatch needs. */
export const SEGMENTS: Record<string, string> = { '1': '22', '9': '11.5', '12': '22' };
export const DEFAULT_TAIL = 2_000_000;
const DIFF_REVIEW_STEP: Record<string, string> = { review: '9', 'review-fix-check': '9', 'design-review-lite': '9', 'adversarial-review': '11', 'codex-review': '11' };
+1 -1
View File
@@ -99,7 +99,7 @@ function merge(cwd: string, base: string, r: Report): boolean {
&& !(outputs.length && matchesAny(outputs, e.path)) && classifyPath(e.path, cwd) === 'code' && !DATA_PATH.test(e.path));
runtime = String(counted.reduce((n, e) => n + e.lines, 0));
} catch { /* unknown */ }
r.add(`RUNTIME_LINES: ${runtime} (gstack-ship-budget check --runtime-lines at the Step 7 and 12 boundaries)`);
r.add(`RUNTIME_LINES: ${runtime} (gstack-ship-budget check --runtime-lines at the Step 9 and 12 boundaries)`);
return true;
}
+33 -14
View File
@@ -13,8 +13,13 @@ says otherwise; print its result lines verbatim. `STOP:` (exit 3) means nothing
names the next act. Read a section only when a phase or the list at the end sends you, never
up front. A STOP or decision this file does not cover: read `ship/SKILL.md` at that step, finish it
there, come back. Unattended, a question with a recommended option takes it (log it); a gate
disposition and every approval stay pending and end the run. Keep a private invocation record
(Release, Decisions, Reviews, Checks, Documentation, Next steps).
disposition and every approval stay pending and end the run. Keep a private invocation record (the
`--record` file): write its six headings at Phase 1, then fill each as its phase ends: **Checks**
(Phases 1-3: lanes, pregate, evals, `Coverage:`, the plan checks, `Scope Check:`), **Decisions** (any
phase: each answered or auto-taken question, in its words), **Reviews** (Phases 4-6: the plan line,
start tokens, `Pre-Landing Review:`, `Gate edits:`, the reviewed tree), **Release** (Phase 7: version,
CHANGELOG, commits), **Documentation** (Step 14.5) and **Next steps** (every phase: the next act).
`gstack-ship-budget handoff` refuses a record that lacks any of them.
**Reviewers are the sections' own.** Phases 3, 4 and 5 and Step 14.5 run their section in full, as
written: every audit, specialist, Red Team, fix-check, adversarial, outside-voice and documentation
@@ -23,11 +28,20 @@ restates none of them and never shortens a reviewer's brief.
Budget: read your own token counter (subagents included) in the program that runs
`gstack-ship-budget` (`--reading R --prior-reading <last R>`); `check --step <done> --next <step>`
opens each phase, `check --step <n> --next dispatch:<roles>` (one role per `DISPATCH:` line) each
reviewer batch, its `RESERVATION_ID:` going to that pass's `--start ... --reservation` (a later batch
of the pass takes no start). Settle each batch in the program that dispatched it: the reading
`BEFORE` right before the readers start, the wait until the last settles, the reading `AFTER`, then
`settle --reservation <id> --dispatched-at <BEFORE> --reading <AFTER> --readers <n>`. Obey `verdict=`.
opens each phase. Obey `verdict=`.
**One program per pass.** Each review pass (Phase 4), Step 11 (Phase 5) and the Step 14.5 audit is
one program, and the parent makes no other call while it runs: (1) the plan, `gstack-review-plan
--base <base>` with the pass's flags (its `DISPATCH:` lines; Step 14.5 has one reader, `docs`);
(2) `check --step <n> --next dispatch:<roles>`, one role per reader, then `gstack-review-log --start
<skill> --reservation <id>` once per row the step logs (Step 14.5 takes none); (3) the reading
`BEFORE`; (4) one subagent per reader, its brief copied verbatim from the section with only its
placeholders filled; (5) a poll until every reader settles; (6) the reading `AFTER`, then `settle
--reservation <id> --dispatched-at <BEFORE> --reading <AFTER> --readers <n>`; a later batch of the pass
(the Red Team, given the specialists' collected lines; an `ADMIT:` remainder) repeats (2)-(6) with
no start; (7) the collect: return only each reader's final output and status. The parent applies
the section's merge, judgment, fixes and persistence to that return in one or two calls; it never
walks the dispatch steps call by call.
## 1. Preflight and tests: Steps 0, 0.9, 1, 2, 3, 4, 5
@@ -50,18 +64,23 @@ only if `gstack-greptile-early detect` finds Greptile.
## 3. Audits: Steps 7, 8, 8.1, 8.2
Read `sections/test-coverage.md` and `sections/plan-completion.md` and run Steps 7, 8, 8.1 and 8.2
as they are written. Prints `Coverage:`, `Plan completion:` and Step 8.2's `Scope Check:`.
as they are written, here (never delegated). Prints `Coverage:`, `Plan completion:` and Step 8.2's
`Scope Check:`; write them under Checks. `check --step 8 --next 9` closes the phase: on `DELEGATE:`,
Steps 9-11.5 go to a fresh child (`sections/budget-handoff.md`) that reads only
`sections/review-army.md`, `sections/adversarial.md` and the record.
## 4. Review: Steps 9, 9.1, 9.2, 9.2.1, 9.3, 9.4
Read `sections/review-army.md` in full and run Step 9 through Step 9.4 and its repeat decision as
written, `gstack-review-plan` included. Prints `Pre-Landing Review:`, `Gate edits:`. `NEXT: stop`
stops; `full` or `scoped` reruns Step 5 and this phase; `done` goes on.
Read `sections/review-army.md` in full. Each pass is one program (above) that runs Step 9's plan
and start, Step 9.1's specialists and Step 9.2's Red Team as written; on its return run the core
checklist, Steps 9.2, 9.2.1, 9.3 and 9.4 and the repeat decision as written, `gstack-review-plan`
included. Prints `Pre-Landing Review:`, `Gate edits:`. `NEXT: stop` stops; `full` or `scoped`
reruns Step 5 and this phase; `done` goes on.
## 5. Adversarial: Step 11
Read `sections/adversarial.md` and run Step 11 as written, only the voices the last full-pass plan
lists (neither prints `ADVERSARIAL: skipped (review plan)`). Missing outside coverage is `unavailable`, never clean. Queued fixes: Steps 9, 10, 11 again.
Read `sections/adversarial.md` and run Step 11 as written in one program (above), only the voices
the last full-pass plan lists (neither prints `ADVERSARIAL: skipped (review plan)`). Missing outside coverage is `unavailable`, never clean. Queued fixes: Steps 9, 10, 11 again.
## 6. Bind: Step 11.5
@@ -72,7 +91,7 @@ planned skip) agree on `wtree`, `start_wtree`, `end_wtree`. Save that reviewed t
- Step 12: `gstack-ship-release version --base <base> [--bump micro|patch] --why '<line>'`; MINOR or MAJOR is a question. `VERSION: skipped (policy: ...)` means the repo's ship policy stamps the version elsewhere: write no version.
- Step 13: one user-facing CHANGELOG entry; `gstack-prose-lint CHANGELOG.md --base origin/<base> --gate` until `PROSE_LINT: ok`. Step 14: TODOS.md deferrals and completions.
- Step 14.5, every ship: read `sections/documentation.md` and run its audit as written, then `gstack-ship-receipt docs-caps --base <base>`; print `Documentation:`. One re-audit at most.
- Step 14.5, every ship: read `sections/documentation.md` and run its audit as written, as one program (above) with `--next dispatch:docs`, then `gstack-ship-receipt docs-caps --base <base>`; print `Documentation:`. One re-audit at most.
- Steps 15, 16: commit features in bisectable chunks, then `gstack-ship-release finish --base <base> --reviewed-tree <t> --docs-post <post> --lane ... [--build <cmd>] --trailer '<t>'`: `ROUTE:`, `DOCS:`, `EVIDENCE:` (FRESH; STALE or MISSING reruns once), `PREGATE:`. Print them, the last `Gate edits:` and each `tests_ran`.
- Step 17: `gstack-ship-release push --branch <branch> [--gate-open <n>]`: `REDACT_PREPUSH:`, `SEALED:`, `PUSH:` (pushed or ALREADY_PUSHED). Its pre-push offer STOP runs Step 17's guard block (`HOOK_STATE:`, `HOOKS_IN_GIT_DIR:`, `PREPUSH_PROMPTED:`); unattended takes the recommended install. Never force-push.
+33 -14
View File
@@ -11,8 +11,13 @@ says otherwise; print its result lines verbatim. `STOP:` (exit 3) means nothing
names the next act. Read a section only when a phase or the list at the end sends you, never
up front. A STOP or decision this file does not cover: read `ship/SKILL.md` at that step, finish it
there, come back. Unattended, a question with a recommended option takes it (log it); a gate
disposition and every approval stay pending and end the run. Keep a private invocation record
(Release, Decisions, Reviews, Checks, Documentation, Next steps).
disposition and every approval stay pending and end the run. Keep a private invocation record (the
`--record` file): write its six headings at Phase 1, then fill each as its phase ends: **Checks**
(Phases 1-3: lanes, pregate, evals, `Coverage:`, the plan checks, `Scope Check:`), **Decisions** (any
phase: each answered or auto-taken question, in its words), **Reviews** (Phases 4-6: the plan line,
start tokens, `Pre-Landing Review:`, `Gate edits:`, the reviewed tree), **Release** (Phase 7: version,
CHANGELOG, commits), **Documentation** (Step 14.5) and **Next steps** (every phase: the next act).
`gstack-ship-budget handoff` refuses a record that lacks any of them.
**Reviewers are the sections' own.** Phases 3, 4 and 5 and Step 14.5 run their section in full, as
written: every audit, specialist, Red Team, fix-check, adversarial, outside-voice and documentation
@@ -21,11 +26,20 @@ restates none of them and never shortens a reviewer's brief.
Budget: read your own token counter (subagents included) in the program that runs
`gstack-ship-budget` (`--reading R --prior-reading <last R>`); `check --step <done> --next <step>`
opens each phase, `check --step <n> --next dispatch:<roles>` (one role per `DISPATCH:` line) each
reviewer batch, its `RESERVATION_ID:` going to that pass's `--start ... --reservation` (a later batch
of the pass takes no start). Settle each batch in the program that dispatched it: the reading
`BEFORE` right before the readers start, the wait until the last settles, the reading `AFTER`, then
`settle --reservation <id> --dispatched-at <BEFORE> --reading <AFTER> --readers <n>`. Obey `verdict=`.
opens each phase. Obey `verdict=`.
**One program per pass.** Each review pass (Phase 4), Step 11 (Phase 5) and the Step 14.5 audit is
one program, and the parent makes no other call while it runs: (1) the plan, `gstack-review-plan
--base <base>` with the pass's flags (its `DISPATCH:` lines; Step 14.5 has one reader, `docs`);
(2) `check --step <n> --next dispatch:<roles>`, one role per reader, then `gstack-review-log --start
<skill> --reservation <id>` once per row the step logs (Step 14.5 takes none); (3) the reading
`BEFORE`; (4) one subagent per reader, its brief copied verbatim from the section with only its
placeholders filled; (5) a poll until every reader settles; (6) the reading `AFTER`, then `settle
--reservation <id> --dispatched-at <BEFORE> --reading <AFTER> --readers <n>`; a later batch of the pass
(the Red Team, given the specialists' collected lines; an `ADMIT:` remainder) repeats (2)-(6) with
no start; (7) the collect: return only each reader's final output and status. The parent applies
the section's merge, judgment, fixes and persistence to that return in one or two calls; it never
walks the dispatch steps call by call.
## 1. Preflight and tests: Steps 0, 0.9, 1, 2, 3, 4, 5
@@ -48,18 +62,23 @@ only if `gstack-greptile-early detect` finds Greptile.
## 3. Audits: Steps 7, 8, 8.1, 8.2
Read `sections/test-coverage.md` and `sections/plan-completion.md` and run Steps 7, 8, 8.1 and 8.2
as they are written. Prints `Coverage:`, `Plan completion:` and Step 8.2's `Scope Check:`.
as they are written, here (never delegated). Prints `Coverage:`, `Plan completion:` and Step 8.2's
`Scope Check:`; write them under Checks. `check --step 8 --next 9` closes the phase: on `DELEGATE:`,
Steps 9-11.5 go to a fresh child (`sections/budget-handoff.md`) that reads only
`sections/review-army.md`, `sections/adversarial.md` and the record.
## 4. Review: Steps 9, 9.1, 9.2, 9.2.1, 9.3, 9.4
Read `sections/review-army.md` in full and run Step 9 through Step 9.4 and its repeat decision as
written, `gstack-review-plan` included. Prints `Pre-Landing Review:`, `Gate edits:`. `NEXT: stop`
stops; `full` or `scoped` reruns Step 5 and this phase; `done` goes on.
Read `sections/review-army.md` in full. Each pass is one program (above) that runs Step 9's plan
and start, Step 9.1's specialists and Step 9.2's Red Team as written; on its return run the core
checklist, Steps 9.2, 9.2.1, 9.3 and 9.4 and the repeat decision as written, `gstack-review-plan`
included. Prints `Pre-Landing Review:`, `Gate edits:`. `NEXT: stop` stops; `full` or `scoped`
reruns Step 5 and this phase; `done` goes on.
## 5. Adversarial: Step 11
Read `sections/adversarial.md` and run Step 11 as written, only the voices the last full-pass plan
lists (neither prints `ADVERSARIAL: skipped (review plan)`). Missing outside coverage is `unavailable`, never clean. Queued fixes: Steps 9, 10, 11 again.
Read `sections/adversarial.md` and run Step 11 as written in one program (above), only the voices
the last full-pass plan lists (neither prints `ADVERSARIAL: skipped (review plan)`). Missing outside coverage is `unavailable`, never clean. Queued fixes: Steps 9, 10, 11 again.
## 6. Bind: Step 11.5
@@ -70,7 +89,7 @@ planned skip) agree on `wtree`, `start_wtree`, `end_wtree`. Save that reviewed t
- Step 12: `gstack-ship-release version --base <base> [--bump micro|patch] --why '<line>'`; MINOR or MAJOR is a question. `VERSION: skipped (policy: ...)` means the repo's ship policy stamps the version elsewhere: write no version.
- Step 13: one user-facing CHANGELOG entry; `gstack-prose-lint CHANGELOG.md --base origin/<base> --gate` until `PROSE_LINT: ok`. Step 14: TODOS.md deferrals and completions.
- Step 14.5, every ship: read `sections/documentation.md` and run its audit as written, then `gstack-ship-receipt docs-caps --base <base>`; print `Documentation:`. One re-audit at most.
- Step 14.5, every ship: read `sections/documentation.md` and run its audit as written, as one program (above) with `--next dispatch:docs`, then `gstack-ship-receipt docs-caps --base <base>`; print `Documentation:`. One re-audit at most.
- Steps 15, 16: commit features in bisectable chunks, then `gstack-ship-release finish --base <base> --reviewed-tree <t> --docs-post <post> --lane ... [--build <cmd>] --trailer '<t>'`: `ROUTE:`, `DOCS:`, `EVIDENCE:` (FRESH; STALE or MISSING reruns once), `PREGATE:`. Print them, the last `Gate edits:` and each `tests_ran`.
- Step 17: `gstack-ship-release push --branch <branch> [--gate-open <n>]`: `REDACT_PREPUSH:`, `SEALED:`, `PUSH:` (pushed or ALREADY_PUSHED). Its pre-push offer STOP runs Step 17's guard block (`HOOK_STATE:`, `HOOKS_IN_GIT_DIR:`, `PREPUSH_PROMPTED:`); unattended takes the recommended install. Never force-push.
+12 -8
View File
@@ -29,11 +29,14 @@ Before each dispatch batch, reserve the batch as one sum, one role per `DISPATCH
On `ADMIT: k of n (remaining=<roles>)`, 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 dispatch batch.** On Capy one execute program runs, in order (elsewhere the same order):
`check --step <n> --next dispatch:<roles>`, `gstack-review-log --start <skill> --reservation <id>` once per
row the step logs (a later batch of the same pass (the Red Team or an ADMIT remainder) takes no start;
`--finish` settles it), the reading `BEFORE`, the readers exactly as the step dispatches them (their
prompts unchanged), the wait until the last settles, the reading `AFTER`, then `gstack-ship-budget settle`.
**One program per review pass.** On Capy one execute program runs a whole pass, in order (elsewhere
the same order): `gstack-review-plan` (its `DISPATCH:` lines), `check --step <n> --next dispatch:<roles>`,
`gstack-review-log --start <skill> --reservation <id>` 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`;
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.
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 <id> --reservation <id>
--dispatched-at <before> --reading <after> --readers <n>` (`--unknown` when the meter cannot see it);
@@ -47,9 +50,10 @@ end status=incomplete. Raising the cap is the owner's call: `/ship --resume <rec
`resume --run <id>` restores the ledger and record from the Drive checkpoint; a
corrupt ledger takes `--rebuild-from-record`.
**verdict=delegate.** The next segment (Steps 7–11.5, 12–22, or 1–22 at start) runs in a
fresh child; you wait and advance only after reconciling it:
1. `gstack-ship-budget handoff --run <id> --from 7 --to 11.5 --base origin/<base> --reading <n>`
**verdict=delegate.** The next segment (Steps 9–11.5, 12–22, or 1–22 at start) runs in a
fresh child; you wait and advance only after reconciling it. Steps 7–8 stay with the parent: the
Steps 9–11.5 child reads review-army.md and adversarial.md and takes their results from the record.
1. `gstack-ship-budget handoff --run <id> --from 9 --to 11.5 --base origin/<base> --reading <n>`
(add `--reviewed-tree <t>`, `--trail <file> --trail-run <id>`). It prints `HANDOFF:` and `RETURN_FILE:`;
the brief lists the child's `READ:` line ranges and names `--to` as its last step.
2. Launch: on Capy a subagent on the shared machine whose brief is "Load gstack, source
+12 -8
View File
@@ -27,11 +27,14 @@ Before each dispatch batch, reserve the batch as one sum, one role per `DISPATCH
On `ADMIT: k of n (remaining=<roles>)`, 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 dispatch batch.** On Capy one execute program runs, in order (elsewhere the same order):
`check --step <n> --next dispatch:<roles>`, `gstack-review-log --start <skill> --reservation <id>` once per
row the step logs (a later batch of the same pass (the Red Team or an ADMIT remainder) takes no start;
`--finish` settles it), the reading `BEFORE`, the readers exactly as the step dispatches them (their
prompts unchanged), the wait until the last settles, the reading `AFTER`, then `gstack-ship-budget settle`.
**One program per review pass.** On Capy one execute program runs a whole pass, in order (elsewhere
the same order): `gstack-review-plan` (its `DISPATCH:` lines), `check --step <n> --next dispatch:<roles>`,
`gstack-review-log --start <skill> --reservation <id>` 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`;
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.
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 <id> --reservation <id>
--dispatched-at <before> --reading <after> --readers <n>` (`--unknown` when the meter cannot see it);
@@ -45,9 +48,10 @@ end status=incomplete. Raising the cap is the owner's call: `/ship --resume <rec
`resume --run <id>` restores the ledger and record from the Drive checkpoint; a
corrupt ledger takes `--rebuild-from-record`.
**verdict=delegate.** The next segment (Steps 7–11.5, 12–22, or 1–22 at start) runs in a
fresh child; you wait and advance only after reconciling it:
1. `gstack-ship-budget handoff --run <id> --from 7 --to 11.5 --base origin/<base> --reading <n>`
**verdict=delegate.** The next segment (Steps 9–11.5, 12–22, or 1–22 at start) runs in a
fresh child; you wait and advance only after reconciling it. Steps 7–8 stay with the parent: the
Steps 9–11.5 child reads review-army.md and adversarial.md and takes their results from the record.
1. `gstack-ship-budget handoff --run <id> --from 9 --to 11.5 --base origin/<base> --reading <n>`
(add `--reviewed-tree <t>`, `--trail <file> --trail-run <id>`). It prints `HANDOFF:` and `RETURN_FILE:`;
the brief lists the child's `READ:` line ranges and names `--to` as its last step.
2. Launch: on Capy a subagent on the shared machine whose brief is "Load gstack, source
+68 -34
View File
@@ -61,7 +61,7 @@ describe('per-call thresholds above the host floor', () => {
test('at a segment boundary, 150k above the floor delegates and less continues; an unknown charge still delegates', () => {
const h = fresh();
const s = start(h, ['--token-cap', '80M', '--reading', '1000000']);
const low = check(h, s.run, '6', '7', ['--reading', '1230000', '--prior-reading', '1000000']);
const low = check(h, s.run, '8', '9', ['--reading', '1230000', '--prior-reading', '1000000']);
expect(low.code, low.out + low.err).toBe(0);
expect(h.field(low.out, 'BUDGET', 'above_floor')).toBe('80000');
expect(h.field(low.out, 'BUDGET', 'verdict')).toBe('continue');
@@ -76,7 +76,7 @@ describe('per-call thresholds above the host floor', () => {
test('--delegate measured (ship/DRIVER.md): an unknown charge continues at a segment boundary; 150k above the floor still delegates', () => {
const h = fresh();
const s = start(h, ['--token-cap', '80M', '--reading', '1000000', '--delegate', 'measured']);
const unknown = check(h, s.run, '6', '7', ['--reading', '1100000']);
const unknown = check(h, s.run, '8', '9', ['--reading', '1100000']);
expect(unknown.code, unknown.out + unknown.err).toBe(0);
expect(h.field(unknown.out, 'BUDGET', 'above_floor')).toBe('unknown');
expect(h.field(unknown.out, 'BUDGET', 'verdict')).toBe('continue');
@@ -105,7 +105,7 @@ describe('a child never stops on the context rule', () => {
test('#36 replay: a child at its first boundary with zero completed steps continues with one CONTEXT_HIGH note', () => {
const h = fresh();
const s = start(h, ['--token-cap', '400M', '--reading', '98000']);
for (const [from, to, step, at] of [['7', '11.5', '6', 1_000_000], ['12', '22', '11.5', 2_000_000]] as const) {
for (const [from, to, step, at] of [['9', '11.5', '8', 1_000_000], ['12', '22', '11.5', 2_000_000]] as const) {
const c = handOff(h, s.run, from, to, at);
const r = check(h, c.run, step, from, ['--reading', String(at + 200_000)]);
expect(r.code, r.out + r.err).toBe(0);
@@ -126,18 +126,18 @@ describe('a child never stops on the context rule', () => {
const child = handOff(h, s.run, '1', '22', 200_000);
expect(h.ledger(child.ledger).depth).toBe(1);
expect(check(h, child.run, '1', '2', ['--reading', '600000']).code).toBe(0);
const d = check(h, child.run, '6', '7', ['--reading', '1500000']);
const d = check(h, child.run, '8', '9', ['--reading', '1500000']);
expect(d.code, d.out + d.err).toBe(0);
expect(h.field(d.out, 'BUDGET', 'verdict')).toBe('delegate');
expect(d.out).toContain(`gstack-ship-budget handoff --run ${child.run} --from 7 --to 22`);
expect(d.out).toContain(`gstack-ship-budget handoff --run ${child.run} --from 9 --to 22`);
expect(d.out).not.toContain('RESERVATION');
const room = ['admission_cap', 'used', 'reserved', 'unknown', 'tail'].map(k => Number(h.field(d.out, 'BUDGET', k)));
const grand = handOff(h, child.run, '7', '22', 1_500_000);
const grand = handOff(h, child.run, '9', '22', 1_500_000);
expect(grand.subCap).toBe(room[0]! - room[1]! - room[2]! - room[3]! - room[4]!);
expect(h.field(grand.v.out, 'BUDGET', 'admission_cap')).toBe(String(grand.subCap));
const g = h.ledger(grand.ledger);
expect(g).toMatchObject({ role: 'child', depth: 2, parent: { run: child.run, ledger: child.ledger, from: '7', to: '22' } });
expect(h.ledger(child.ledger).handoffs[0]).toMatchObject({ depth: 2, from: '7', to: '22' });
expect(g).toMatchObject({ role: 'child', depth: 2, parent: { run: child.run, ledger: child.ledger, from: '9', to: '22' } });
expect(h.ledger(child.ledger).handoffs[0]).toMatchObject({ depth: 2, from: '9', to: '22' });
const text = fs.readFileSync(grand.brief, 'utf8');
expect(text).toContain('"depth": 2');
expect(text).not.toContain('a delegate rule stops you');
@@ -157,11 +157,11 @@ describe('a child never stops on the context rule', () => {
chain.push(c.ledger);
}
expect(chain.map(f => h.ledger(f).depth)).toEqual([1, 2, 3]);
const r = check(h, run, '6', '7', ['--reading', String((reading += 100_000))]);
const r = check(h, run, '8', '9', ['--reading', String((reading += 100_000))]);
expect(r.code, r.out + r.err).toBe(0);
expect(h.field(r.out, 'BUDGET', 'verdict')).toBe('continue');
expect(r.out).toMatch(/^CONTEXT_HIGH: .*chain depth 3 is the maximum/m);
const refused = h.sb(['handoff', '--run', run, '--from', '7', '--to', '22', '--base', 'origin/main', '--reading', String(reading)]);
const refused = h.sb(['handoff', '--run', run, '--from', '9', '--to', '22', '--base', 'origin/main', '--reading', String(reading)]);
expect(refused.code).toBe(2);
expect(refused.err).toContain('chain depth 3');
});
@@ -174,12 +174,12 @@ describe('a child never stops on the context rule', () => {
const returnFile = ho.out.match(/^RETURN_FILE: (\S+)$/m)![1]!;
const child = h.field(h.sb(['verify-handoff', '--in', brief, '--reading', '110000']).out, 'BUDGET_RUN', 'id');
expect(check(h, child, '1', '2', ['--reading', '120000']).code).toBe(0);
const stop = check(h, child, '6', '7', ['--reading', '2120000']);
const stop = check(h, child, '8', '9', ['--reading', '2120000']);
expect(stop.code).toBe(3);
expect(stop.out).toContain('STOP: reason=budget_exhausted');
expect(stop.out).toContain('(BUDGET_CAP_REACHED)');
expect(stop.out).not.toContain('context_threshold');
expect(fs.readFileSync(returnFile, 'utf8')).toMatch(/^RETURN: handoff=[0-9a-f]{64} status=incomplete steps=1-22 next=7/);
expect(fs.readFileSync(returnFile, 'utf8')).toMatch(/^RETURN: handoff=[0-9a-f]{64} status=incomplete steps=1-22 next=9/);
});
});
@@ -189,9 +189,9 @@ describe('a chained return is charged once', () => {
const s = start(h, ['--token-cap', '200M', '--reading', '1000000']);
const child = handOff(h, s.run, '1', '22', 1_100_000, ['--reading', '1200000']);
expect(check(h, child.run, '1', '2', ['--reading', '1500000']).code).toBe(0);
expect(h.field(check(h, child.run, '6', '7', ['--reading', '2000000']).out, 'BUDGET', 'verdict')).toBe('delegate');
const grand = handOff(h, child.run, '7', '22', 2_100_000, ['--reading', '2200000']);
expect(check(h, grand.run, '7', '8', ['--reading', '3000000']).code).toBe(0);
expect(h.field(check(h, child.run, '8', '9', ['--reading', '2000000']).out, 'BUDGET', 'verdict')).toBe('delegate');
const grand = handOff(h, child.run, '9', '22', 2_100_000, ['--reading', '2200000']);
expect(check(h, grand.run, '9', '10', ['--reading', '3000000']).code).toBe(0);
expect(h.sb(['return', '--run', grand.run, '--to', '22', '--changed', 'Reviews', '--reading', '6000000']).code).toBe(0);
const mid = h.sb(['reconcile', '--run', child.run, '--return', grand.returnFile, '--reading', '6100000']);
expect(mid.code, mid.out + mid.err).toBe(0);
@@ -239,13 +239,13 @@ describe('a chained return is charged once', () => {
const s = start(h, ['--token-cap', '400M', '--meter', 'estimate']);
const child = handOff(h, s.run, '1', '22', null);
expect(check(h, child.run, '1', '2').code).toBe(0);
expect(h.field(check(h, child.run, '6', '7').out, 'BUDGET', 'verdict')).toBe('delegate');
const grand = handOff(h, child.run, '7', '22', null);
expect(check(h, grand.run, '7', '8').code).toBe(0);
expect(check(h, grand.run, '8', '9').code).toBe(0);
expect(h.field(check(h, child.run, '8', '9').out, 'BUDGET', 'verdict')).toBe('delegate');
const grand = handOff(h, child.run, '9', '22', null);
expect(check(h, grand.run, '9', '10').code).toBe(0);
expect(check(h, grand.run, '10', '11').code).toBe(0);
const gret = h.sb(['return', '--run', grand.run, '--to', '22', '--changed', 'Reviews']);
const grandUsed = Number(h.field(gret.out, 'BUDGET', 'used'));
expect(grandUsed).toBe(stepProjection(cal, 'estimate-tokens', '8'));
expect(grandUsed).toBe(stepProjection(cal, 'estimate-tokens', '10'));
const childOwn = stepProjection(cal, 'estimate-tokens', '2');
for (let i = 0; i < 2; i++) expect(h.sb(['reconcile', '--run', child.run, '--return', grand.returnFile]).code).toBe(0);
const cret = h.sb(['return', '--run', child.run, '--to', '22', '--changed', 'Reviews']);
@@ -264,8 +264,8 @@ describe('a chained return is charged once', () => {
const s = start(h, ['--token-cap', '400M', '--session', FIXTURE.sessions.parent]);
const child = handOff(h, s.run, '1', '22', null, ['--session', 'child-session-1']);
expect(check(h, child.run, '1', '2').code).toBe(0);
expect(h.field(check(h, child.run, '6', '7').out, 'BUDGET', 'verdict')).toBe('delegate');
const ho = h.sb(['handoff', '--run', child.run, '--from', '7', '--to', '22', '--base', 'origin/main']);
expect(h.field(check(h, child.run, '8', '9').out, 'BUDGET', 'verdict')).toBe('delegate');
const ho = h.sb(['handoff', '--run', child.run, '--from', '9', '--to', '22', '--base', 'origin/main']);
const brief = ho.out.match(/^HANDOFF: (\S+)/m)![1]!;
const fake = path.join(h.root, 'fake-claude');
fs.writeFileSync(fake, '#!/bin/sh\nexit 0\n', { mode: 0o755 });
@@ -282,7 +282,7 @@ describe('a reconcile that stops the parent stops every check (replay 2, #36)',
test('after an incomplete child, check refuses every step until resume; a regenerated child that returns complete also clears it', () => {
const h = fresh();
const s = start(h, ['--token-cap', '200M', '--reading', '0', '--reserve', '0']);
const child = handOff(h, s.run, '7', '11.5', 1_000_000);
const child = handOff(h, s.run, '9', '11.5', 1_000_000);
const ret = h.sb(['return', '--run', child.run, '--to', '9', '--status', 'incomplete', '--reading', '5000000']);
expect(ret.code, ret.out + ret.err).toBe(0);
const rec = h.sb(['reconcile', '--run', s.run, '--return', child.returnFile, '--reading', '5100000']);
@@ -292,19 +292,19 @@ describe('a reconcile that stops the parent stops every check (replay 2, #36)',
for (const next of ['9', '7', 'dispatch:red-team']) {
const c = check(h, s.run, '8', next, ['--reading', '5200000']);
expect(c.code, next).toBe(3);
expect(c.out).toMatch(/^STOP: reason=child_incomplete at Step 7; resume first: /m);
expect(c.out).toMatch(/^STOP: reason=child_incomplete at Step 9; resume first: /m);
expect(c.out).not.toContain('RESERVATION:');
}
const resumed = h.sb(['resume', '--run', s.run, '--reading', '5300000']);
expect(resumed.code, resumed.out + resumed.err).toBe(0);
expect(resumed.out).toContain('RESUMED: from=Step 7 (child_incomplete)');
expect(resumed.out).toContain('RESUMED: from=Step 9 (child_incomplete)');
expect(check(h, s.run, '8', '9', ['--reading', '5400000']).code).toBe(0);
const t = start(h, ['--token-cap', '200M', '--reading', '0', '--reserve', '0']);
const first = handOff(h, t.run, '7', '11.5', 1_000_000);
const first = handOff(h, t.run, '9', '11.5', 1_000_000);
h.sb(['return', '--run', first.run, '--to', '9', '--status', 'incomplete', '--reading', '2000000']);
expect(h.sb(['reconcile', '--run', t.run, '--return', first.returnFile, '--reading', '2000000']).code).toBe(3);
const second = handOff(h, t.run, '7', '11.5', 2_100_000);
const second = handOff(h, t.run, '9', '11.5', 2_100_000);
expect(h.sb(['return', '--run', second.run, '--to', '11.5', '--reading', '3000000']).code).toBe(0);
const done = h.sb(['reconcile', '--run', t.run, '--return', second.returnFile, '--reading', '3100000']);
expect(done.code, done.out).toBe(0);
@@ -318,12 +318,12 @@ describe('a child returns complete only when its own work is finished', () => {
const s = start(h, ['--token-cap', '200M', '--reading', '98000']);
const child = handOff(h, s.run, '1', '22', 200_000);
expect(check(h, child.run, '1', '2', ['--reading', '600000']).code).toBe(0);
expect(h.field(check(h, child.run, '6', '7', ['--reading', '1500000']).out, 'BUDGET', 'verdict')).toBe('delegate');
const grand = handOff(h, child.run, '7', '22', 1_500_000);
expect(h.field(check(h, child.run, '8', '9', ['--reading', '1500000']).out, 'BUDGET', 'verdict')).toBe('delegate');
const grand = handOff(h, child.run, '9', '22', 1_500_000);
const ret = h.sb(['return', '--run', child.run, '--to', '22', '--changed', 'Reviews', '--reading', '1600000']);
expect(ret.code, ret.out + ret.err).toBe(0);
expect(ret.out).toMatch(/^RETURN: handoff=[0-9a-f]{64} status=incomplete steps=1-22/);
expect(ret.out).toContain(`UNRECONCILED: handoff=${h.ledger(child.ledger).handoffs[0].sha} steps=7-22`);
expect(ret.out).toContain(`UNRECONCILED: handoff=${h.ledger(child.ledger).handoffs[0].sha} steps=9-22`);
expect(ret.out).toContain(grand.returnFile);
const top = h.sb(['reconcile', '--run', s.run, '--return', child.returnFile, '--reading', '1700000']);
expect(top.code).toBe(3);
@@ -334,14 +334,14 @@ describe('a child returns complete only when its own work is finished', () => {
const h = fresh();
const s = start(h, ['--token-cap', '900M', '--reading', '0', '--reserve', '0']);
const subCap = K.stop_overhead + Math.floor(1.5 * K.per_dispatch);
const ho = h.sb(['handoff', '--run', s.run, '--from', '7', '--to', '11.5', '--base', 'origin/main', '--reading', '100000', '--sub-cap', String(subCap)]);
const ho = h.sb(['handoff', '--run', s.run, '--from', '9', '--to', '11.5', '--base', 'origin/main', '--reading', '100000', '--sub-cap', String(subCap)]);
expect(ho.code, ho.out + ho.err).toBe(0);
const child = h.field(h.sb(['verify-handoff', '--in', ho.out.match(/^HANDOFF: (\S+)/m)![1]!, '--reading', '100000']).out, 'BUDGET_RUN', 'id');
const b = check(h, child, '9', 'dispatch:specialist,specialist,red-team', ['--reading', '100000']);
expect(b.code, b.out + b.err).toBe(0);
expect(b.out).toMatch(/^ADMIT: 1 of 3 \(remaining=specialist,red-team\)/m);
const ret = h.sb(['return', '--run', child, '--to', '11.5', '--reading', '100000']);
expect(ret.out).toMatch(/^RETURN: handoff=[0-9a-f]{64} status=incomplete steps=7-11.5/);
expect(ret.out).toMatch(/^RETURN: handoff=[0-9a-f]{64} status=incomplete steps=9-11.5/);
expect(ret.out).toMatch(/^DROPPED: step=9 reservation=\S+ roles=specialist,red-team/m);
expect(ret.out.trim().split('\n').at(-1)).toMatch(/^GSTACK_RESULT: skill=ship status=incomplete run=/);
});
@@ -349,12 +349,46 @@ describe('a child returns complete only when its own work is finished', () => {
test("a grandchild handoff outside the child's own Steps refuses and reserves nothing", () => {
const h = fresh();
const s = start(h, ['--token-cap', '200M', '--reading', '98000']);
const child = handOff(h, s.run, '7', '11.5', 200_000);
const child = handOff(h, s.run, '9', '11.5', 200_000);
for (const [from, to] of [['7', '22'], ['6', '11.5']]) {
const r = h.sb(['handoff', '--run', child.run, '--from', from!, '--to', to!, '--base', 'origin/main', '--reading', '300000']);
expect(r.code).toBe(2);
expect(r.err).toContain(`--from ${from} --to ${to} is outside this child's Steps 7-11.5`);
expect(r.err).toContain(`--from ${from} --to ${to} is outside this child's Steps 9-11.5`);
}
expect(h.ledger(child.ledger).reservations.filter((r: any) => r.kind === 'child')).toHaveLength(0);
});
});
describe('variant E: the review segment and the docs reader (GSTA-167 replay 5)', () => {
// Value: protects=Steps 7-8 stay with the parent and the fresh child starts at Step 9 with Steps 9-11.5, so the child reads only the review sections; fails_when=the Step 7 boundary delegates again or the Step 9 boundary hands off any other segment;
// why_new=the boundary tests above take whichever segment the code names; seam=none; no_claim=what a child costs (handoffs/GSTA-167/replay5-anatomy.md estimates ~3M on #34)
test('Step 7 is no boundary; Step 9 delegates Steps 9-11.5 and Step 12 delegates Steps 12-22', () => {
const h = fresh();
const high = (step: string, next: string, at: number) => check(h, start(h, ['--token-cap', '400M', '--reading', '1000000']).run, step, next, ['--reading', String(at + 400_000), '--prior-reading', String(at)]);
const seven = high('6', '7', 1_100_000);
expect(h.field(seven.out, 'BUDGET', 'verdict')).toBe('continue');
expect(seven.out).not.toContain('DELEGATE:');
const nine = high('8', '9', 1_100_000);
expect(h.field(nine.out, 'BUDGET', 'verdict')).toBe('delegate');
expect(nine.out).toMatch(/^DELEGATE: Steps 9-11\.5 go to a fresh child: gstack-ship-budget handoff --run \S+ --from 9 --to 11\.5 /m);
expect(high('11.5', '12', 1_100_000).out).toMatch(/^DELEGATE: Steps 12-22 go to a fresh child/m);
});
// Value: protects=the Step 14.5 documentation audit is a metered dispatch role: priced per reader, reserved, and measured from its own reading pair, so its reader stays out of Step 14.5's step sample; fails_when=dispatch:docs is refused, priced other than a reader, or settles UNMEASURED with a valid pair, or the step sample swallows the audit (#34 replay 5: projected 1.83M, actual 7.64M, calibration=step:5.092);
// why_new=no role covered the docs audit; seam=none; no_claim=the docs reader's real size
test('dispatch:docs is reserved at the reader price, settles with a DRIFT line, and Step 14.5 measures only the parent\'s own calls', () => {
const h = fresh();
const s = start(h, ['--token-cap', '400M', '--reading', '1000000']);
expect(check(h, s.run, '14', '14.5', ['--reading', '1100000']).code).toBe(0);
const c = check(h, s.run, '14.5', 'dispatch:docs', ['--reading', '1200000']);
expect(c.code, c.out + c.err).toBe(0);
expect(h.field(c.out, 'BUDGET', 'projected')).toBe(String(K.per_dispatch));
const id = h.field(c.out, 'RESERVATION', 'id');
const settled = h.sb(['settle', '--run', s.run, '--reservation', id, '--dispatched-at', '1200000', '--reading', '3200000', '--readers', '1']);
expect(settled.code, settled.out + settled.err).toBe(0);
expect(settled.out).toMatch(new RegExp(`^DRIFT: id=${id} kind=dispatch next=dispatch:docs projected=${K.per_dispatch} actual=2000000 `, 'm'));
const step = check(h, s.run, '14.5', '15', ['--reading', '3500000']);
expect(step.out).toMatch(/^DRIFT: id=\S+ kind=step next=14\.5 projected=\d+ actual=400000 /m);
expect(h.sb(['check', '--run', s.run, '--step', '15', '--next', 'dispatch:doc', '--reading', '3600000']).err).toContain('valid: specialist, red-team, adversarial-native, outside-voice, fix-check, docs');
});
});
+1 -1
View File
@@ -49,7 +49,7 @@ describe('the Capy meter is the agent\'s own jam counter', () => {
const h = fresh();
const s = h.sb(['start', '--record', h.record(), '--token-cap', '200M', '--reading', '1000000']);
const run = h.field(s.out, 'BUDGET_RUN', 'id');
const ho = h.sb(['handoff', '--run', run, '--from', '7', '--to', '11.5', '--base', 'origin/main', '--reading', '8000000']);
const ho = h.sb(['handoff', '--run', run, '--from', '9', '--to', '11.5', '--base', 'origin/main', '--reading', '8000000']);
expect(ho.code, ho.out + ho.err).toBe(0);
const brief = ho.out.match(/^HANDOFF: (\S+) /m)![1]!;
const returnFile = ho.out.match(/^RETURN_FILE: (\S+)$/m)![1]!;
+14 -12
View File
@@ -88,7 +88,7 @@ describe('a dispatch is measured to its return, never across the parent\'s own w
test('#34 replay: a Red Team settled by the step change after the child\'s review and QA is UNMEASURED, so the voices keep the per-reader factor', () => {
const h = fresh();
const s = start(h, 200_000_000);
check(h, s.run, '8', '9', 3_500_000);
check(h, s.run, '8', '9', 3_500_000, ['--prior-reading', '3400000']);
const specialists = h.field(check(h, s.run, '9', 'dispatch:' + SIX.join(','), 4_000_000).out, 'RESERVATION', 'id');
expect(h.reviewLog(['--start', 'review', '--reservation', specialists]).code).toBe(0);
let reading = 4_000_000 + 6 * READER;
@@ -109,7 +109,7 @@ describe('a dispatch is measured to its return, never across the parent\'s own w
test('settled with a reading at its return, the Red Team measures only its own interval; the parent\'s QA after it lands in Step 9\'s sample', () => {
const h = fresh();
const s = start(h, 200_000_000);
check(h, s.run, '8', '9', 3_500_000);
check(h, s.run, '8', '9', 3_500_000, ['--prior-reading', '3400000']);
const red = h.field(check(h, s.run, '9', 'dispatch:red-team', 4_000_000).out, 'RESERVATION', 'id');
expect(h.reviewLog(['--start', 'review', '--reservation', red]).code).toBe(0);
const settled = ok(h, ['settle', '--run', s.run, '--reservation', red, '--dispatched-at', '4000000', '--reading', String(4_000_000 + READER)]);
@@ -231,7 +231,7 @@ describe('the per-call charge takes a prior reading from the previous call only
test('#34 replay: a child passing the parent\'s handoff reading as its prior gets call=unknown, not 1068386', () => {
const h = fresh();
const s = start(h, 400_000_000, 2_110_477);
const ho = ok(h, ['handoff', '--run', s.run, '--from', '7', '--to', '11.5', '--base', 'origin/main', '--reading', '8094129']);
const ho = ok(h, ['handoff', '--run', s.run, '--from', '9', '--to', '11.5', '--base', 'origin/main', '--reading', '8094129']);
const brief = ho.out.match(/^HANDOFF: (\S+) /m)![1]!;
const v = ok(h, ['verify-handoff', '--in', brief, '--reading', '8800000']);
const child = h.field(v.out, 'BUDGET_RUN', 'id');
@@ -253,22 +253,24 @@ describe('the budget section states both contracts', () => {
// 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 reviewer batches on Capy'));
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 reviewer batch is one execute program in a fixed order, so the readers' reading pair brackets only them and no start or poll costs its own parent call, and each reviewer's brief is copied from its section; fails_when=the order breaks (a start after the readers are created, a settle before the poll) or the Capy paragraph stops copying the section's brief;
// why_new=no test reads the batch order; seam=none; no_claim=the model's adherence
test('a reviewer batch is one execute program: check --step --next, review-log --start, BEFORE, create, poll, AFTER, settle', () => {
// Value: protects=each review pass (and Step 11, and the Step 14.5 audit) is one execute program in a fixed order, so the readers' reading pair brackets only them and no plan, start, poll or later batch costs its own parent call, and each reviewer's brief is copied from its section; fails_when=the order breaks (a start after the readers are created, a settle before the poll, the plan after the dispatch) or the Capy paragraph goes back to one program per batch or stops copying the section's brief;
// why_new=no test reads the pass order; seam=none; no_claim=the model's adherence (GSTA-167 replay 5: arm D walked Steps 9-9.4 in 27 calls)
test('a review pass is one execute program: plan, check --next dispatch, review-log --start, BEFORE, create, poll, AFTER, settle, collect', () => {
const capy = fs.readFileSync(path.join(import.meta.dir, '..', 'capy', 'skill', 'gstack', 'SKILL.md'), 'utf8');
const pass = capy.slice(capy.indexOf('/ship reviewer batches on Capy'), capy.indexOf('## Plan, approve, ship'));
expect(pass).toMatch(/one execute program per batch/);
const pass = capy.slice(capy.indexOf('/ship reviewers on Capy'), capy.indexOf('## Plan, approve, ship'));
expect(pass).toMatch(/one execute program per review pass, per Step 11 and for the Step 14\.5 audit/);
expect(pass).not.toMatch(/one execute program per batch/);
expect(pass).toContain('its brief copied verbatim from the section that dispatches it');
const order = ['check --run <id> --step <n> --next dispatch:<roles>', 'gstack-review-log --start', 'tools.subagent.create', 'tools.subagent.get', 'AFTER', 'gstack-ship-budget settle'];
const block = tmpl.slice(tmpl.indexOf('**One program per dispatch batch.**'));
const tmplOrder = ['check --step <n> --next dispatch:<roles>', 'gstack-review-log --start', 'BEFORE', 'AFTER', 'gstack-ship-budget settle'];
expect(pass).toContain('never walks the dispatch steps call by call');
const order = ['gstack-review-plan --base <base>', 'check --run <id> --step <n> --next dispatch:<roles>', 'gstack-review-log --start', 'tools.subagent.create', 'tools.subagent.get', 'AFTER', 'gstack-ship-budget settle', 'return only each reader\'s final output'];
const block = tmpl.slice(tmpl.indexOf('**One program per review pass.**'));
const tmplOrder = ['gstack-review-plan', 'check --step <n> --next dispatch:<roles>', 'gstack-review-log --start', 'BEFORE', 'AFTER', 'gstack-ship-budget settle', 'the collect'];
for (const [text, want] of [[pass, order], [block, tmplOrder]] as const) {
const at = want.map(o => text.indexOf(o));
expect(at.every(i => i >= 0), want.filter((_, k) => at[k]! < 0).join(',')).toBe(true);
+2 -2
View File
@@ -226,7 +226,7 @@ describe('gstack-review-log --start under a token cap', () => {
test('close counts starts reserved in its handoff children; an unreserved row in the child window is still flagged', () => {
const h = fresh();
const root = capped(h, '200M');
const ho = h.sb(['handoff', '--run', root.run, '--from', '7', '--to', '11.5', '--base', 'origin/main', '--reading', '1000000']);
const ho = h.sb(['handoff', '--run', root.run, '--from', '9', '--to', '11.5', '--base', 'origin/main', '--reading', '1000000']);
expect(ho.code, ho.out + ho.err).toBe(0);
const v = h.sb(['verify-handoff', '--in', ho.out.match(/^HANDOFF: (\S+) /m)![1]!, '--reading', '100000']);
expect(v.code, v.out + v.err).toBe(0);
@@ -243,7 +243,7 @@ describe('gstack-review-log --start under a token cap', () => {
expect(clean.code, clean.out).toBe(0);
const h2 = fresh();
const root2 = capped(h2, '200M');
const ho2 = h2.sb(['handoff', '--run', root2.run, '--from', '7', '--to', '11.5', '--base', 'origin/main', '--reading', '1000000']);
const ho2 = h2.sb(['handoff', '--run', root2.run, '--from', '9', '--to', '11.5', '--base', 'origin/main', '--reading', '1000000']);
expect(h2.sb(['verify-handoff', '--in', ho2.out.match(/^HANDOFF: (\S+) /m)![1]!, '--reading', '100000']).code).toBe(0);
expect(h2.reviewLog([JSON.stringify({ ...row, timestamp: new Date(Date.now() + 1000).toISOString() })]).code).toBe(0);
const flagged = h2.sb(['close', '--run', root2.run, '--reading', '1500000']);
+4 -4
View File
@@ -44,15 +44,15 @@ describe('check --runtime-lines', () => {
const r = h.sb(['start', '--record', h.record(`r${cap}.md`), '--token-cap', cap, '--reading', '0', '--call-floor', '0']);
return h.field(r.out, 'BUDGET_RUN', 'id');
};
const small = h.sb(['check', '--run', start('80M'), '--step', '6', '--next', '7', '--reading', '1220000', '--prior-reading', '1000000', '--runtime-lines', '0']);
const small = h.sb(['check', '--run', start('80M'), '--step', '8', '--next', '9', '--reading', '1220000', '--prior-reading', '1000000', '--runtime-lines', '0']);
expect(small.code).toBe(0);
expect(h.field(small.out, 'BUDGET', 'verdict')).toBe('continue');
expect(small.out).toContain('SMALL_RUN: runtime_lines=0');
expect(small.out).not.toContain('DELEGATE:');
const large = h.sb(['check', '--run', start('81M'), '--step', '6', '--next', '7', '--reading', '1220000', '--prior-reading', '1000000', '--runtime-lines', '900']);
const large = h.sb(['check', '--run', start('81M'), '--step', '8', '--next', '9', '--reading', '1220000', '--prior-reading', '1000000', '--runtime-lines', '900']);
expect(h.field(large.out, 'BUDGET', 'verdict')).toBe('delegate');
const omitted = h.sb(['check', '--run', start('82M'), '--step', '6', '--next', '7', '--reading', '1220000', '--prior-reading', '1000000']);
const omitted = h.sb(['check', '--run', start('82M'), '--step', '8', '--next', '9', '--reading', '1220000', '--prior-reading', '1000000']);
expect(h.field(omitted.out, 'BUDGET', 'verdict')).toBe('delegate');
expect(h.sb(['check', '--run', start('83M'), '--step', '6', '--next', '7', '--reading', '5', '--runtime-lines', 'many']).code).toBe(2);
expect(h.sb(['check', '--run', start('83M'), '--step', '8', '--next', '9', '--reading', '5', '--runtime-lines', 'many']).code).toBe(2);
});
});
+5 -5
View File
@@ -189,11 +189,11 @@ describe('delegation', () => {
test('a segment boundary with an unknown per-call charge delegates; --delegate never stops instead', () => {
const h = fresh();
const a = started(h, ['--token-cap', '80M', '--reading', '0']);
const d = h.sb(['check', '--run', a.run, '--step', '6', '--next', '7', '--reading', '100']);
const d = h.sb(['check', '--run', a.run, '--step', '8', '--next', '9', '--reading', '100']);
expect(d.code).toBe(0);
expect(h.field(d.out, 'BUDGET', 'verdict')).toBe('delegate');
expect(d.out).not.toContain('RESERVATION');
const small = h.sb(['check', '--run', a.run, '--step', '6', '--next', '7', '--reading', '1100', '--prior-reading', '1000']);
const small = h.sb(['check', '--run', a.run, '--step', '8', '--next', '9', '--reading', '1100', '--prior-reading', '1000']);
expect(h.field(small.out, 'BUDGET', 'verdict')).toBe('continue');
const n = started(h, ['--token-cap', '80M', '--reading', '0', '--delegate', 'never']);
const stop = h.sb(['check', '--run', n.run, '--step', '11.5', '--next', '12', '--reading', '100']);
@@ -204,9 +204,9 @@ describe('delegation', () => {
test('full chain: delegate, handoff, verify, child checks, 40-line return, idempotent reconcile, close', () => {
const h = fresh();
const s = started(h, ['--token-cap', '80M', '--reading', '1000000']);
const d = h.sb(['check', '--run', s.run, '--step', '6', '--next', '7', '--reading', '1200000', '--prior-reading', '800000']);
const d = h.sb(['check', '--run', s.run, '--step', '8', '--next', '9', '--reading', '1200000', '--prior-reading', '800000']);
expect(h.field(d.out, 'BUDGET', 'verdict')).toBe('delegate');
const ho = h.sb(['handoff', '--run', s.run, '--from', '7', '--to', '11.5', '--base', 'origin/main', '--reading', '1300000']);
const ho = h.sb(['handoff', '--run', s.run, '--from', '9', '--to', '11.5', '--base', 'origin/main', '--reading', '1300000']);
expect(ho.code).toBe(0);
const brief = ho.out.match(/^HANDOFF: (\S+) sha256=([0-9a-f]{64}) sub_cap=(\d+)$/m)!;
expect(brief).toBeTruthy();
@@ -225,7 +225,7 @@ describe('delegation', () => {
expect(cc.code).toBe(0);
const past = h.sb(['check', '--run', child, '--step', '11', '--next', '12', '--reading', '6000000']);
expect(past.code).toBe(0);
expect(past.out).toMatch(/^CONTEXT_HIGH: .*Step 12 is past this child's Steps 7-11\.5/m);
expect(past.out).toMatch(/^CONTEXT_HIGH: .*Step 12 is past this child's Steps 9-11\.5/m);
const ret = h.sb(['return', '--run', child, '--to', '11.5', '--changed', 'Reviews,Checks', '--reading', '9000000']);
expect(ret.code).toBe(0);
expect(ret.out.trim().split('\n').length).toBeLessThanOrEqual(40);
+29
View File
@@ -122,6 +122,35 @@ describe('ship/DRIVER.md covers the skeleton', () => {
});
});
describe('one program per pass (GSTA-167 variant E)', () => {
const flat = (s: string) => s.replace(/\s+/g, ' ');
const phase = (h: string) => flat(doc.slice(doc.indexOf(h), doc.indexOf('\n## ', doc.indexOf(h) + 1)));
// Value: protects=a review pass, Step 11 and the Step 14.5 audit each run as one program in the fixed order plan, reserve and start, BEFORE, readers, poll, AFTER and settle, later batches, collect, so the parent spends no call inside a pass and the reading pair brackets only the readers; fails_when=a phase goes back to walking Steps 9-9.4 call by call (replay 5: arm D's 69 orchestrator calls in Steps 7-14.5 against round 4's 21), the order breaks, or a phase stops pointing at the shared program;
// why_new=the coverage tests above check step ids, codes and section names only; seam=none; no_claim=the parent's adherence or the call count it reaches
test('the shared program lists its acts in order and Phases 4, 5 and Step 14.5 run through it', () => {
const block = flat(doc.slice(doc.indexOf('**One program per pass.**'), doc.indexOf('\n## ', doc.indexOf('**One program per pass.**'))));
expect(block).toContain('Each review pass (Phase 4), Step 11 (Phase 5) and the Step 14.5 audit is one program, and the parent makes no other call while it runs');
const order = ['gstack-review-plan --base <base>', 'check --step <n> --next dispatch:<roles>', 'gstack-review-log --start <skill> --reservation <id>', '(3) the reading `BEFORE`',
'(4) one subagent per reader, its brief copied verbatim from the section', '(5) a poll until every reader settles', '(6) the reading `AFTER`', '--dispatched-at <BEFORE> --reading <AFTER> --readers <n>',
'a later batch of the pass (the Red Team', '(7) the collect: return only each reader\'s final output', 'it never walks the dispatch steps call by call'];
const at = order.map(o => block.indexOf(o));
expect(order.filter((_, k) => at[k]! < 0)).toEqual([]);
expect([...at].sort((a, b) => a - b)).toEqual(at);
expect(phase('## 4. Review')).toContain('Each pass is one program (above)');
expect(phase('## 5. Adversarial')).toContain('in one program (above)');
expect(phase('## 7. Release')).toContain('read `sections/documentation.md` and run its audit as written, as one program (above) with `--next dispatch:docs`');
});
// Value: protects=Steps 7-8 run in the parent and the fresh child starts at Step 9, reading only the review sections and the record; fails_when=the driver delegates the audits again or the child is told to read the audit sections;
// why_new=the budget tests pin the boundary, not the driver's phase text; seam=none; no_claim=the child's actual reads
test('Phase 3 keeps the audits here and closes with the Step 9 handoff to a review-only child', () => {
const p3 = phase('## 3. Audits');
for (const s of ['as they are written, here (never delegated)', 'write them under Checks', '`check --step 8 --next 9` closes the phase', 'Steps 9-11.5 go to a fresh child',
'reads only `sections/review-army.md`, `sections/adversarial.md` and the record']) expect(p3).toContain(s);
expect(read('ship/sections/budget-handoff.md')).toContain('--from 9 --to 11.5');
});
});
describe('STOP anchors', () => {
test('every STOP row is rendered and its anchor names a heading that exists', () => {
for (const s of SHIP_DRIVER_STOPS) {
+39 -9
View File
@@ -3,11 +3,13 @@
* and #36 the Steps 7-11.5 child booted at 1.29M by reading the whole ship
* skill, preamble.md and docs/ship-budget.md before its first step; the brief
* now names ship/SKILL.md's step list, the segment's sections and
* budget-handoff.md, and nothing else.
* budget-handoff.md, and nothing else. Variant E (replay 5): Steps 7-8 stay
* with the parent, so the review child (Steps 9-11.5) reads review-army.md and
* adversarial.md, and the record the driver keeps carries the six headings.
*/
import { afterEach, describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import { segmentSections } from '../lib/ship-handoff';
import { RECORD_HEADINGS, segmentSections } from '../lib/ship-handoff';
import { budgetHarness, type Harness } from './helpers/ship-budget-harness';
const ROOT = `${import.meta.dir}/..`;
@@ -26,11 +28,11 @@ function briefFor(from: string, to: string): string {
const readSection = (text: string) => text.slice(text.indexOf('## Read'), text.indexOf('\n## ', text.indexOf('## Read') + 1));
describe('the handoff brief names only the segment\'s sections', () => {
test('Steps 7-11.5: the step list plus test-coverage, plan-completion, review-army and adversarial', () => {
const read = readSection(briefFor('7', '11.5'));
test('Steps 9-11.5: the step list plus review-army and adversarial only', () => {
const read = readSection(briefFor('9', '11.5'));
expect(read).toContain("ship/SKILL.md's step list");
for (const s of ['test-coverage.md', 'plan-completion.md', 'review-army.md', 'adversarial.md', 'budget-handoff.md']) expect(read).toContain(s);
for (const s of ['changelog.md', 'documentation.md', 'pr-body.md', 'capy-pr.md', 'tests.md']) expect(read).not.toContain(s);
for (const s of ['review-army.md', 'adversarial.md', 'budget-handoff.md']) expect(read).toContain(s);
for (const s of ['test-coverage.md', 'plan-completion.md', 'changelog.md', 'documentation.md', 'pr-body.md', 'capy-pr.md', 'tests.md']) expect(read).not.toContain(s);
expect(read).toMatch(/Never read ship\/sections\/preamble\.md in full or docs\/ship-budget\.md/);
});
@@ -54,14 +56,15 @@ describe('the handoff brief names only the segment\'s sections', () => {
const lineOf = (file: string, heading: string) => fs.readFileSync(`${ROOT}/${file}`, 'utf8').split('\n').findIndex(l => l.startsWith(heading)) + 1;
const inside = (rs: [number, number][], n: number) => n > 0 && rs.some(([a, b]) => a <= n && n <= b);
const total = (file: string) => fs.readFileSync(`${ROOT}/${file}`, 'utf8').replace(/\n$/, '').split('\n').length;
const seg = briefFor('7', '11.5');
const seg = briefFor('9', '11.5');
expect(seg).toMatch(/Read only the `READ:` ranges below/);
const r = ranges(seg);
const skill = r['ship/SKILL.md']!;
expect(skill[0]![0]).toBe(lineOf('ship/SKILL.md', '# Ship:'));
for (const h of ['## Step 11.5:', '## Important Rules', '### Ship control flow']) expect(inside(skill, lineOf('ship/SKILL.md', h)), h).toBe(true);
for (const h of ['## Preamble (run first)', '## Section index', '## Step 3:', '## Step 12:']) expect(inside(skill, lineOf('ship/SKILL.md', h)), h).toBe(false);
for (const s of ['review-army', 'test-coverage', 'plan-completion', 'adversarial', 'budget-handoff']) expect(r[`ship/sections/${s}.md`]).toEqual([[1, total(`ship/sections/${s}.md`)]]);
for (const h of ['## Preamble (run first)', '## Section index', '## Step 3:', '## Step 7:', '## Step 8:', '## Step 12:']) expect(inside(skill, lineOf('ship/SKILL.md', h)), h).toBe(false);
for (const s of ['review-army', 'adversarial', 'budget-handoff']) expect(r[`ship/sections/${s}.md`]).toEqual([[1, total(`ship/sections/${s}.md`)]]);
for (const s of ['test-coverage', 'plan-completion']) expect(r[`ship/sections/${s}.md`]).toBeUndefined();
expect(Object.keys(r).filter(k => k.includes('audit-') || k.includes('changelog'))).toEqual([]);
const late = ranges(briefFor('12', '22'))['ship/SKILL.md']!;
for (const h of ['## Step 12:', '## Step 14.5:', '## Step 17:', '## Step 22:', '## Important Rules']) expect(inside(late, lineOf('ship/SKILL.md', h)), h).toBe(true);
@@ -95,4 +98,31 @@ describe('the handoff brief names only the segment\'s sections', () => {
const ids = new Set(manifest.sections.map((s: any) => s.id));
for (const id of segmentSections('1', '22')) expect(ids.has(id)).toBe(true);
});
// Value: protects=the record ship/DRIVER.md tells the parent to keep is one gstack-ship-budget handoff accepts: the driver names each of the six headings with the phase that writes it, and a record holding them hands Steps 9-11.5 off while one missing a heading is refused by name; fails_when=the driver drops or renames a heading the handoff requires (GSTA-167 replay 5: "record ... lacks headings: Release, Decisions, Reviews, Checks, Documentation, Next steps"), or the refusal stops naming the missing heading;
// why_new=the harness record above is hand-written, never the driver's; seam=none; no_claim=that the parent writes each heading on time
test('the driver names each record heading with its phase, and a record built from it hands Steps 9-11.5 off', () => {
const driver = fs.readFileSync(`${ROOT}/ship/DRIVER.md`, 'utf8');
const named = [...driver.matchAll(/\*\*([A-Z][a-z]+(?: steps)?)\*\*\s+\((?:Phases? [\d-]+|Step [\d.]+|any\s+phase|every phase)/g)].map(m => m[1]);
expect(new Set(named)).toEqual(new Set(RECORD_HEADINGS));
expect(driver).toContain('write its six headings at Phase 1');
const h = fresh();
const record = (headings: readonly string[]) => {
const file = h.record(`driver-${headings.length}.md`);
fs.writeFileSync(file, ['# /ship invocation record', '', ...headings.flatMap(x => [`## ${x}`, x === 'Checks' ? '- Coverage: 89% value-weighted; Scope Check: CLEAN' : '', ''])].join('\n'));
return file;
};
const handoff = (file: string) => {
const s = h.sb(['start', '--record', file, '--token-cap', '400M', '--reading', '98000']);
expect(s.code, s.out + s.err).toBe(0);
return h.sb(['handoff', '--run', h.field(s.out, 'BUDGET_RUN', 'id'), '--from', '9', '--to', '11.5', '--base', 'origin/main', '--reading', '1000000']);
};
const ok = handoff(record(RECORD_HEADINGS));
expect(ok.code, ok.out + ok.err).toBe(0);
const read = readSection(fs.readFileSync(ok.out.match(/^HANDOFF: (\S+) /m)![1]!, 'utf8'));
expect(read).toContain('review-army.md, adversarial.md, budget-handoff.md');
const refused = handoff(record(RECORD_HEADINGS.filter(x => x !== 'Next steps')));
expect(refused.code).toBe(2);
expect(refused.err).toContain('lacks headings: Next steps');
});
});
+2 -2
View File
@@ -1,4 +1,4 @@
{
"bytes": 841,
"why": "GSTA-167 turns: Steps 1-3, 5, 12 and 15-17 name gstack-ship-preflight and gstack-ship-release; own-jam meter and dispatch-pair metering in budget-handoff"
"bytes": 1203,
"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)"
}