mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-01 06:24:41 +01:00
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>
This commit is contained in:
1 parent
8915643197
commit
ba42df715a
5 files changed
+23
-6
No files matched your search
+1
-1
@@ -5,7 +5,7 @@
|
||||
**/ship takes a token cap and stops cleanly at it, and long runs hand their heavy segments to fresh-context children.** On gstack-evals#31 a /ship spent 54M tokens against a 25M cap because the cap was only a number in a brief; #26, whose agent read the counter after every step, stopped at 39.40M of 40M. `/ship --token-cap 25M` (or `GSTACK_SHIP_TOKEN_CAP`) now admits each step and dispatch batch only when it fits, and a run started from a long-lived context, or one whose per-call charge passes 150k at Step 7 or Step 12, runs that segment in a fresh child.
|
||||
|
||||
### Added
|
||||
- **`gstack-ship-budget`** (`lib/ship-budget.ts`, `lib/ship-budget-cli.ts`, `lib/ship-handoff.ts`): an admission cap on a per-run token ledger under `<state root>/ship-runs/<run>/` (its own schema; only `withFileLock` is shared with `lib/spend-ledger.ts`). `start`, `check` (the `BUDGET:` line, `verdict=continue|delegate|stop`, a `RESERVATION:` id per admitted step or batch), `settle`/`release`, `handoff`/`launch`/`verify-handoff`/`return`/`reconcile`, `resume` (cap raises need `--approved-by` and the owner's words; `--rebuild-from-record`; restore from a private Drive checkpoint) and `close`. Exit 0 continue/delegate, 3 stop, 2 usage. Meters are versioned contracts: the Capy root counter (`--reading`), Claude Code journal usage (`lib/claude-usage.ts`, bound by `--session` or an echoed nonce) or `--meter estimate`.
|
||||
- **`gstack-ship-budget`** (`lib/ship-budget.ts`, `lib/ship-budget-cli.ts`, `lib/ship-handoff.ts`): an admission cap on a per-run token ledger under `<state root>/ship-runs/<run>/` (its own schema; only `withFileLock` is shared with `lib/spend-ledger.ts`). `start`, `check` (the `BUDGET:` line, `verdict=continue|delegate|stop`, a `RESERVATION:` id per admitted step or batch), `settle`/`release`, `handoff`/`launch`/`verify-handoff`/`return`/`reconcile`, `resume` (cap raises need `--approved-by` and the owner's words; `--rebuild-from-record`; restore from a private Drive checkpoint) and `close`. Exit 0 continue/delegate, 3 stop, 2 usage. Each meter is a versioned contract, and there are three: the Capy root counter (`--reading`), Claude Code journal usage (`lib/claude-usage.ts`, bound by `--session` or an echoed nonce) and `--meter estimate`.
|
||||
- **`gstack-review-log --start ... --reservation <id>`**: under a cap, a dispatch without an unspent, one-shot reservation refuses before launch (`BUDGET_CHECK_SKIPPED`), and `--finish` settles it. Without a cap nothing changes.
|
||||
- **/ship**: the skeleton's Budget paragraph (`--token-cap`, `--resume <record>`, `--continue <handoff>`) and a new `budget-handoff` section with the meter recipes, reservations, stop and resume, and the delegate flow per host.
|
||||
- **Result codes** `BUDGET_CAP_REACHED`, `BUDGET_READING_MISSING`, `BUDGET_READING_REJECTED`, `BUDGET_METER_UNAVAILABLE`, `BUDGET_LEDGER_INVALID`, `BUDGET_CHECK_SKIPPED`, `HANDOFF_PIN_MOVED` with docs/troubleshooting.md anchors; `lib/ship-budget-calibration.json` seeded from #19, #26 and #31.
|
||||
|
||||
@@ -21,7 +21,7 @@ export const BUDGET_RESULT_CODES = {
|
||||
},
|
||||
BUDGET_READING_REJECTED: {
|
||||
anchor: 'budget-reading-rejected',
|
||||
summary: 'the reading does not fit the run\'s meter contract: it is lower than the last reading of the same epoch, or it was passed to a meter that reads its own source',
|
||||
summary: 'the reading does not fit the run\'s meter contract: it is lower than the last reading of the same epoch, it was passed to a meter that reads its own source, or --prior-reading is above --reading',
|
||||
fix: 'pass the counter the run started with (the BUDGET line names meter, unit and epoch); a different meter starts a new epoch through gstack-ship-budget resume --meter <m>',
|
||||
},
|
||||
BUDGET_METER_UNAVAILABLE: {
|
||||
|
||||
+5
-3
@@ -303,18 +303,20 @@ export function pointMarker(marker: string | undefined, l: Ledger): void {
|
||||
function activeLedger(marker: string): Ledger | undefined {
|
||||
let pointer: { ledger?: string };
|
||||
try { pointer = JSON.parse(fs.readFileSync(marker, 'utf8')); } catch (e: any) { if (e.code === 'ENOENT') return undefined; throw e; }
|
||||
const l = pointer.ledger ? readLedger(pointer.ledger) : undefined;
|
||||
const l = pointer.ledger && fs.existsSync(pointer.ledger) ? readLedger(pointer.ledger) : undefined;
|
||||
return l && l.cap !== null && !l.closed_at ? l : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* gstack-review-log --start under a cap: the dispatch needs an unspent, one-shot
|
||||
* dispatch reservation of the active run, consumed atomically with the start
|
||||
* capture. No marker (no cap) runs capture() exactly as before.
|
||||
* capture. No marker (no cap), a marker whose ledger is gone, or a stopped run
|
||||
* started without --reservation (a later uncapped review on the branch) runs
|
||||
* capture() exactly as before.
|
||||
*/
|
||||
export function admitReviewStart(marker: string, reservation: string | undefined, capture: () => string, env: NodeJS.ProcessEnv = process.env): string {
|
||||
const active = activeLedger(marker);
|
||||
if (!active) return capture();
|
||||
if (!active || (active.stopped && !reservation)) return capture();
|
||||
return mutateLedger(active.file, env, l => {
|
||||
const fix = `run gstack-ship-budget check --run ${l.run} --step <s> --next dispatch:<roles> and pass --reservation <id>`;
|
||||
if (l.stopped) throw new BudgetError('BUDGET_CAP_REACHED', resultLine('gstack-review-log', 'BUDGET_CAP_REACHED', `run ${l.run} stopped at Step ${l.stopped.step}: ${l.stopped.reason}`), 1);
|
||||
|
||||
+1
-1
@@ -203,7 +203,7 @@ export function cmdReconcile(argv: string[], ctx: Ctx): Outcome {
|
||||
const r = l.reservations.find(x => x.id === h.reservation);
|
||||
if (r && (r.state === 'open' || r.state === 'consumed')) { r.state = 'settled'; r.settled_at = new Date().toISOString(); }
|
||||
if (l.meter.contract.meter === 'estimate') l.estimate_used += childUsed;
|
||||
h.status = ret?.status ?? 'interrupted';
|
||||
h.status = ret && ret.to !== h.to && ret.status === 'complete' ? 'incomplete' : ret?.status ?? 'interrupted';
|
||||
h.child_used = childUsed;
|
||||
h.reconciled_at = new Date().toISOString();
|
||||
pointMarker(l.marker, l);
|
||||
|
||||
@@ -116,4 +116,19 @@ describe('gstack-review-log --start under a token cap', () => {
|
||||
expect(r.code).toBe(1);
|
||||
expect(r.err).toContain('(BUDGET_CAP_REACHED)');
|
||||
});
|
||||
|
||||
test('a stopped run or a lost ledger does not block a later uncapped review on the branch', () => {
|
||||
const h = fresh();
|
||||
const run = capped(h, '20M');
|
||||
expect(h.sb(['check', '--run', run.run, '--step', '9', '--next', 'dispatch:specialist,red-team', '--reading', '9000000']).code).toBe(3);
|
||||
const afterStop = h.reviewLog(['--start', 'review']);
|
||||
expect(afterStop.code).toBe(0);
|
||||
expect(afterStop.out.trim()).toMatch(UUID);
|
||||
const h2 = fresh();
|
||||
const lost = capped(h2);
|
||||
fs.unlinkSync(lost.ledger);
|
||||
const afterLoss = h2.reviewLog(['--start', 'review']);
|
||||
expect(afterLoss.code).toBe(0);
|
||||
expect(afterLoss.out.trim()).toMatch(UUID);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user