* ship: token cap with real meters and fresh-context delegation (GSTA-152 PR A) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes (stopped or lost capped run no longer blocks uncapped review starts; short child return reconciles incomplete) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: carry estimate spend across a meter epoch change; calibration docs match their evidence Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: load the committed calibration file by its repo path so pregate maps it to a lane Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * ship: fix fence placeholders and register claude-usage fixture (GSTA-152 PR A) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: run ship-budget and review-log bins through their interpreters so Windows can launch them Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
8.6 KiB
/ship token budget and fresh-context handoffs
/ship --token-cap 25M (or GSTACK_SHIP_TOKEN_CAP=25M) bounds what one /ship run spends, and
long runs hand their heavy segments to fresh-context children. The helper is
bin/gstack-ship-budget; the skill's procedure is ship/sections/budget-handoff.md.
There is no default cap: without one, /ship meters usage when it can and prints it, and
nothing else changes.
What the cap guarantees
It is an admission cap, not a hard ceiling. Before every step and every dispatch batch,
check admits the work only when
used + open reservations + unknown charges + projection + tail reserve + stop overhead <= cap
Exactly at the cap is admitted; one token over is not. A parallel review batch is reserved as
one sum before any of it starts. Admitted work can still cost more than its projection, and
gstack cannot stop a host mid-turn from bash, so a run can finish above the cap by the
misprojection of work already admitted. The tail reserve (default 2M, --reserve <n>) keeps
room for the receipt steps, and the stop overhead (calibrated) keeps room to stop cleanly.
Quickstart
Capy. Read the root counter in one execute program that returns only that number:
return (await tools.thread.get({ thread_id: "this" })).cost.tokens;
then
gstack-ship-budget start --record <record> --token-cap 25M --reading <n>
gstack-ship-budget check --run <id> --step 6 --next 7 --reading <n> [--prior-reading <n from your previous call>]
The counter rolls up every subagent of the thread, outside voices included; Capy exposes no per-subagent counter, so other work in the same thread counts too (it only stops earlier).
Claude Code. gstack-ship-budget start --record <record> --token-cap 25M --session <id>. Without the
session id, start --meter claude prints a nonce: echo it once, then rerun with --nonce <value>.
The helper reads the session journal, its subagent journals and the journals of headless
children it launched; you never pass --reading.
Anywhere else. --meter estimate projects with the calibration and prints meter=estimate.
With a cap set and no readable meter, start refuses
(BUDGET_METER_UNAVAILABLE).
The BUDGET line
BUDGET: used=<n> reserved=<n> unknown=<n> admission_cap=<n|none> tail=<n> unit=<u> meter=<m> epoch=<n> call=<n|unknown> next=<id> projected=<n> verdict=continue|delegate|stop
| Field | Meaning |
|---|---|
used |
Metered usage since start (all epochs), or settled estimates under meter=estimate |
reserved |
Open reservations: admitted steps, started dispatches, and each open child's sub-cap counted as max(sub_cap, child_used), never both |
unknown |
Charges the meter cannot see (an external-CLI outside voice on Claude Code), counted at their reservation |
admission_cap |
The cap, or none |
tail |
Tail reserve plus stop overhead, held back from every admission |
unit, meter, epoch |
The meter contract: capy-tokens (root counter), claude-usage-tokens (journal usage), estimate-tokens; a meter change opens a new epoch |
call |
The per-call charge: two Capy readings in consecutive calls, or the last Claude Code call's usage |
next, projected |
What the check was for and its calibrated projection |
verdict |
continue (exit 0, prints RESERVATION: under a cap), delegate (exit 0), stop (exit 3) |
Any exit other than 0 means the next step does not start. Exit 2 is a usage or validation error on stderr with the code, the value and the repair. GSTACK_RESULT appears only on a stop, an incomplete close and a child's return; /ship's real terminal line stays Step 22's.
Dispatches
Under a cap, gstack-review-log --start refuses a dispatch without an unspent, one-shot
reservation (BUDGET_CHECK_SKIPPED):
gstack-ship-budget check --run <id> --step 9 --next dispatch:specialist,red-team --reading <n>
gstack-review-log --start review --reservation <id>
Roles are review-depth's: specialist, red-team, adversarial-native, outside-voice,
fix-check. --finish <token> settles the reservation at the dispatch's completion receipt.
close audits after the fact: each review-log row of the run with no reservation gets one
BUDGET_CHECK_SKIPPED line and the run ends status=incomplete for the owner to accept
(close --approved-by <who> --words "<their words>"); a reservation is never created after the fact.
Delegation
At start, a per-call charge of 100k or more delegates the whole /ship. Before Step 7 and before Step 12, 150k or more (or an unknown charge) delegates the next segment (7–11.5, 12–22):
gstack-ship-budget handoff --run <id> --from 12 --to 22 --base origin/<base> --reading <n>writes a brief (mode 0600, under the run directory) with the pins (HEAD, merge base, working tree, reviewed tree), the record with its six headings and start tokens, the decisions in scope with the owner's words, the decision trail, the budget and the return contract. It reserves the child's sub-cap: min(cap − used − reserved − tail, the segment estimate), or--sub-cap.- The child is a Capy subagent on the shared machine, or on Claude Code
gstack-ship-budget launch --in <brief>: a backgroundclaude -p(resolved through lib/claude-bin.ts) with explicit permission flags and the return file named. - The child runs
verify-handofffirst; a moved pin refuses (HANDOFF_PIN_MOVED). It admits against its sub-cap with child_used = reading − reading at handoff, is the only writer of the record, and never delegates again: a delegate rule stops it. It ends withreturn(at most 40 lines). gstack-ship-budget reconcile --run <id> --return <file> --reading <n>; idempotent per handoff hash, so a duplicate return charges nothing twice. A refused, incomplete or interrupted child exits 3: regenerate the handoff or stop.
--delegate never turns delegate into stop for hosts without children.
Stop and resume
A stop prints
STOP: reason=budget_exhausted spent=<n> reserved=<n> unknown=<n> admission_cap=<n> next=<id> projected=<n>
RECOVER: /ship --resume <record> --token-cap <used + reserved + unknown + projected tail>
GSTACK_RESULT: skill=ship status=incomplete run=<dir>
Raising the cap is the owner's call; the resume journals who raised it and their words:
gstack-ship-budget resume --record <record> --token-cap <n> --approved-by Garry --words "raise it to finish" --reading <n>
Every command takes --run or --record; runs are never found by branch or directory, and a
/ship without --resume or --continue always starts a new run. The ledger and record are
mirrored to a private Drive checkpoint (GSTACK_SHIP_BUDGET_CHECKPOINT_DIR, else the
project-user Drive folder on Capy) bound to the run id and their hashes, so after a machine
change resume --run <id> restores both. A corrupt ledger
(BUDGET_LEDGER_INVALID) is rebuilt from the
record's block with --rebuild-from-record; open reservations are then charged as unknown.
Calibration
lib/ship-budget-calibration.json holds per-meter constants (per call, per dispatch, stop
overhead, calls and dispatches per step) fitted from metered runs: each constant is the
largest observation times 1.5, rounded up to 10,000 tokens. The leave-one-out test
(test/ship-budget-calibration.test.ts) covers the per-call constant only, the one observed
across three runs; per-dispatch has two observations and stop overhead one. Only the Capy
meter has observations; the Claude Code and estimate units are seeded from it until a
Claude-metered /ship completes.
Token cap and USD spend cap
A lane's --spend-cap (USD) bounds paid gates and evals through lib/spend-ledger.ts; --token-cap
bounds the /ship run's model tokens through its own ledger. They are independent: set both
when a lane runs paid gates inside /ship, and neither converts into the other.
Claiming savings
Savings count completed ships only. Preregister the comparison with /eval-plan (arms, bars,
stop rules) before claiming a saving from the cap or from delegation.
Result codes
BUDGET_CAP_REACHED · BUDGET_READING_MISSING · BUDGET_READING_REJECTED · BUDGET_METER_UNAVAILABLE · BUDGET_LEDGER_INVALID · BUDGET_CHECK_SKIPPED · HANDOFF_PIN_MOVED