Files
gstack/test/ship-budget-batch.test.ts
Garry Tanandcapy-ai[bot] 21277291c8 v1.91.345.0 feat(ship): compact driver plus preflight/release bins over the restored reviewers (GSTA-167) (#3648)
* feat(ship): GSTA-167 turn levers on the restored reviewers: ship preflight/release bins, own-jam meter, dispatch-pair metering, ship/DRIVER.md

Ports the non-reviewer parts of #3633 (gsta-167-pr34) and #3637 (gsta-167-pr5 @2914400)
onto main ae3d58c (v1.91.331.0). Reviewer briefs, checklists, the fix-check prompt and
the depth rules are byte-identical to main and now pinned in
test/review-packets-removed.test.ts.

Left out: gstack-review-packet and its libs, parallel/batched fanout, --like, collect,
the audit prompt split, the docs-audit packet, review-army/adversarial/outside-voice
edits, the gate-scan summary-only block and the review-depth scope-error change.
Packet-bytes reader pricing is dropped from gstack-ship-budget; handoff READ ranges
send each section whole; the driver runs the audit, review, adversarial and
documentation sections as written.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(context-budget): raise the capy/skill/gstack eager ceiling to its captured value after the main merge (own-jam meter, driver and reviewer-batch prose, GSTA-167)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(ship-driver): name ship/DRIVER.md.tmpl so the pregate maps the template to its test lane (GSTA-167)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(ship-driver): keep the skill generator off lib/ship-budget so a minimal setup checkout still renders (GSTA-167)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(windows): keep test/ship-release.test.ts off the Windows subset; it launches the shebang bin and sh stubs directly (GSTA-167)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* 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>

* feat(ship): variant G orchestration: one call per driver phase (9 for a clean pass, at most 12), gstack-ship-preflight --audit for single reads and deterministic audits, an observe-mode ledger with note and credits, 240 s wait pings, and small-model audit readers (GSTA-167)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(ship-budget): a Capy handoff child needs no jam id: verify-handoff with no reading makes it parent-metered (METER: parent), it never delegates on an unknown charge, and reconcile charges the parent's counter delta (GSTA-167 replay 7e)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(ship): a poll settles a reader only at done, failed or a final summary, never a fresh idle; return settles the child's open step reservation (GSTA-167 replay 7e)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(ship-budget): return releases an open step reservation under the estimate meter instead of booking its projection; measured meters still settle it (GSTA-167 replay 7e)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(review): a clean fix-check after below-floor fixes is done; Step 11 and fix-check below-floor findings are notes at pass 1

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(ship): verbatim reviewer briefs and loss-free collection (gstack-review-dispatch render/sent/collect/judge/verify)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes

- verify requires each output's brief rendered and sent verbatim, reruns judge
  against the recorded judged.jsonl hash, and flags outputs collected after judge
- judge drops self-folds, fold cycles and chains
- prose findings parse a leading label or a label heading, not any severity word
- releasePaths: manifests count as release metadata only for version-only edits,
  lockfiles never; TODOS.md stays out of lane-evidence exemptions
- push stops when the sealed-output guard did not run
- ship-audit reads the test tree once instead of one git grep per file
- drop the uncalled --delegate measured mode; doc/help step-number fixes

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes (pass 2)

- collect keeps a finding wherever its label stands (headings, table rows,
  mid-line, Informational and upper-case labels); only top-level list items
  inherit a label heading, and any heading or bold-only line ends it
- judge treats a merged file missing any collected id as a drop; verify flags
  merged_ids_missing and a sent reader never collected (NOT_COLLECTED)
- finish computes release paths after --build, so a build that rewrites
  package.json is a build-input change

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(review-dispatch): verify per render, voided superseded mismatches

- verify treats each render as one dispatch of its reader, so a two-pass
  ledger compares pass-1 sends with pass-1 briefs (later renders print as
  <reader>@2); outputs bind to the latest send of their own render
- a mismatched send re-sent byte-exact before anything was collected is void
  and counted (voided=N on the verdict line); one that produced an output
  stays MISMATCH
- an indented bold sub-line no longer ends a label section (fix-check)
- docs: a failed reader is collected from its failure text

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(review-dispatch): only the newest render of a path checks its file; absolute ledger paths

- a later render into the same briefs directory overwrites the earlier brief by
  design, so verify checks a brief file only for the newest render of that path
- collect and judge record absolute output, merged and judged paths, so verify
  reads them from any directory
- docs: a failed reader's failure text is parsed like any output

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore: changelog and TODOS

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(ship-release): the changelog_fragments fragment is release metadata

Fix-check 4 (CRITICAL) after the main merge: releasePaths omitted
<changelog_fragments.dir>/<branch-slug>.md, so finish stopped on the
fragment Step 13 writes under gstack's policy, or routed it to re-review.
version now names the fragment in its Step 13 NEXT line instead of
[Unreleased], DRIVER.md Step 13 writes and lints where NEXT says, and the
preflight POLICY line says Steps 12-16.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore: changelog and TODOS

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>
2026-10-31 01:09:31 +00:00

94 lines
5.7 KiB
TypeScript

/**
* GSTA-167 (gstack-evals #35, gate class): full-depth Step 9 launched 7 Opus
* readers in parallel at about 0.76M each, and the shared counter went 13.2M ->
* 18.5M before the parent's poll saw it. A dispatch batch now prices each reader
* at max(its projection, the run's measured mean for that role), admits the
* largest prefix that fits near the cap (ADMIT: k of n), and keeps the rest as
* remaining until a later check requests them; close flags readers never re-checked.
*/
import { afterEach, describe, expect, test } from 'bun:test';
import { loadCalibration, unitConstants } from '../lib/ship-budget';
import { budgetHarness, type Harness } from './helpers/ship-budget-harness';
const cal = loadCalibration();
const K = unitConstants(cal, 'capy-tokens');
const harnesses: Harness[] = [];
const fresh = () => { const h = budgetHarness(); harnesses.push(h); return h; };
afterEach(() => { while (harnesses.length) harnesses.pop()!.cleanup(); });
const READERS = ['specialist', 'specialist', 'specialist', 'specialist', 'specialist', 'red-team', 'adversarial-native'];
const AT_35 = 13_200_000;
/** Room for exactly three readers at the prior above the #35 reading, with 1M to spare. */
const CAP_35 = AT_35 + 3 * K.per_dispatch + K.stop_overhead + 1_000_000;
function start(h: Harness, cap: number) {
const r = h.sb(['start', '--record', h.record(), '--token-cap', String(cap), '--reading', '0', '--reserve', '0']);
expect(r.code, r.out + r.err).toBe(0);
return { run: h.field(r.out, 'BUDGET_RUN', 'id'), ledger: h.field(r.out, 'BUDGET_RUN', 'ledger') };
}
const batch = (h: Harness, run: string, roles: string[], reading: number, extra: string[] = []) =>
h.sb(['check', '--run', run, '--step', '9', '--next', 'dispatch:' + roles.join(','), '--reading', String(reading), ...extra]);
describe('parallel dispatch admission near the cap', () => {
test('#35 replay: a 7-reader batch near the cap admits the largest prefix and names the rest', () => {
const h = fresh();
const s = start(h, CAP_35);
const c = batch(h, s.run, READERS, AT_35);
expect(c.code, c.out + c.err).toBe(0);
expect(c.out).toMatch(/^ADMIT: 3 of 7 \(remaining=specialist,specialist,red-team,adversarial-native\)/m);
expect(h.field(c.out, 'BUDGET', 'verdict')).toBe('continue');
expect(h.field(c.out, 'RESERVATION', 'next')).toBe('dispatch:specialist,specialist,specialist');
expect(Number(h.field(c.out, 'RESERVATION', 'tokens'))).toBe(3 * K.per_dispatch);
const r = h.ledger(s.ledger).reservations.find((x: any) => x.kind === 'dispatch');
expect(r).toMatchObject({ roles: ['specialist', 'specialist', 'specialist'], remaining: ['specialist', 'specialist', 'red-team', 'adversarial-native'] });
});
test('a role\'s measured mean overrides a lower projection', () => {
const h = fresh();
const s = start(h, 200_000_000);
let id = h.field(batch(h, s.run, ['red-team'], 0).out, 'RESERVATION', 'id');
let reading = 3_000_000, at = 0;
for (let i = 0; i < 4; i++, reading += 500_000) { id = h.field(batch(h, s.run, ['specialist'], reading, ['--returned', id, '--dispatched-at', String(at)]).out, 'RESERVATION', 'id'); at = reading; }
const c = batch(h, s.run, ['red-team', 'specialist'], reading, ['--returned', id, '--dispatched-at', String(at)]);
expect(c.code, c.out + c.err).toBe(0);
const f = (500_000 / K.per_dispatch) * cal.headroom;
const specialist = Math.ceil((K.per_dispatch * f) / cal.round_to) * cal.round_to;
expect(specialist).toBeLessThan(3_000_000);
expect(Number(h.field(c.out, 'BUDGET', 'projected'))).toBe(3_000_000 + specialist);
});
test('when not even the first reader fits, the batch stops as before', () => {
const h = fresh();
const s = start(h, CAP_35);
const c = batch(h, s.run, READERS, CAP_35 - K.stop_overhead - K.per_dispatch + 1);
expect(c.code).toBe(3);
expect(c.out).toContain('STOP: reason=budget_exhausted');
expect(c.out).not.toContain('ADMIT:');
expect(Number(h.field(c.out, 'BUDGET', 'projected'))).toBe(READERS.length * K.per_dispatch);
expect(h.ledger(s.ledger).reservations.filter((x: any) => x.kind === 'dispatch')).toHaveLength(0);
});
test('the remainder re-check admits after the first readers settle; close flags readers never re-checked', () => {
const h = fresh();
const s = start(h, CAP_35);
const first = h.field(batch(h, s.run, READERS, AT_35).out, 'RESERVATION', 'id');
expect(h.sb(['settle', '--run', s.run, '--reservation', first, '--dispatched-at', String(AT_35), '--reading', String(AT_35 + 3 * 760_000)]).code).toBe(0);
const rest = batch(h, s.run, READERS.slice(3), AT_35 + 3 * 760_000);
expect(rest.code, rest.out + rest.err).toBe(0);
expect(rest.out).not.toContain('ADMIT:');
expect(h.field(rest.out, 'RESERVATION', 'next')).toBe('dispatch:' + READERS.slice(3).join(','));
expect(h.ledger(s.ledger).reservations.find((x: any) => x.id === first).remaining).toEqual([]);
expect(h.sb(['close', '--run', s.run, '--reading', String(AT_35 + 4_000_000)]).code).toBe(0);
const d = start(h, CAP_35);
expect(batch(h, d.run, READERS, AT_35).out).toContain('ADMIT: 3 of 7');
const close = h.sb(['close', '--run', d.run, '--reading', String(AT_35 + 3_000_000)]);
expect(close.code).toBe(3);
expect(close.out).toMatch(/^DROPPED: step=9 reservation=\S+ roles=specialist,specialist,red-team,adversarial-native/m);
expect(close.out.trim().split('\n').at(-1)).toMatch(/^GSTACK_RESULT: skill=ship status=incomplete run=/);
const accepted = h.sb(['close', '--run', d.run, '--reading', String(AT_35 + 3_000_000), '--approved-by', 'Garry', '--words', 'three readers are enough']);
expect(accepted.code).toBe(0);
expect(accepted.out).toContain('ACCEPTED: 0 unreserved dispatches and 1 unfinished batches by Garry');
});
});