mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-01 06:24:41 +01:00
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>
This commit is contained in:
1 parent
80e5625cfc
commit
559d749174
18 files changed
+601
-205
No files matched your search
@@ -3,10 +3,13 @@
|
||||
* gstack-ship-budget — the /ship token budget and fresh-context handoff
|
||||
* (docs/ship-budget.md). An admission cap: no step or dispatch starts unless
|
||||
* used + open reservations + unknown + its projection + the tail and stop
|
||||
* reserve fit under the cap.
|
||||
* reserve fit under the cap. --mode observe (ship/DRIVER.md) only records:
|
||||
* note at each program's end, and the cap is one hard stop. Every --reading
|
||||
* takes --credits <cost.credits> beside it.
|
||||
*
|
||||
* start --record <path> [--token-cap <n|none>] [--meter capy|claude|estimate] [--reading <n>] [--prior-reading <n>]
|
||||
* [--session <id>|--nonce <v>] [--reserve <n>] [--delegate auto|never|measured] [--calibration <file>] [--ledger <path>]
|
||||
* start --record <path> [--token-cap <n|none>] [--mode admit|observe] [--meter capy|claude|estimate] [--reading <n>] [--credits <c>]
|
||||
* [--prior-reading <n>] [--session <id>|--nonce <v>] [--reserve <n>] [--delegate auto|never|measured] [--calibration <file>] [--ledger <path>]
|
||||
* note --run|--record --step <id> [--label <text>] --reading <n> [--credits <c>] [--dispatched-at <n> [--dispatched-credits <c>] --readers <k>]
|
||||
* check --run <id>|--record <path> --step <id> --next <id|dispatch:<role>[,<role>...]> [--reading <n>] [--prior-reading <n>] [--returned <id>[,<id>...]] [--runtime-lines <n>]
|
||||
* settle --run|--record --reservation <id> [--unknown] [--reading <n>] release --run|--record --reservation <id>
|
||||
* handoff --run|--record --from <step> --to <step> --base origin/<base> [--reviewed-tree <t>] [--trail <f> --trail-run <id>]
|
||||
@@ -24,18 +27,18 @@
|
||||
* lib/ship-handoff.ts.
|
||||
*/
|
||||
import { BudgetError } from '../lib/ship-budget';
|
||||
import { cmdCheck, cmdClose, cmdResume, cmdSettle, cmdStart, type Outcome } from '../lib/ship-budget-cli';
|
||||
import { cmdCheck, cmdClose, cmdNote, cmdResume, cmdSettle, cmdStart, type Outcome } from '../lib/ship-budget-cli';
|
||||
import { cmdHandoff, cmdLaunch, cmdReconcile, cmdReturn, cmdVerifyHandoff } from '../lib/ship-handoff';
|
||||
|
||||
const [command, ...rest] = process.argv.slice(2);
|
||||
const ctx = { env: process.env, cwd: process.cwd() };
|
||||
const commands: Record<string, () => Outcome> = {
|
||||
start: () => cmdStart(rest, ctx), check: () => cmdCheck(rest, ctx), settle: () => cmdSettle(rest, ctx), release: () => cmdSettle(rest, ctx, true),
|
||||
start: () => cmdStart(rest, ctx), note: () => cmdNote(rest, ctx), check: () => cmdCheck(rest, ctx), settle: () => cmdSettle(rest, ctx), release: () => cmdSettle(rest, ctx, true),
|
||||
handoff: () => cmdHandoff(rest, ctx), launch: () => cmdLaunch(rest, ctx), 'verify-handoff': () => cmdVerifyHandoff(rest, ctx), return: () => cmdReturn(rest, ctx),
|
||||
reconcile: () => cmdReconcile(rest, ctx), resume: () => cmdResume(rest, ctx), close: () => cmdClose(rest, ctx),
|
||||
};
|
||||
if (!command || command === '--help' || command === '-h' || !commands[command]) {
|
||||
const text = (await Bun.file(import.meta.path).text()).split('\n').slice(2, 26).map(l => l.replace(/^ \* ?/, '')).join('\n');
|
||||
const text = (await Bun.file(import.meta.path).text()).split('\n').slice(2, 27).map(l => l.replace(/^ \* ?/, '')).join('\n');
|
||||
(command && command !== '--help' && command !== '-h' ? console.error : console.log)(text);
|
||||
process.exit(command && command !== '--help' && command !== '-h' ? 2 : 0);
|
||||
}
|
||||
|
||||
@@ -44,10 +44,9 @@ Run directories and scratch on Capy live under `~/.capy/work/<name>` (`--out ~/.
|
||||
|
||||
Capy checklist for `/autoplan`: drive `gstack-autoplan`, never a hand adaptation presented as a run. Relay `gstack-gate render <dir>/gate.json --all` with `rec` (every recommendation, plan changes included) as the one-word default; answer with `--by agent:<id> --owner-reply "<their words>"`. Set `GSTACK_AUTOPLAN_CHECKPOINT_DIR` to a private project-user Drive folder; after machine replacement, `gstack-autoplan resume --out <new dir> --from <that dir>` restores the newest valid checkpoint.
|
||||
|
||||
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 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`.
|
||||
Token caps on /ship: pass a brief's cap as `--token-cap <n>`. /ship reads `cost.tokens` and `cost.credits` 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 them as `--reading <n> --credits <c>`, never in a call of its own. `ship/DRIVER.md` starts `--mode observe`: each program ends with `note`, and only a note past the cap stops (`docs/ship-budget.md`).
|
||||
|
||||
/ship on Capy is about a dozen calls, each one execute program (`ship/DRIVER.md`). A review pass, Step 11 and the Step 14.5 audit each run inside one program, in order: (1) `gstack-review-plan --base <base>` with the pass's flags (its `DISPATCH:` lines); (2) `gstack-review-log --start <skill>` once per row the step logs (under ship/SKILL.md's admission mode, add `--reservation <id>` from check's `RESERVATION_ID:` line); (3) `BEFORE`, the counter's tokens and credits; (4) `tools.subagent.create`, one reader each, `machine: "shared"`, its brief copied verbatim from the section that dispatches it; audit readers (Steps 7, 8, 9.2.1, 14.5) cannot block, so they add `model: { model_id: "anthropic/claude-sonnet-5-5", reasoning_mode: "low" }` (`anthropic/claude-haiku-5-5` also fits); (5) a poll of `tools.subagent.get` with `transcript: "none"` every ~20 s for at most 240 s; (6) `gstack-ship-budget note --step <n> --reading <AFTER> --credits <c> --dispatched-at <BEFORE> --dispatched-credits <c> --readers <n>`; (7) return only each reader's final output, or what still runs. The parent never walks the dispatch steps call by call. Waits: never let 5 minutes pass between calls. During tests, pregate, readers and CI, re-issue `wait` with `duration_seconds: 240` and the operation ids: a call after a longer gap bills the whole context fresh (about 40x a cached call), and a ping costs a few percent of that. A small waiting child saves nothing: the parent's cache expires during its wait too.
|
||||
|
||||
## Plan, approve, ship
|
||||
|
||||
|
||||
+24
-2
@@ -21,6 +21,28 @@ room for the receipt steps, and the stop overhead (calibrated) keeps room to sto
|
||||
the run has measured a step, the tail reserve scales with its measured step cost (a `--reserve`
|
||||
you set stays as given); the stop overhead never scales.
|
||||
|
||||
## Observe mode (ship/DRIVER.md)
|
||||
|
||||
`start --mode observe` turns the ledger into a record. No step or dispatch is admitted, reserved or
|
||||
delegated, so a run spends no call on the meter: each program the driver runs ends with
|
||||
|
||||
gstack-ship-budget note --run <id> --step <last step> [--label <what ran>] --reading <tokens> --credits <credits> \
|
||||
[--dispatched-at <tokens before the readers> --dispatched-credits <credits before> --readers <n>]
|
||||
|
||||
which prints `NOTE: <step>[:<label>] used=<n> credits=<c> [readers=<n> readers_tokens=<n> readers_credits=<c>]`
|
||||
and the `BUDGET:` line. The cap is one hard stop: a note (or a check) whose `used + unknown` is past
|
||||
`--token-cap` prints the stop block and exits 3, and nothing else stops the run. No review-log marker
|
||||
is written, so `gstack-review-log --start` takes no `--reservation`, and `close` audits no
|
||||
unreserved dispatch. `close` prints `BUDGET_TIMELINE:`, one entry per note. The admission mode
|
||||
below is what `ship/SKILL.md` runs; it is unchanged.
|
||||
|
||||
**Credits.** Every command that takes `--reading` also takes `--credits <cost.credits>` from the
|
||||
same `thread.get`. Tokens count a cached read the same as a fresh one; credits are what the run
|
||||
costs, including the full-context re-bill after a gap over five minutes (about 40x a cached call,
|
||||
handoffs/GSTA-167/cache-probe.md). The first credits reading is the baseline; the `BUDGET:`,
|
||||
`BUDGET_FINAL:` and record-block lines print `credits=<used>` once one exists, and a reading below
|
||||
the last is refused like a token reading ([BUDGET_READING_REJECTED](troubleshooting.md#budget-reading-rejected)).
|
||||
|
||||
## Quickstart
|
||||
|
||||
**Capy.** Read your own jam's counter inside the execute program that runs the command, never as a
|
||||
@@ -165,8 +187,8 @@ line prints `call`, `call_floor` and `above_floor`, so each verdict can be check
|
||||
grandchild's.
|
||||
|
||||
`--delegate never` turns delegate into stop for a parent on a host without children; a child
|
||||
continues with a `CONTEXT_HIGH:` note instead. `--delegate measured` (what `ship/DRIVER.md` starts
|
||||
with) delegates only when a measured call is 150k or more above the floor at a segment boundary; an
|
||||
continues with a `CONTEXT_HIGH:` note instead. `--delegate measured` (what `ship/DRIVER.md` started
|
||||
with before observe mode) delegates only when a measured call is 150k or more above the floor at a segment boundary; an
|
||||
unknown charge continues there. A parent driving /ship from the driver carries no skill text a fresh
|
||||
child would shed, so a child pays its fixed cost only when the parent's own context grew.
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* ship-audit — gstack-ship-preflight --audit (GSTA-167 variant G): the parent's single
|
||||
* reads and the deterministic part of /ship's non-blocking audits in one call. It writes
|
||||
* the branch diff once to <out>/diff.patch, the commits to <out>/commits.txt and the
|
||||
* TODOS.md items that name a touched path to <out>/todos.md, maps each changed runtime
|
||||
* file to the tests that name it (Step 7), lists the plan candidates Step 8 offers, and
|
||||
* prints which audit still needs a reader. The parent reads these small files and never
|
||||
* TODOS.md or the diff whole; the audits' briefs stay their sections' own.
|
||||
*/
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { classifyPath } from './review-depth';
|
||||
import { resolveStateRoot } from './state-root';
|
||||
import { bin, gh, git, hostedOrigin, Report, short } from './ship-bin-common';
|
||||
|
||||
const DATA_PATH = /\.(json|jsonl|ndjson|ya?ml|toml|csv|tsv|svg|png|jpe?g|gif|webp|ico|pdf)$/i;
|
||||
const TEST_PATH = /(^|\/)(test|tests|__tests__|spec)\/|\.(test|spec)\.[a-z0-9]+$/i;
|
||||
const TEST_SPECS = ['test', 'tests', '__tests__', 'spec', '*.test.*', '*.spec.*'];
|
||||
const TODOS_HITS_MAX = 120;
|
||||
|
||||
const stem = (p: string) => path.basename(p).replace(/\.[^.]+$/, '');
|
||||
const readOr = (file: string) => { try { return fs.readFileSync(file, 'utf8'); } catch { return ''; } };
|
||||
|
||||
/** TODOS.md lines naming a touched path or its file stem (a stem under 4 characters only by full path), under their nearest heading. */
|
||||
export function todosHits(text: string, touched: readonly string[]): string[] {
|
||||
const keys = [...new Set(touched.flatMap(p => [p, ...(stem(p).length >= 4 ? [stem(p)] : [])]))];
|
||||
const out: string[] = [];
|
||||
let heading = '', printed = '';
|
||||
for (const line of text.split('\n')) {
|
||||
if (/^#{1,6} /.test(line)) { heading = line; continue; }
|
||||
if (!keys.some(k => line.includes(k))) continue;
|
||||
if (heading && heading !== printed) { out.push(heading); printed = heading; }
|
||||
out.push(line);
|
||||
if (out.length >= TODOS_HITS_MAX) break;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Step 8's discovery: the PR body's Plan: binding, docs/designs/ files this branch changed or that name it, then personal plan files naming it. */
|
||||
function planCandidates(cwd: string, fork: string, branch: string): string[] {
|
||||
const found: string[] = [];
|
||||
if (hostedOrigin(cwd)) {
|
||||
const bound = /^\s*Plan:\s*(\S+)/m.exec(gh(cwd, ['pr', 'view', '--json', 'body', '-q', '.body']).stdout.replace(/[\r`]/g, ''))?.[1];
|
||||
if (bound) found.push(`PLAN_BINDING: ${bound}`);
|
||||
}
|
||||
const designs = new Set([
|
||||
...git(cwd, ['diff', '--name-only', '--diff-filter=AM', fork, '--', 'docs/designs/*.md']).stdout.split('\n'),
|
||||
...(branch ? git(cwd, ['grep', '-l', '-F', '-e', branch, '--', 'docs/designs/*.md']).stdout.split('\n') : []),
|
||||
].filter(Boolean));
|
||||
for (const d of designs) found.push(`PLAN_CANDIDATE: ${d}`);
|
||||
if (!branch) return found;
|
||||
const slug = /^SLUG=(\S+)/m.exec(bin(cwd, 'gstack-slug', [], 30_000).stdout)?.[1] ?? path.basename(cwd).replace(/[^a-zA-Z0-9._-]/g, '');
|
||||
const home = process.env.HOME ?? '';
|
||||
for (const dir of [path.join(resolveStateRoot(process.env), 'projects', slug), path.join(home, '.claude/plans'), path.join(home, '.codex/plans'), path.join(cwd, '.gstack/plans')]) {
|
||||
let names: string[];
|
||||
try { names = fs.readdirSync(dir).filter(n => n.endsWith('.md')); } catch { continue; }
|
||||
for (const n of names) if (readOr(path.join(dir, n)).includes(branch)) found.push(`PLAN_CANDIDATE: ${path.join(dir, n)}`);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
export function audit(cwd: string, base: string, out: string): Report {
|
||||
const r = new Report();
|
||||
const fork = git(cwd, ['merge-base', `origin/${base}`, 'HEAD']).stdout.trim() || `origin/${base}`;
|
||||
fs.mkdirSync(out, { recursive: true, mode: 0o700 });
|
||||
const diff = git(cwd, ['diff', fork]).stdout;
|
||||
const files = git(cwd, ['diff', '--name-only', fork]).stdout.split('\n').filter(Boolean);
|
||||
const commits = git(cwd, ['log', '--oneline', `origin/${base}..HEAD`]).stdout;
|
||||
fs.writeFileSync(path.join(out, 'diff.patch'), diff, { mode: 0o600 });
|
||||
fs.writeFileSync(path.join(out, 'commits.txt'), commits, { mode: 0o600 });
|
||||
r.add(`CONTEXT: diff=${path.join(out, 'diff.patch')} bytes=${Buffer.byteLength(diff)} files=${files.length} commits=${commits.split('\n').filter(Boolean).length} log=${path.join(out, 'commits.txt')}`);
|
||||
|
||||
if (!fs.existsSync(path.join(cwd, 'TODOS.md'))) r.add('TODOS: none (no TODOS.md)');
|
||||
else {
|
||||
const text = readOr(path.join(cwd, 'TODOS.md'));
|
||||
const hits = todosHits(text, files);
|
||||
fs.writeFileSync(path.join(out, 'todos.md'), hits.join('\n') + (hits.length ? '\n' : ''), { mode: 0o600 });
|
||||
r.add(`TODOS: bytes=${Buffer.byteLength(text)} hits=${hits.filter(l => !/^#{1,6} /.test(l)).length} file=${path.join(out, 'todos.md')} (Steps 8.2 and 14 read this file, never TODOS.md whole)`);
|
||||
}
|
||||
|
||||
const tests = files.filter(p => TEST_PATH.test(p)).map(p => ({ p, text: readOr(path.join(cwd, p)) }));
|
||||
const runtime = files.filter(p => !TEST_PATH.test(p) && !DATA_PATH.test(p) && fs.existsSync(path.join(cwd, p)) && classifyPath(p, cwd) === 'code');
|
||||
const map = runtime.map(p => ({
|
||||
p,
|
||||
changed: tests.filter(t => t.p.includes(stem(p)) || t.text.includes(stem(p))).length,
|
||||
referencing: git(cwd, ['grep', '-l', '-F', '-e', stem(p), '--', ...TEST_SPECS]).stdout.split('\n').filter(Boolean).length,
|
||||
}));
|
||||
const mapFile = path.join(out, 'coverage.txt');
|
||||
fs.writeFileSync(mapFile, map.map(m => `${m.p} changed_tests=${m.changed} referencing_tests=${m.referencing}\n`).join(''), { mode: 0o600 });
|
||||
for (const m of map.slice(0, 8)) r.add(`COVERAGE_MAP: ${m.p} changed_tests=${m.changed} referencing_tests=${m.referencing}`);
|
||||
const untested = map.filter(m => m.changed === 0).map(m => m.p);
|
||||
r.add(!runtime.length ? 'Coverage: skipped (no runtime file changed; no audit reader)'
|
||||
: untested.length ? `Coverage: ${untested.length} of ${runtime.length} changed runtime files have no changed test naming them (${short(untested, 4)}); map=${mapFile}`
|
||||
: `Coverage: deterministic (each of ${runtime.length} changed runtime files has a changed test naming it; no audit reader); map=${mapFile}`);
|
||||
|
||||
const branch = git(cwd, ['branch', '--show-current']).stdout.trim().replace(/\//g, '-').replace(/[^a-zA-Z0-9._-]/g, '');
|
||||
const plans = planCandidates(cwd, fork, branch);
|
||||
for (const line of plans.slice(0, 5)) r.add(line);
|
||||
if (!plans.length) r.add('Plan completion audit: not run (no plan is bound to this branch and no docs/designs/ file matches). Fix: add "Plan: <path>" to the PR body, or run /autoplan.');
|
||||
r.add(`READER: coverage=${untested.length ? 'needed' : 'none'} plan=${plans.length ? 'needed' : 'none'}`);
|
||||
return r;
|
||||
}
|
||||
+55
-10
@@ -13,10 +13,10 @@ import * as path from 'node:path';
|
||||
import { resultLine } from './result-codes';
|
||||
import {
|
||||
BudgetError, DEFAULT_TAIL, DELEGATE_SEGMENT_CALL, DELEGATE_START_CALL, MAX_CHAIN_DEPTH, METER_CONTRACTS, SEGMENTS, SHIP_STEPS,
|
||||
aboveFloor, admits, applyReading, budgetError, budgetLine, keepsSmallRun, ledgerFileFor, loadCalibration, measureSettled, mintRunId, mutateLedger, newEpoch,
|
||||
aboveFloor, admits, applyCredits, applyReading, budgetError, creditsUsed, parseCredits, budgetLine, keepsSmallRun, ledgerFileFor, loadCalibration, measureSettled, mintRunId, mutateLedger, newEpoch,
|
||||
parseNext, parseRecordBlock, parseStep, parseTokens, pointMarker, projector, readLedger, reserveIn, restoreRun, reviewPaths,
|
||||
runDirFor, settleIn, stepProjection, tally, unitConstants, unreservedDispatches, usageError, writeLedger,
|
||||
type Ledger, type MeterKind, type Next,
|
||||
type Ledger, type MeterKind, type Next, type Note,
|
||||
} from './ship-budget';
|
||||
import { claudeConfigDir, loadMeterState, newMeterState, nonceSessions, readClaudeUsage, saveMeterState, sessionJournals } from './claude-usage';
|
||||
|
||||
@@ -51,6 +51,7 @@ const freshReading = (l: Ledger, o: Opts) => l.meter.contract.meter === 'claude'
|
||||
* reading spans several calls, so it is ignored (call unknown) with a note.
|
||||
*/
|
||||
export function readMeter(l: Ledger, o: Opts, requireReading: boolean, notes: string[] = []): number | null {
|
||||
if (str(o, 'credits') !== undefined) applyCredits(l, parseCredits(str(o, 'credits'), '--credits'));
|
||||
const reading = tokensOpt(o, 'reading');
|
||||
const prior = tokensOpt(o, 'prior-reading');
|
||||
const meter = l.meter.contract.meter;
|
||||
@@ -136,6 +137,7 @@ export function writeReturnFile(file: string, lines: string[]): void {
|
||||
* continues with a CONTEXT_HIGH note. Admission still stops anyone.
|
||||
*/
|
||||
export function decide(l: Ledger, step: string, next: Next, call: number | null, startThreshold = false, runtimeLines?: number): Outcome {
|
||||
if (l.mode === 'observe') return observed(l, step, next.kind === 'dispatch' ? `dispatch:${next.roles.join(',')}` : next.step, call);
|
||||
if (next.kind === 'dispatch') return admitBatch(l, step, next.roles, call);
|
||||
const p = projector(l);
|
||||
const nextId = next.step;
|
||||
@@ -171,6 +173,14 @@ export function decide(l: Ledger, step: string, next: Next, call: number | null,
|
||||
return { stdout: lines, exit: 0 };
|
||||
}
|
||||
|
||||
/** An observe run's verdict: continue, or the hard stop once used + unknown pass the cap. Nothing is reserved. */
|
||||
function observed(l: Ledger, step: string, next: string, call: number | null, line?: string): Outcome {
|
||||
const t = tally(l);
|
||||
const head = line ? [line] : [];
|
||||
if (l.cap !== null && t.used + t.unknown > l.cap) return { stdout: [...head, ...stopLines(l, { call, next, step, projected: 0, reason: 'budget_exhausted' })], exit: 3 };
|
||||
return { stdout: [...head, budgetLine(l, { call, next, projected: 0, verdict: 'continue' })], exit: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* A dispatch batch: each reader is priced by projector().reader and the batch is
|
||||
* reserved as one sum. When it
|
||||
@@ -225,7 +235,9 @@ function pickMeter(o: Opts): MeterKind | undefined {
|
||||
}
|
||||
|
||||
export function cmdStart(argv: string[], ctx: Ctx): Outcome {
|
||||
const o = parseArgs(argv, ['token-cap', 'record', 'meter', 'reading', 'prior-reading', 'session', 'nonce', 'reserve', 'delegate', 'calibration', 'ledger', 'call-floor']);
|
||||
const o = parseArgs(argv, ['token-cap', 'record', 'meter', 'reading', 'credits', 'prior-reading', 'session', 'nonce', 'reserve', 'delegate', 'calibration', 'ledger', 'call-floor', 'mode']);
|
||||
const mode = str(o, 'mode') ?? 'admit';
|
||||
if (mode !== 'admit' && mode !== 'observe') throw usageError(`--mode '${mode}' is not admit or observe`);
|
||||
const record = str(o, 'record');
|
||||
if (!record) throw usageError('start needs --record <invocation record path>');
|
||||
const rawCap = str(o, 'token-cap') ?? ctx.env.GSTACK_SHIP_TOKEN_CAP;
|
||||
@@ -256,7 +268,7 @@ export function cmdStart(argv: string[], ctx: Ctx): Outcome {
|
||||
delegate, role: 'parent', meter: { contract, epoch: 1, baseline: 0, last: null, ...(sessions ? { sessions, config_dir: claudeConfigDir(ctx.env) } : {}) },
|
||||
epochs: [], carried_used: 0, estimate_used: 0, unknown: 0, next_id: 1, reservations: [], handoffs: [], cap_raises: [],
|
||||
marker: paths.marker, review_log: paths.reviewLog, ...(str(o, 'calibration') ? { calibration: path.resolve(str(o, 'calibration')!) } : {}),
|
||||
...(callFloor !== undefined ? { call_floor: callFloor } : {}),
|
||||
...(callFloor !== undefined ? { call_floor: callFloor } : {}), ...(mode === 'observe' ? { mode } : {}),
|
||||
};
|
||||
if (meter === 'capy' && tokensOpt(o, 'reading') === undefined) throw budgetError('BUDGET_READING_MISSING', 'start --meter capy');
|
||||
if (meter === 'capy') l.meter.baseline = tokensOpt(o, 'reading')!;
|
||||
@@ -274,7 +286,7 @@ export function cmdStart(argv: string[], ctx: Ctx): Outcome {
|
||||
* reviewer consumed), measure them, then admit the next step or dispatch.
|
||||
*/
|
||||
export function cmdCheck(argv: string[], ctx: Ctx): Outcome {
|
||||
const o = parseArgs(argv, ['run', 'record', 'step', 'next', 'reading', 'prior-reading', 'returned', 'dispatched-at', 'readers', 'runtime-lines']);
|
||||
const o = parseArgs(argv, ['run', 'record', 'step', 'next', 'reading', 'credits', 'prior-reading', 'returned', 'dispatched-at', 'readers', 'runtime-lines']);
|
||||
const runtimeRaw = str(o, 'runtime-lines');
|
||||
if (runtimeRaw !== undefined && !/^\d+$/.test(runtimeRaw)) throw usageError(`--runtime-lines '${runtimeRaw}' is not a line count; pass runtime_lines from gstack-review-plan --json`);
|
||||
const step = parseStep(str(o, 'step'));
|
||||
@@ -304,7 +316,7 @@ export function cmdCheck(argv: string[], ctx: Ctx): Outcome {
|
||||
}
|
||||
|
||||
export function cmdSettle(argv: string[], ctx: Ctx, release = false): Outcome {
|
||||
const o = parseArgs(argv, ['run', 'record', 'reservation', ...(release ? [] : ['reading', 'dispatched-at', 'readers'])], release ? [] : ['unknown']);
|
||||
const o = parseArgs(argv, ['run', 'record', 'reservation', ...(release ? [] : ['reading', 'credits', 'dispatched-at', 'readers'])], release ? [] : ['unknown']);
|
||||
const id = str(o, 'reservation');
|
||||
if (!id) throw usageError(`${release ? 'release' : 'settle'} needs --reservation <id>`);
|
||||
return mutateLedger(ledgerFileFor({ run: str(o, 'run'), record: str(o, 'record') }, ctx.env), ctx.env, l => {
|
||||
@@ -320,19 +332,21 @@ export function cmdSettle(argv: string[], ctx: Ctx, release = false): Outcome {
|
||||
}
|
||||
|
||||
export function cmdClose(argv: string[], ctx: Ctx): Outcome {
|
||||
const o = parseArgs(argv, ['run', 'record', 'reading', 'approved-by', 'words']);
|
||||
const o = parseArgs(argv, ['run', 'record', 'reading', 'credits', 'approved-by', 'words']);
|
||||
return mutateLedger(ledgerFileFor({ run: str(o, 'run'), record: str(o, 'record') }, ctx.env), ctx.env, l => {
|
||||
readMeter(l, o, false);
|
||||
for (const r of l.reservations) if (r.kind === 'step' && r.state === 'open') settleIn(l, r);
|
||||
if (freshReading(l, o)) measureSettled(l);
|
||||
const t = tally(l);
|
||||
const open = l.reservations.filter(r => r.kind === 'dispatch' && r.state === 'consumed').length;
|
||||
const lines = [`BUDGET_FINAL: used=${t.used} reserved=${t.reserved} unknown=${t.unknown} admission_cap=${l.cap ?? 'none'} unit=${l.meter.contract.unit} meter=${l.meter.contract.meter} epoch=${l.meter.epoch} open_dispatches=${open}`];
|
||||
const credits = creditsUsed(l);
|
||||
const lines = [`BUDGET_FINAL: used=${t.used}${credits === null ? '' : ` credits=${credits}`} reserved=${t.reserved} unknown=${t.unknown} admission_cap=${l.cap ?? 'none'} unit=${l.meter.contract.unit} meter=${l.meter.contract.meter} epoch=${l.meter.epoch} open_dispatches=${open}`];
|
||||
const sum = (kind: string, k: 'projected' | 'actual') => (l.samples ?? []).filter(s => s.kind === kind).reduce((n, s) => n + s[k], 0);
|
||||
const count = (kind: string) => (l.samples ?? []).filter(s => s.kind === kind).length;
|
||||
if (l.notes?.length) lines.push(`BUDGET_TIMELINE: ${l.notes.map(noteText).join('; ')}`);
|
||||
lines.push(`BUDGET_DRIFT: steps=${count('step')} step_projected=${sum('step', 'projected')} step_actual=${sum('step', 'actual')} dispatches=${count('dispatch')} dispatch_projected=${sum('dispatch', 'projected')} dispatch_actual=${sum('dispatch', 'actual')}`);
|
||||
const accepted = new Set((l as any).accepted_skips as string[] | undefined ?? []);
|
||||
const skipped = l.cap === null ? [] : unreservedDispatches(l).filter(s => !accepted.has(s.started));
|
||||
const skipped = l.cap === null || l.mode === 'observe' ? [] : unreservedDispatches(l).filter(s => !accepted.has(s.started));
|
||||
const dropped = l.reservations.filter(r => r.remaining?.length && !accepted.has(r.id));
|
||||
if ((skipped.length || dropped.length) && str(o, 'approved-by') && str(o, 'words')) {
|
||||
(l as any).accepted_skips = [...accepted, ...skipped.map(s => s.started), ...dropped.map(r => r.id)];
|
||||
@@ -352,9 +366,39 @@ export function cmdClose(argv: string[], ctx: Ctx): Outcome {
|
||||
});
|
||||
}
|
||||
|
||||
const noteText = (n: Note) => `${n.step}${n.label ? `:${n.label}` : ''} used=${n.used}${n.credits === undefined ? '' : ` credits=${n.credits}`}`
|
||||
+ (n.readers ? ` readers=${n.readers.n} readers_tokens=${n.readers.tokens}${n.readers.credits === undefined ? '' : ` readers_credits=${n.readers.credits}`}` : '');
|
||||
|
||||
/**
|
||||
* note: record one program's readings (tokens and credits) and, when --dispatched-at
|
||||
* brackets them, the readers it dispatched; stop only once used + unknown pass the cap.
|
||||
* It reserves, admits and delegates nothing: ship/DRIVER.md's observe runs end every
|
||||
* program with it instead of a check at each boundary.
|
||||
*/
|
||||
export function cmdNote(argv: string[], ctx: Ctx): Outcome {
|
||||
const o = parseArgs(argv, ['run', 'record', 'step', 'label', 'reading', 'credits', 'dispatched-at', 'dispatched-credits', 'readers']);
|
||||
const step = parseStep(str(o, 'step'));
|
||||
const label = str(o, 'label');
|
||||
return mutateLedger(ledgerFileFor({ run: str(o, 'run'), record: str(o, 'record') }, ctx.env), ctx.env, l => {
|
||||
if (l.closed_at) throw usageError(`run ${l.run} is closed`);
|
||||
if (l.stopped) return { stdout: [budgetLine(l, { call: null, next: l.stopped.next, projected: 0, verdict: 'stop' }), `STOP: reason=${l.stopped.reason} at Step ${l.stopped.step}; resume first: gstack-ship-budget resume --record ${l.record}`, `GSTACK_RESULT: skill=ship status=incomplete run=${l.dir}`], exit: 3 };
|
||||
if (l.meter.contract.meter === 'capy' && str(o, 'reading') === undefined) throw budgetError('BUDGET_READING_MISSING', `note --step ${step}: run ${l.run} meter=capy`);
|
||||
const dispatched = dispatchedOpt(o, null);
|
||||
const before = str(o, 'dispatched-credits') === undefined ? undefined : parseCredits(str(o, 'dispatched-credits'), '--dispatched-credits');
|
||||
if (before !== undefined && !dispatched) throw usageError('--dispatched-credits needs --dispatched-at <the token reading taken with it>');
|
||||
readMeter(l, o, true);
|
||||
const credits = creditsUsed(l);
|
||||
const readers = dispatched && l.meter.last !== null ? { n: dispatched.readers ?? 1, tokens: l.meter.last - dispatched.at,
|
||||
...(before !== undefined && l.credits ? { credits: Math.round((l.credits.last - before) * 100) / 100 } : {}) } : undefined;
|
||||
const n: Note = { step, ...(label ? { label } : {}), at: new Date().toISOString(), used: tally(l).used, ...(credits === null ? {} : { credits }), ...(readers ? { readers } : {}) };
|
||||
(l.notes ??= []).push(n);
|
||||
return observed(l, step, label ?? step, null, `NOTE: ${noteText(n)}`);
|
||||
});
|
||||
}
|
||||
|
||||
/** resume: re-attach after a stop or a machine change; rebuild or restore a lost ledger; journal a cap raise. */
|
||||
export function cmdResume(argv: string[], ctx: Ctx): Outcome {
|
||||
const o = parseArgs(argv, ['record', 'run', 'reading', 'prior-reading', 'meter', 'session', 'nonce', 'token-cap', 'approved-by', 'words'], ['rebuild-from-record']);
|
||||
const o = parseArgs(argv, ['record', 'run', 'reading', 'credits', 'prior-reading', 'meter', 'session', 'nonce', 'token-cap', 'approved-by', 'words'], ['rebuild-from-record']);
|
||||
const cap = str(o, 'token-cap');
|
||||
if (cap !== undefined && (!str(o, 'approved-by') || !str(o, 'words'))) throw usageError('resume --token-cap needs --approved-by <who> --words <the owner\'s words>; raising the cap is the owner\'s call');
|
||||
const lines: string[] = [];
|
||||
@@ -409,6 +453,7 @@ function rebuildLedger(file: string, record: string | undefined, ctx: Ctx, o: Op
|
||||
meter: { contract: METER_CONTRACTS[meter], epoch: num('epoch') + 1, baseline: meter === 'capy' ? reading! : 0, last: meter === 'capy' ? reading! : null },
|
||||
epochs: [], carried_used: num('used'), estimate_used: 0, unknown: num('unknown') + num('reserved'), next_id: 1, reservations: [], handoffs: [], cap_raises: [],
|
||||
...(() => { try { return { marker: reviewPaths(ctx.cwd, ctx.env).marker, review_log: reviewPaths(ctx.cwd, ctx.env).reviewLog }; } catch { return {}; } })(),
|
||||
...(b.mode === 'observe' ? { mode: 'observe' as const } : {}),
|
||||
};
|
||||
if (meter === 'estimate') l.estimate_used = 0;
|
||||
fs.mkdirSync(l.dir, { recursive: true, mode: 0o700 });
|
||||
|
||||
+28
-4
@@ -62,6 +62,8 @@ export interface Reservation {
|
||||
/** Every review-log start this reservation admitted: one per role (token and started_at keep the first). */
|
||||
starts?: Array<{ token: string; at: string }>;
|
||||
}
|
||||
/** One observation: the run's totals at a program's end, and the readers it dispatched when it measured them. */
|
||||
export interface Note { step: string; label?: string; at: string; used: number; credits?: number; readers?: { n: number; tokens: number; credits?: number } }
|
||||
export interface Sample { id: string; kind: 'step' | 'dispatch'; epoch: number; prior: number; projected: number; actual: number }
|
||||
export interface Handoff {
|
||||
n: number; sha: string; file: string; from: string; to: string; sub_cap: number; reservation: string; reading_at: number | null;
|
||||
@@ -82,6 +84,11 @@ export interface Ledger {
|
||||
call_floor?: number; depth?: number; steps_done?: string[];
|
||||
/** Capy per-call charges (--reading minus a fresh --prior-reading), per epoch: step priors use their median. */
|
||||
calls?: Array<{ epoch: number; value: number }>;
|
||||
/** observe (ship/DRIVER.md): readings are recorded by note and the cap is one hard stop; no reservation, admission or delegation. */
|
||||
mode?: 'admit' | 'observe';
|
||||
/** Capy cost.credits readings beside the token meter: baseline is the first reading, last the newest. */
|
||||
credits?: { baseline: number; last: number };
|
||||
notes?: Note[];
|
||||
stopped?: { at: string; reason: string; step: string; next: string }; closed_at?: string;
|
||||
}
|
||||
|
||||
@@ -376,10 +383,26 @@ export function admits(t: { used: number; reserved: number; unknown: number }, p
|
||||
return cap === null || t.used + t.reserved + t.unknown + projected + reserve <= cap;
|
||||
}
|
||||
|
||||
/** A cost.credits value: a non-negative decimal, kept to two places. */
|
||||
export function parseCredits(raw: string | undefined, flag: string): number {
|
||||
if (!/^\d+(?:\.\d+)?$/.test((raw ?? '').trim())) throw usageError(`${flag} '${raw ?? ''}' is not a credits reading; fix: pass cost.credits from the same thread.get as --reading, e.g. 845009.2`);
|
||||
return Math.round(Number(raw) * 100) / 100;
|
||||
}
|
||||
|
||||
/** Store a credits reading; the first is the baseline, and one below the last is refused like a token reading. */
|
||||
export function applyCredits(l: Ledger, value: number | undefined): void {
|
||||
if (value === undefined) return;
|
||||
if (l.credits && value < l.credits.last) throw budgetError('BUDGET_READING_REJECTED', `--credits ${value} is below the last credits reading ${l.credits.last}`);
|
||||
l.credits = { baseline: l.credits?.baseline ?? value, last: value };
|
||||
}
|
||||
|
||||
export const creditsUsed = (l: Ledger): number | null => (l.credits ? Math.round((l.credits.last - l.credits.baseline) * 100) / 100 : null);
|
||||
|
||||
export function budgetLine(l: Ledger, f: { call: number | null; next: string; projected: number; verdict: string }): string {
|
||||
const t = tally(l);
|
||||
const p = projector(l);
|
||||
return `BUDGET: used=${t.used} reserved=${t.reserved} unknown=${t.unknown} admission_cap=${l.cap ?? 'none'} tail=${p.reserve} unit=${l.meter.contract.unit} meter=${l.meter.contract.meter} epoch=${l.meter.epoch} call=${f.call ?? 'unknown'} call_floor=${p.floor} above_floor=${aboveFloor(f.call, p.floor) ?? 'unknown'} next=${f.next} projected=${f.projected} calibration=${p.label} verdict=${f.verdict}`;
|
||||
const credits = creditsUsed(l);
|
||||
return `BUDGET: used=${t.used}${credits === null ? '' : ` credits=${credits}`} reserved=${t.reserved} unknown=${t.unknown} admission_cap=${l.cap ?? 'none'} tail=${p.reserve} unit=${l.meter.contract.unit} meter=${l.meter.contract.meter} epoch=${l.meter.epoch} call=${f.call ?? 'unknown'} call_floor=${p.floor} above_floor=${aboveFloor(f.call, p.floor) ?? 'unknown'} next=${f.next} projected=${f.projected} calibration=${p.label} verdict=${f.verdict}`;
|
||||
}
|
||||
|
||||
/** The per-call context above the host floor that the delegate thresholds compare; null when the charge is unknown. */
|
||||
@@ -437,7 +460,8 @@ const BLOCK = /```gstack-budget\n[\s\S]*?\n```\n?/;
|
||||
export function recordBlock(l: Ledger): string {
|
||||
const t = tally(l);
|
||||
const open = l.handoffs.filter(h => !h.reconciled_at).map(h => h.sha).join(',') || 'none';
|
||||
return `run=${l.run} ledger=${l.file} role=${l.role} cap=${l.cap ?? 'none'} tail=${l.tail} unit=${l.meter.contract.unit} meter=${l.meter.contract.meter} epoch=${l.meter.epoch} baseline=${l.meter.baseline} used=${t.used} reserved=${t.reserved} unknown=${t.unknown} stop=${l.stopped?.reason ?? 'none'} handoff=${open}`;
|
||||
const credits = creditsUsed(l);
|
||||
return `run=${l.run} ledger=${l.file} role=${l.role} cap=${l.cap ?? 'none'} tail=${l.tail} unit=${l.meter.contract.unit} meter=${l.meter.contract.meter} epoch=${l.meter.epoch} baseline=${l.meter.baseline} used=${t.used}${credits === null ? '' : ` credits=${credits}`} reserved=${t.reserved} unknown=${t.unknown} stop=${l.stopped?.reason ?? 'none'} handoff=${open}${l.mode === 'observe' ? ' mode=observe' : ''}`;
|
||||
}
|
||||
|
||||
/** The record's fenced gstack-budget block: written by the bin, never by the agent. */
|
||||
@@ -495,7 +519,7 @@ export function reviewPaths(cwd: string, env: NodeJS.ProcessEnv): { marker: stri
|
||||
|
||||
export function pointMarker(marker: string | undefined, l: Ledger): void {
|
||||
if (!marker) return;
|
||||
if (l.cap === null) { try { fs.unlinkSync(marker); } catch {} return; }
|
||||
if (l.cap === null || l.mode === 'observe') { try { fs.unlinkSync(marker); } catch {} return; }
|
||||
writePrivate(marker, JSON.stringify({ run: l.run, ledger: l.file }) + '\n');
|
||||
}
|
||||
|
||||
@@ -503,7 +527,7 @@ 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 && fs.existsSync(pointer.ledger) ? readLedger(pointer.ledger) : undefined;
|
||||
return l && l.cap !== null && !l.closed_at ? l : undefined;
|
||||
return l && l.cap !== null && l.mode !== 'observe' && !l.closed_at ? l : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+4
-4
@@ -117,7 +117,7 @@ function brief(l: Ledger, h: Omit<Handoff, 'sha'>, pins: Record<string, unknown>
|
||||
}
|
||||
|
||||
export function cmdHandoff(argv: string[], ctx: Ctx): Outcome {
|
||||
const o = parseArgs(argv, ['run', 'record', 'from', 'to', 'base', 'reviewed-tree', 'trail', 'trail-run', 'sub-cap', 'reading', 'prior-reading', 'child-session']);
|
||||
const o = parseArgs(argv, ['run', 'record', 'from', 'to', 'base', 'reviewed-tree', 'trail', 'trail-run', 'sub-cap', 'reading', 'credits', 'prior-reading', 'child-session']);
|
||||
const from = parseStep(str(o, 'from'), '--from'), to = parseStep(str(o, 'to'), '--to');
|
||||
if (SHIP_STEPS.indexOf(from as any) > SHIP_STEPS.indexOf(to as any)) throw usageError(`--from ${from} is after --to ${to}`);
|
||||
const base = str(o, 'base');
|
||||
@@ -160,7 +160,7 @@ function handoffBlock(text: string): any {
|
||||
}
|
||||
|
||||
export function cmdVerifyHandoff(argv: string[], ctx: Ctx): Outcome {
|
||||
const o = parseArgs(argv, ['in', 'reading', 'session']);
|
||||
const o = parseArgs(argv, ['in', 'reading', 'credits', 'session']);
|
||||
const file = str(o, 'in');
|
||||
if (!file) throw usageError('verify-handoff needs --in <brief>');
|
||||
const text = fs.readFileSync(file, 'utf8');
|
||||
@@ -205,7 +205,7 @@ export function cmdVerifyHandoff(argv: string[], ctx: Ctx): Outcome {
|
||||
}
|
||||
|
||||
export function cmdReturn(argv: string[], ctx: Ctx): Outcome {
|
||||
const o = parseArgs(argv, ['run', 'to', 'changed', 'status', 'reading']);
|
||||
const o = parseArgs(argv, ['run', 'to', 'changed', 'status', 'reading', 'credits']);
|
||||
const to = parseStep(str(o, 'to'), '--to');
|
||||
const status = str(o, 'status') ?? 'complete';
|
||||
if (!['complete', 'refused', 'incomplete'].includes(status)) throw usageError(`--status '${status}' is not complete, refused or incomplete`);
|
||||
@@ -240,7 +240,7 @@ export function parseReturn(text: string): { handoff: string; status: string; to
|
||||
}
|
||||
|
||||
export function cmdReconcile(argv: string[], ctx: Ctx): Outcome {
|
||||
const o = parseArgs(argv, ['run', 'record', 'return', 'handoff', 'reading', 'prior-reading']);
|
||||
const o = parseArgs(argv, ['run', 'record', 'return', 'handoff', 'reading', 'credits', 'prior-reading']);
|
||||
return mutateLedger(ledgerFileFor({ run: str(o, 'run'), record: str(o, 'record') }, ctx.env), ctx.env, l => {
|
||||
const retFile = str(o, 'return');
|
||||
const text = retFile && fs.existsSync(retFile) ? fs.readFileSync(retFile, 'utf8') : undefined;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { classifyPath } from './review-depth';
|
||||
import { audit } from './ship-audit';
|
||||
import { matchesAny } from './ship-policy';
|
||||
import { bin, firstLine, flagReader, gh, git, hostedOrigin, laneLine, lanes, Report, runLanes, shipPolicy, short } from './ship-bin-common';
|
||||
|
||||
@@ -18,6 +19,8 @@ export const PREFLIGHT_HELP = `gstack-ship-preflight — /ship Steps 1-3 and Ste
|
||||
|
||||
gstack-ship-preflight --base <base> [--lane <label>=<command>]... Steps 1-3; with lanes also Step 5
|
||||
gstack-ship-preflight --tests-only --base <base> --lane <label>=<command>... Step 5 only (after a repair)
|
||||
gstack-ship-preflight --audit --base <base> --out <dir> the single reads and Steps 7-8's deterministic audits
|
||||
(CONTEXT, TODOS, COVERAGE_MAP, Coverage, PLAN_*, READER: coverage=needed|none plan=needed|none)
|
||||
|
||||
Prints at most 30 lines: BRANCH, STATUS, DIFF, PR/CARRY, APPLE, SCOPE, MERGE, DISTRIBUTION, POLICY,
|
||||
RUNTIME_LINES, PREGATE, one LANE line per lane, then NEXT: lines. STOP: names what the agent must
|
||||
@@ -121,6 +124,11 @@ export async function preflight(argv: readonly string[], cwd: string): Promise<{
|
||||
const base = flag('--base');
|
||||
const list = lanes(argv);
|
||||
if (has('--help') || has('-h')) return { text: PREFLIGHT_HELP + '\n', exit: 0 };
|
||||
if (has('--audit')) {
|
||||
const out = flag('--out');
|
||||
if (!base || !out) return { text: 'gstack-ship-preflight: --audit needs --base <base> --out <dir>\n', exit: 2 };
|
||||
return { text: audit(cwd, base, out).render(), exit: 0 };
|
||||
}
|
||||
if (!base || typeof list === 'string') return { text: `gstack-ship-preflight: ${typeof list === 'string' ? list : '--base <base> is required'}\n`, exit: 2 };
|
||||
if (has('--tests-only') && !list.length) return { text: 'gstack-ship-preflight: --tests-only needs at least one --lane <label>=<command>\n', exit: 2 };
|
||||
const r = new Report();
|
||||
|
||||
+90
-70
@@ -3,104 +3,124 @@
|
||||
# /ship driver: run /ship from a parent agent
|
||||
|
||||
Read this instead of `ship/SKILL.md` when a parent agent on any host runs /ship
|
||||
(`SESSION_KIND: unattended`): bins do the mechanics; this file holds the phases, the judgment and the
|
||||
lines each phase prints. Commands live in `<root>/bin/` (`<root>` holds `ship/`). Run each with the
|
||||
`GSTACK_LAUNCH` assignments your installer printed (`. <the GSTACK_LAUNCH_FILE it printed>`), never
|
||||
(`SESSION_KIND: unattended`). Commands live in `<root>/bin/` (`<root>` holds `ship/`); run each with
|
||||
the `GSTACK_LAUNCH` assignments your installer printed (`. <the GSTACK_LAUNCH_FILE it printed>`), never
|
||||
`export $(...)`, `env`, `printenv` or a bare `set` in a tool fence.
|
||||
|
||||
**Rules.** A phase is one program (an execute program on Capy, else a shell script) unless it
|
||||
says otherwise; print its result lines verbatim. `STOP:` (exit 3) means nothing after it ran; `NEXT:`
|
||||
names the next act. Read a section only when a phase or the list at the end sends you, never
|
||||
**Rules.** Each phase below is one call: one program (an execute program on Capy, else a shell
|
||||
script) that runs the bins, reads, dispatches and waits, returning only result lines and reader
|
||||
outputs; print them verbatim. The model keeps only lead judgment, fixes, CHANGELOG and PR prose, the
|
||||
gates and the report. `STOP:` (exit 3) means nothing after it ran; `NEXT:` 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 (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.
|
||||
(Phases 2-4: lanes, pregate, evals, `Coverage:`, the plan checks, `Scope Check:`), **Decisions** (any
|
||||
phase: each answered or auto-taken question, in its words), **Reviews** (Phases 3-5: the plan line,
|
||||
start tokens, `Pre-Landing Review:`, `Gate edits:`, the reviewed tree), **Release** (Phases 5-7:
|
||||
version, CHANGELOG, commits), **Documentation** (Step 14.5) and **Next steps** (every phase: the next act).
|
||||
|
||||
**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
|
||||
prompt, its severity and confidence rules and its gates come from that section verbatim. This file
|
||||
restates none of them and never shortens a reviewer's brief.
|
||||
**Reviewers are the sections' own.** Every audit, specialist, Red Team, QA, fix-check, adversarial,
|
||||
outside-voice and documentation brief, its severity and confidence rules and its gates come from its
|
||||
section verbatim, with only its placeholders filled. This file restates none of them and
|
||||
never shortens a reviewer's brief. Audit readers (Steps 7, 8, 9.2.1, 14.5) cannot block, so each runs on a
|
||||
small model at low effort; their findings go to the PR body.
|
||||
|
||||
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. Obey `verdict=`.
|
||||
**Calls.** One pass with no fixes is 9 calls: Phases 1-8 and the report; at most 12 with a STOP's
|
||||
section read or a fix-check. A pass that fixes something repeats Phases 3-4. Waits add pings, not work.
|
||||
|
||||
**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.
|
||||
**Budget: observe.** `gstack-ship-budget start --mode observe` records, never admits: each program ends
|
||||
with `gstack-ship-budget note --step <last step> --reading R --credits C`, R and C read in that program
|
||||
(Capy: `cost.tokens` and `cost.credits` of your own jam). No check per step, no reservation, no meter-only
|
||||
call, no handoff; under `--token-cap` a note past the cap prints `verdict=stop`, the one stop check.
|
||||
A program that dispatches readers takes `BEFORE` (R and C) right before they start and passes
|
||||
`--dispatched-at <R> --dispatched-credits <C> --readers <n>` with the reading after the last settles.
|
||||
|
||||
## 1. Preflight and tests: Steps 0, 0.9, 1, 2, 3, 4, 5
|
||||
**Waits.** No call waits past 240 s: a program polls or sleeps at most 240 s and returns what still
|
||||
runs; the next call resumes (Capy: `wait` 240 s on the operation). A call after a gap over 5 minutes
|
||||
bills the whole context fresh (about 40x a cached call); a ping costs a few percent of that. A small
|
||||
waiting child saves nothing: the parent's cache expires during its wait too. Past 100 minutes, stop.
|
||||
|
||||
`gstack-ship-budget start --record <rec> [--token-cap <n>] --delegate measured --reading R` and
|
||||
**One program per pass.** Each review pass, Step 11 and the Step 14.5 audit is one program, and the
|
||||
parent makes no other call while it runs: (1) `gstack-review-plan --base <base>` with the pass's
|
||||
flags (its `DISPATCH:` lines; Step 14.5 has one reader, `docs`); (2) `gstack-review-log --start
|
||||
<skill>` once per row the step logs (Step 14.5 takes none); (3) `BEFORE`; (4) one subagent per reader;
|
||||
(5) a poll until every reader settles; (6) `note` with the reading pair; a later batch (the Red Team,
|
||||
given the specialists' collected lines) repeats (3)-(6); (7) the collect: return only each reader's
|
||||
final output and status. The next call applies the section's merge, judgment, fixes and persistence;
|
||||
it never walks the dispatch steps call by call.
|
||||
|
||||
## 1. Load: Step 0
|
||||
|
||||
The install check and this file.
|
||||
|
||||
## 2. Preflight, tests, evals and reads: Steps 0.9, 1, 2, 3, 4, 5, 6, 6.5, 10
|
||||
|
||||
`gstack-ship-budget start --record <rec> --mode observe [--token-cap <n>] --reading R --credits C`, then
|
||||
`gstack-ship-preflight --base <base> --lane <label>=<command>...` (the test commands AGENTS.md or
|
||||
CLAUDE.md names). Prints `STATUS:`, `DIFF:`, `PR:`, `CARRY:`, `SCOPE:`,
|
||||
`MERGE:`, `DISTRIBUTION:`, `POLICY:`, `RUNTIME_LINES:` (for `check --runtime-lines`), `PREGATE:`, `LANE:`.
|
||||
`CARRY: carried` ends the run on its `GSTACK_RESULT:` line. `--delegate measured` delegates a segment
|
||||
only when a measured call is 150k or more above the host floor (`DELEGATE:`). A lane
|
||||
passes on `tests_ran: N` with exit 0; `tests_ran: 0` is ZERO-RUN, a failed lane; `unknown` is `count unavailable`.
|
||||
In-branch failures stop the ship; after a repair rerun with `--tests-only`. `requires-remote` needs
|
||||
`gstack-pregate clear` before Step 18. The readiness dashboard and a `PERSIST:` hint never replace Step 9.
|
||||
CLAUDE.md names): `STATUS:`, `DIFF:`, `PR:`, `CARRY:`, `SCOPE:`, `MERGE:`, `DISTRIBUTION:`,
|
||||
`POLICY:`, `RUNTIME_LINES:`, `PREGATE:`, `LANE:`. `CARRY: carried` ends the run on its `GSTACK_RESULT:`
|
||||
line. A lane passes on `tests_ran: N` with exit 0; `tests_ran: 0` is ZERO-RUN, a failed lane; `unknown`
|
||||
is `count unavailable`. In-branch failures stop the ship; after a repair rerun with `--tests-only`.
|
||||
`requires-remote` needs `gstack-pregate clear` before Step 18. When prompts, templates, judges or eval
|
||||
harness changed, start the project's eval selector and pre-merge command (detached past 30 min); else
|
||||
record `evals: none affected`. Steps 6.5 and 10 run only if `gstack-greptile-early detect` finds
|
||||
Greptile. Then `gstack-ship-preflight --audit --base <base> --out <run dir>` writes the diff once
|
||||
(`CONTEXT:`) and the TODOS.md items naming touched paths (`TODOS:`), and prints the coverage map, the
|
||||
plan candidates and `READER: coverage=<needed|none> plan=<needed|none>`. The same program reads
|
||||
`sections/review-army.md`, `sections/adversarial.md`, `sections/documentation.md`, the todos and commits
|
||||
files, and `sections/test-coverage.md` or `sections/plan-completion.md` only when its reader is `needed`.
|
||||
Never read TODOS.md or the full diff into the parent. A `PERSIST:` hint never replaces Step 9.
|
||||
|
||||
## 2. Evals and Greptile: Steps 6, 6.5, 10
|
||||
## 3. Audits and review: Steps 7, 8, 9, 9.1, 9.2, 9.2.1
|
||||
|
||||
When prompts, templates, judges or eval harness changed, run the project's eval selector and
|
||||
pre-merge command (detached past 30 min); else record `evals: none affected`. Steps 6.5 and 10 run
|
||||
only if `gstack-greptile-early detect` finds Greptile.
|
||||
One program: the audit readers `READER:` names (Step 7 from `sections/test-coverage.md`, Step 8 from
|
||||
`sections/plan-completion.md`, each run as written), Step 9.2.1's QA reader (that step of
|
||||
`sections/review-army.md` as its brief) and the pass (above): Step 9's plan and start, Step 9.1's
|
||||
specialists and Step 9.2's Red Team as written. `Coverage:` and Step 8's line come from `--audit` when no
|
||||
reader ran.
|
||||
|
||||
## 3. Audits: Steps 7, 8, 8.1, 8.2
|
||||
## 4. Judgment: Steps 8.1, 8.2, 9.3, 9.4 and Step 11
|
||||
|
||||
Read `sections/test-coverage.md` and `sections/plan-completion.md` and run Steps 7, 8, 8.1 and 8.2
|
||||
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.
|
||||
Steps 8.1 and 8.2 (`Scope Check:`) on the todos and commits files, then the core checklist, Steps
|
||||
9.3, 9.4 and the repeat decision as written (`gstack-review-plan` included), fixes and persistence in
|
||||
one program. Prints `Pre-Landing Review:`, `Gate edits:`. `NEXT: stop` stops; `full` or
|
||||
`scoped` reruns Step 5 (`--tests-only`) and Phases 3-4; `done`: the same program runs Step 11 from
|
||||
`sections/adversarial.md` as written (above), only the voices the last full-pass plan lists (neither
|
||||
prints `ADVERSARIAL: skipped (review plan)`).
|
||||
|
||||
## 4. Review: Steps 9, 9.1, 9.2, 9.2.1, 9.3, 9.4
|
||||
## 5. Voices, bind and version: Steps 11, 11.5, 12
|
||||
|
||||
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.
|
||||
Judge the voices; missing outside coverage is `unavailable`, never clean; queued fixes rerun Phases
|
||||
3-4. Then `gstack-review-read`: the final Step 9 row and Step 11 native row (or `CURRENT` freshness
|
||||
after a planned skip) agree on `wtree`, `start_wtree`, `end_wtree`; save that reviewed tree. Then
|
||||
`gstack-ship-release version --base <base> [--bump micro|patch] --why '<line>'`; MINOR or MAJOR is a
|
||||
question; `VERSION: skipped (policy: ...)`: write no version.
|
||||
|
||||
## 5. Adversarial: Step 11
|
||||
## 6. Notes and documentation: Steps 13, 14, 14.5
|
||||
|
||||
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.
|
||||
Step 13: one user-facing CHANGELOG entry from the commits file; `gstack-prose-lint CHANGELOG.md --base
|
||||
origin/<base> --gate` until `PROSE_LINT: ok`. Step 14: TODOS.md deferrals and completions, edited from
|
||||
the todos file. Step 14.5, every ship: read `sections/documentation.md` and run its audit as written, as
|
||||
one program (above), then `gstack-ship-receipt docs-caps --base <base>`; print `Documentation:`.
|
||||
One re-audit at most.
|
||||
|
||||
## 6. Bind: Step 11.5
|
||||
## 7. Release: Steps 15, 16, 17
|
||||
|
||||
`gstack-review-read`: the final Step 9 row and Step 11 native row (or `CURRENT` freshness after a
|
||||
planned skip) agree on `wtree`, `start_wtree`, `end_wtree`. Save that reviewed tree.
|
||||
|
||||
## 7. Release: Steps 12, 13, 14, 14.5, 15, 16, 17
|
||||
|
||||
- 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, 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.
|
||||
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`. Then
|
||||
`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.
|
||||
|
||||
## 8. Publish: Steps 18, 19, 19-C, 20, 21, 22
|
||||
|
||||
- Step 18: the open-PR lookup; a title file starting `v<NEW_VERSION> `. `UNATTENDED_SHIP:` with no open PR at HEAD takes Step 18-H.
|
||||
- Step 19 body: the receipt block; with `CAPY_PR: on` (Step 19-C) What changed, Why, Blast radius, Approvals (`gstack-approvals render`); Summary, Test Coverage, Pre-Landing Review (`REVIEW_LOOP:`, `Gate edits:`, deferred, notes), Exploratory QA, Plan Completion, Verification Results, Issues, TODOS, Documentation, Test plan. `gstack-prose-lint <top> --sections 'Why,Summary,Blast radius' --gate`; `gstack-issue-links`; `gstack-review-scope summary --since "$REVIEW_SINCE" && gstack-ship-receipt write --pr <n> --base <base> --gate @<f> --pregate <f> --issues @<f> --review-loop @<f> --docs @<f> --body <body>`; `gstack-post pr-body` and `pr-title` (a new PR via the host's PR tool; a draft while a gate finding is `open`). `SHIP_RECEIPT:` opens your report.
|
||||
- Step 20: the ship metrics row via `gstack-review-log`.
|
||||
- Step 22: `gstack-ship-budget close`, then `gstack-ship-receipt result --pr <n>`: `GSTACK_RESULT: skill=ship status=complete run=<dir>`. Under a merge queue, `gstack-ship-queue ready --pr <n> [--screen|--no-screen]` at the pushed head; paste its output.
|
||||
- Step 22: `gstack-ship-budget close --reading R --credits C` (`BUDGET_FINAL:` and `BUDGET_TIMELINE:` carry tokens and credits; put both in the report), then `gstack-ship-receipt result --pr <n>`: `GSTACK_RESULT: skill=ship status=complete run=<dir>`. Under a merge queue, `gstack-ship-queue ready --pr <n> [--screen|--no-screen]` at the pushed head; paste its output.
|
||||
|
||||
## Where each STOP sends you (paths under `<root>/ship/`)
|
||||
|
||||
|
||||
+90
-70
@@ -1,104 +1,124 @@
|
||||
# /ship driver: run /ship from a parent agent
|
||||
|
||||
Read this instead of `ship/SKILL.md` when a parent agent on any host runs /ship
|
||||
(`SESSION_KIND: unattended`): bins do the mechanics; this file holds the phases, the judgment and the
|
||||
lines each phase prints. Commands live in `<root>/bin/` (`<root>` holds `ship/`). Run each with the
|
||||
`GSTACK_LAUNCH` assignments your installer printed (`. <the GSTACK_LAUNCH_FILE it printed>`), never
|
||||
(`SESSION_KIND: unattended`). Commands live in `<root>/bin/` (`<root>` holds `ship/`); run each with
|
||||
the `GSTACK_LAUNCH` assignments your installer printed (`. <the GSTACK_LAUNCH_FILE it printed>`), never
|
||||
`export $(...)`, `env`, `printenv` or a bare `set` in a tool fence.
|
||||
|
||||
**Rules.** A phase is one program (an execute program on Capy, else a shell script) unless it
|
||||
says otherwise; print its result lines verbatim. `STOP:` (exit 3) means nothing after it ran; `NEXT:`
|
||||
names the next act. Read a section only when a phase or the list at the end sends you, never
|
||||
**Rules.** Each phase below is one call: one program (an execute program on Capy, else a shell
|
||||
script) that runs the bins, reads, dispatches and waits, returning only result lines and reader
|
||||
outputs; print them verbatim. The model keeps only lead judgment, fixes, CHANGELOG and PR prose, the
|
||||
gates and the report. `STOP:` (exit 3) means nothing after it ran; `NEXT:` 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 (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.
|
||||
(Phases 2-4: lanes, pregate, evals, `Coverage:`, the plan checks, `Scope Check:`), **Decisions** (any
|
||||
phase: each answered or auto-taken question, in its words), **Reviews** (Phases 3-5: the plan line,
|
||||
start tokens, `Pre-Landing Review:`, `Gate edits:`, the reviewed tree), **Release** (Phases 5-7:
|
||||
version, CHANGELOG, commits), **Documentation** (Step 14.5) and **Next steps** (every phase: the next act).
|
||||
|
||||
**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
|
||||
prompt, its severity and confidence rules and its gates come from that section verbatim. This file
|
||||
restates none of them and never shortens a reviewer's brief.
|
||||
**Reviewers are the sections' own.** Every audit, specialist, Red Team, QA, fix-check, adversarial,
|
||||
outside-voice and documentation brief, its severity and confidence rules and its gates come from its
|
||||
section verbatim, with only its placeholders filled. This file restates none of them and
|
||||
never shortens a reviewer's brief. Audit readers (Steps 7, 8, 9.2.1, 14.5) cannot block, so each runs on a
|
||||
small model at low effort; their findings go to the PR body.
|
||||
|
||||
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. Obey `verdict=`.
|
||||
**Calls.** One pass with no fixes is 9 calls: Phases 1-8 and the report; at most 12 with a STOP's
|
||||
section read or a fix-check. A pass that fixes something repeats Phases 3-4. Waits add pings, not work.
|
||||
|
||||
**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.
|
||||
**Budget: observe.** `gstack-ship-budget start --mode observe` records, never admits: each program ends
|
||||
with `gstack-ship-budget note --step <last step> --reading R --credits C`, R and C read in that program
|
||||
(Capy: `cost.tokens` and `cost.credits` of your own jam). No check per step, no reservation, no meter-only
|
||||
call, no handoff; under `--token-cap` a note past the cap prints `verdict=stop`, the one stop check.
|
||||
A program that dispatches readers takes `BEFORE` (R and C) right before they start and passes
|
||||
`--dispatched-at <R> --dispatched-credits <C> --readers <n>` with the reading after the last settles.
|
||||
|
||||
## 1. Preflight and tests: Steps 0, 0.9, 1, 2, 3, 4, 5
|
||||
**Waits.** No call waits past 240 s: a program polls or sleeps at most 240 s and returns what still
|
||||
runs; the next call resumes (Capy: `wait` 240 s on the operation). A call after a gap over 5 minutes
|
||||
bills the whole context fresh (about 40x a cached call); a ping costs a few percent of that. A small
|
||||
waiting child saves nothing: the parent's cache expires during its wait too. Past 100 minutes, stop.
|
||||
|
||||
`gstack-ship-budget start --record <rec> [--token-cap <n>] --delegate measured --reading R` and
|
||||
**One program per pass.** Each review pass, Step 11 and the Step 14.5 audit is one program, and the
|
||||
parent makes no other call while it runs: (1) `gstack-review-plan --base <base>` with the pass's
|
||||
flags (its `DISPATCH:` lines; Step 14.5 has one reader, `docs`); (2) `gstack-review-log --start
|
||||
<skill>` once per row the step logs (Step 14.5 takes none); (3) `BEFORE`; (4) one subagent per reader;
|
||||
(5) a poll until every reader settles; (6) `note` with the reading pair; a later batch (the Red Team,
|
||||
given the specialists' collected lines) repeats (3)-(6); (7) the collect: return only each reader's
|
||||
final output and status. The next call applies the section's merge, judgment, fixes and persistence;
|
||||
it never walks the dispatch steps call by call.
|
||||
|
||||
## 1. Load: Step 0
|
||||
|
||||
The install check and this file.
|
||||
|
||||
## 2. Preflight, tests, evals and reads: Steps 0.9, 1, 2, 3, 4, 5, 6, 6.5, 10
|
||||
|
||||
`gstack-ship-budget start --record <rec> --mode observe [--token-cap <n>] --reading R --credits C`, then
|
||||
`gstack-ship-preflight --base <base> --lane <label>=<command>...` (the test commands AGENTS.md or
|
||||
CLAUDE.md names). Prints `STATUS:`, `DIFF:`, `PR:`, `CARRY:`, `SCOPE:`,
|
||||
`MERGE:`, `DISTRIBUTION:`, `POLICY:`, `RUNTIME_LINES:` (for `check --runtime-lines`), `PREGATE:`, `LANE:`.
|
||||
`CARRY: carried` ends the run on its `GSTACK_RESULT:` line. `--delegate measured` delegates a segment
|
||||
only when a measured call is 150k or more above the host floor (`DELEGATE:`). A lane
|
||||
passes on `tests_ran: N` with exit 0; `tests_ran: 0` is ZERO-RUN, a failed lane; `unknown` is `count unavailable`.
|
||||
In-branch failures stop the ship; after a repair rerun with `--tests-only`. `requires-remote` needs
|
||||
`gstack-pregate clear` before Step 18. The readiness dashboard and a `PERSIST:` hint never replace Step 9.
|
||||
CLAUDE.md names): `STATUS:`, `DIFF:`, `PR:`, `CARRY:`, `SCOPE:`, `MERGE:`, `DISTRIBUTION:`,
|
||||
`POLICY:`, `RUNTIME_LINES:`, `PREGATE:`, `LANE:`. `CARRY: carried` ends the run on its `GSTACK_RESULT:`
|
||||
line. A lane passes on `tests_ran: N` with exit 0; `tests_ran: 0` is ZERO-RUN, a failed lane; `unknown`
|
||||
is `count unavailable`. In-branch failures stop the ship; after a repair rerun with `--tests-only`.
|
||||
`requires-remote` needs `gstack-pregate clear` before Step 18. When prompts, templates, judges or eval
|
||||
harness changed, start the project's eval selector and pre-merge command (detached past 30 min); else
|
||||
record `evals: none affected`. Steps 6.5 and 10 run only if `gstack-greptile-early detect` finds
|
||||
Greptile. Then `gstack-ship-preflight --audit --base <base> --out <run dir>` writes the diff once
|
||||
(`CONTEXT:`) and the TODOS.md items naming touched paths (`TODOS:`), and prints the coverage map, the
|
||||
plan candidates and `READER: coverage=<needed|none> plan=<needed|none>`. The same program reads
|
||||
`sections/review-army.md`, `sections/adversarial.md`, `sections/documentation.md`, the todos and commits
|
||||
files, and `sections/test-coverage.md` or `sections/plan-completion.md` only when its reader is `needed`.
|
||||
Never read TODOS.md or the full diff into the parent. A `PERSIST:` hint never replaces Step 9.
|
||||
|
||||
## 2. Evals and Greptile: Steps 6, 6.5, 10
|
||||
## 3. Audits and review: Steps 7, 8, 9, 9.1, 9.2, 9.2.1
|
||||
|
||||
When prompts, templates, judges or eval harness changed, run the project's eval selector and
|
||||
pre-merge command (detached past 30 min); else record `evals: none affected`. Steps 6.5 and 10 run
|
||||
only if `gstack-greptile-early detect` finds Greptile.
|
||||
One program: the audit readers `READER:` names (Step 7 from `sections/test-coverage.md`, Step 8 from
|
||||
`sections/plan-completion.md`, each run as written), Step 9.2.1's QA reader (that step of
|
||||
`sections/review-army.md` as its brief) and the pass (above): Step 9's plan and start, Step 9.1's
|
||||
specialists and Step 9.2's Red Team as written. `Coverage:` and Step 8's line come from `--audit` when no
|
||||
reader ran.
|
||||
|
||||
## 3. Audits: Steps 7, 8, 8.1, 8.2
|
||||
## 4. Judgment: Steps 8.1, 8.2, 9.3, 9.4 and Step 11
|
||||
|
||||
Read `sections/test-coverage.md` and `sections/plan-completion.md` and run Steps 7, 8, 8.1 and 8.2
|
||||
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.
|
||||
Steps 8.1 and 8.2 (`Scope Check:`) on the todos and commits files, then the core checklist, Steps
|
||||
9.3, 9.4 and the repeat decision as written (`gstack-review-plan` included), fixes and persistence in
|
||||
one program. Prints `Pre-Landing Review:`, `Gate edits:`. `NEXT: stop` stops; `full` or
|
||||
`scoped` reruns Step 5 (`--tests-only`) and Phases 3-4; `done`: the same program runs Step 11 from
|
||||
`sections/adversarial.md` as written (above), only the voices the last full-pass plan lists (neither
|
||||
prints `ADVERSARIAL: skipped (review plan)`).
|
||||
|
||||
## 4. Review: Steps 9, 9.1, 9.2, 9.2.1, 9.3, 9.4
|
||||
## 5. Voices, bind and version: Steps 11, 11.5, 12
|
||||
|
||||
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.
|
||||
Judge the voices; missing outside coverage is `unavailable`, never clean; queued fixes rerun Phases
|
||||
3-4. Then `gstack-review-read`: the final Step 9 row and Step 11 native row (or `CURRENT` freshness
|
||||
after a planned skip) agree on `wtree`, `start_wtree`, `end_wtree`; save that reviewed tree. Then
|
||||
`gstack-ship-release version --base <base> [--bump micro|patch] --why '<line>'`; MINOR or MAJOR is a
|
||||
question; `VERSION: skipped (policy: ...)`: write no version.
|
||||
|
||||
## 5. Adversarial: Step 11
|
||||
## 6. Notes and documentation: Steps 13, 14, 14.5
|
||||
|
||||
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.
|
||||
Step 13: one user-facing CHANGELOG entry from the commits file; `gstack-prose-lint CHANGELOG.md --base
|
||||
origin/<base> --gate` until `PROSE_LINT: ok`. Step 14: TODOS.md deferrals and completions, edited from
|
||||
the todos file. Step 14.5, every ship: read `sections/documentation.md` and run its audit as written, as
|
||||
one program (above), then `gstack-ship-receipt docs-caps --base <base>`; print `Documentation:`.
|
||||
One re-audit at most.
|
||||
|
||||
## 6. Bind: Step 11.5
|
||||
## 7. Release: Steps 15, 16, 17
|
||||
|
||||
`gstack-review-read`: the final Step 9 row and Step 11 native row (or `CURRENT` freshness after a
|
||||
planned skip) agree on `wtree`, `start_wtree`, `end_wtree`. Save that reviewed tree.
|
||||
|
||||
## 7. Release: Steps 12, 13, 14, 14.5, 15, 16, 17
|
||||
|
||||
- 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, 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.
|
||||
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`. Then
|
||||
`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.
|
||||
|
||||
## 8. Publish: Steps 18, 19, 19-C, 20, 21, 22
|
||||
|
||||
- Step 18: the open-PR lookup; a title file starting `v<NEW_VERSION> `. `UNATTENDED_SHIP:` with no open PR at HEAD takes Step 18-H.
|
||||
- Step 19 body: the receipt block; with `CAPY_PR: on` (Step 19-C) What changed, Why, Blast radius, Approvals (`gstack-approvals render`); Summary, Test Coverage, Pre-Landing Review (`REVIEW_LOOP:`, `Gate edits:`, deferred, notes), Exploratory QA, Plan Completion, Verification Results, Issues, TODOS, Documentation, Test plan. `gstack-prose-lint <top> --sections 'Why,Summary,Blast radius' --gate`; `gstack-issue-links`; `gstack-review-scope summary --since "$REVIEW_SINCE" && gstack-ship-receipt write --pr <n> --base <base> --gate @<f> --pregate <f> --issues @<f> --review-loop @<f> --docs @<f> --body <body>`; `gstack-post pr-body` and `pr-title` (a new PR via the host's PR tool; a draft while a gate finding is `open`). `SHIP_RECEIPT:` opens your report.
|
||||
- Step 20: the ship metrics row via `gstack-review-log`.
|
||||
- Step 22: `gstack-ship-budget close`, then `gstack-ship-receipt result --pr <n>`: `GSTACK_RESULT: skill=ship status=complete run=<dir>`. Under a merge queue, `gstack-ship-queue ready --pr <n> [--screen|--no-screen]` at the pushed head; paste its output.
|
||||
- Step 22: `gstack-ship-budget close --reading R --credits C` (`BUDGET_FINAL:` and `BUDGET_TIMELINE:` carry tokens and credits; put both in the report), then `gstack-ship-receipt result --pr <n>`: `GSTACK_RESULT: skill=ship status=complete run=<dir>`. Under a merge queue, `gstack-ship-queue ready --pr <n> [--screen|--no-screen]` at the pushed head; paste its output.
|
||||
|
||||
## Where each STOP sends you (paths under `<root>/ship/`)
|
||||
|
||||
|
||||
@@ -6,13 +6,16 @@ The cap is an **admission cap**: `gstack-ship-budget check` admits the next step
|
||||
dispatch only when used + open reservations + unknown + its projection + the tail
|
||||
and stop reserve fit. Work already admitted can still overrun its projection.
|
||||
Field-by-field reference: docs/ship-budget.md. Exit 0 continue or delegate, 3 stop,
|
||||
2 usage; any non-zero exit means the next step does not start.
|
||||
2 usage; any non-zero exit means the next step does not start. A `ship/DRIVER.md` run starts
|
||||
`--mode observe` instead: no check, reservation or handoff; each program ends with `note`, and only a
|
||||
note past the cap stops.
|
||||
|
||||
**Meter.** Read one per command:
|
||||
- Capy: read `(await tools.thread.get({ thread_id: "<your own jam id>" })).cost.tokens` (your own jam, its
|
||||
- Capy: read `(await tools.thread.get({ thread_id: "<your own jam id>" })).cost` (your own jam, its
|
||||
subagents included; a root thread's is `"this"`) in the same program as the
|
||||
`gstack-ship-budget` command, never its own call: `--reading <n>`. `--prior-reading <n>` (per-call
|
||||
charge; three of them price steps at the run's own median) is your previous call's reading; one older than the run's last reading is ignored.
|
||||
`gstack-ship-budget` command, never its own call: `--reading <cost.tokens> --credits <cost.credits>`.
|
||||
`--prior-reading <n>` (per-call charge; three of them price steps at the run's own median) is a reading
|
||||
your previous call already took; never spend a call only to read the meter. One older than the run's last reading is ignored.
|
||||
- Claude Code: `start --session <id>`, or echo the nonce the refusal prints once and
|
||||
rerun `start --nonce <value>`; the helper reads the journals itself (never `--reading`).
|
||||
- Elsewhere: `--meter estimate` (printed `meter=estimate`), only when the owner accepts it.
|
||||
|
||||
@@ -4,13 +4,16 @@ The cap is an **admission cap**: `gstack-ship-budget check` admits the next step
|
||||
dispatch only when used + open reservations + unknown + its projection + the tail
|
||||
and stop reserve fit. Work already admitted can still overrun its projection.
|
||||
Field-by-field reference: docs/ship-budget.md. Exit 0 continue or delegate, 3 stop,
|
||||
2 usage; any non-zero exit means the next step does not start.
|
||||
2 usage; any non-zero exit means the next step does not start. A `ship/DRIVER.md` run starts
|
||||
`--mode observe` instead: no check, reservation or handoff; each program ends with `note`, and only a
|
||||
note past the cap stops.
|
||||
|
||||
**Meter.** Read one per command:
|
||||
- Capy: read `(await tools.thread.get({ thread_id: "<your own jam id>" })).cost.tokens` (your own jam, its
|
||||
- Capy: read `(await tools.thread.get({ thread_id: "<your own jam id>" })).cost` (your own jam, its
|
||||
subagents included; a root thread's is `"this"`) in the same program as the
|
||||
`gstack-ship-budget` command, never its own call: `--reading <n>`. `--prior-reading <n>` (per-call
|
||||
charge; three of them price steps at the run's own median) is your previous call's reading; one older than the run's last reading is ignored.
|
||||
`gstack-ship-budget` command, never its own call: `--reading <cost.tokens> --credits <cost.credits>`.
|
||||
`--prior-reading <n>` (per-call charge; three of them price steps at the run's own median) is a reading
|
||||
your previous call already took; never spend a call only to read the meter. One older than the run's last reading is ignored.
|
||||
- Claude Code: `start --session <id>`, or echo the nonce the refusal prints once and
|
||||
rerun `start --nonce <value>`; the helper reads the journals itself (never `--reading`).
|
||||
- Elsewhere: `--meter estimate` (printed `meter=estimate`), only when the owner accepts it.
|
||||
|
||||
@@ -392,3 +392,60 @@ describe('variant E: the review segment and the docs reader (GSTA-167 replay 5)'
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
describe('variant G: observe mode, note and credits (GSTA-167)', () => {
|
||||
// Value: protects=an observe run records each program's tokens and credits and the readers a program bracketed, reserves nothing, writes no review-log marker, and closes with the timeline; fails_when=note admits, reserves or delegates, a review-log start under observe needs a reservation, close flags the unreserved start, or the timeline drops credits;
|
||||
// why_new=observe mode and note are new; seam=gstack-review-log --start; no_claim=the host counter's accuracy
|
||||
test('note records tokens, credits and the reader pair, reserves nothing, and close prints BUDGET_TIMELINE', () => {
|
||||
const h = fresh();
|
||||
const s = start(h, ['--mode', 'observe', '--token-cap', '400M', '--reading', '1000000', '--credits', '100']);
|
||||
expect(s.r.out).not.toContain('RESERVATION');
|
||||
const six = h.sb(['note', '--run', s.run, '--step', '6', '--reading', '1200000', '--credits', '150.5']);
|
||||
expect(six.code, six.out + six.err).toBe(0);
|
||||
expect(six.out).toMatch(/^NOTE: 6 used=200000 credits=50\.5$/m);
|
||||
expect(h.field(six.out, 'BUDGET', 'verdict')).toBe('continue');
|
||||
expect(h.field(six.out, 'BUDGET', 'credits')).toBe('50.5');
|
||||
expect(h.reviewLog(['--start', 'review']).code).toBe(0);
|
||||
const pass = h.sb(['note', '--run', s.run, '--step', '9', '--label', 'dispatch:specialist,red-team', '--reading', '3200000', '--credits', '400',
|
||||
'--dispatched-at', '1200000', '--dispatched-credits', '150.5', '--readers', '2']);
|
||||
expect(pass.code, pass.out + pass.err).toBe(0);
|
||||
expect(pass.out).toMatch(/^NOTE: 9:dispatch:specialist,red-team used=2200000 credits=300 readers=2 readers_tokens=2000000 readers_credits=249\.5$/m);
|
||||
const l = h.ledger(s.ledger);
|
||||
expect(l.reservations).toEqual([]);
|
||||
expect(l.mode).toBe('observe');
|
||||
expect(fs.readFileSync(l.record, 'utf8')).toMatch(/used=2200000 credits=300 .*mode=observe/);
|
||||
const close = h.sb(['close', '--run', s.run, '--reading', '3300000', '--credits', '410']);
|
||||
expect(close.code, close.out + close.err).toBe(0);
|
||||
expect(close.out).toContain('BUDGET_FINAL: used=2300000 credits=310 reserved=0');
|
||||
expect(close.out).toContain('BUDGET_TIMELINE: 6 used=200000 credits=50.5; 9:dispatch:specialist,red-team used=2200000 credits=300 readers=2 readers_tokens=2000000 readers_credits=249.5');
|
||||
});
|
||||
|
||||
// Value: protects=the cap stays one hard stop in observe mode: a note past it stops with the resumable block, and a credits reading below the last is refused like a token reading; fails_when=observe never stops, stops below the cap, or accepts a credits regression;
|
||||
// why_new=observe mode is new; seam=none; no_claim=what a stop costs
|
||||
test('a note past the cap stops; at the cap it continues; a lower credits reading is refused', () => {
|
||||
const h = fresh();
|
||||
const s = start(h, ['--mode', 'observe', '--token-cap', '5M', '--reading', '1000000', '--credits', '100']);
|
||||
expect(h.sb(['note', '--run', s.run, '--step', '8', '--reading', '6000000', '--credits', '200']).code).toBe(0);
|
||||
const low = h.sb(['note', '--run', s.run, '--step', '9', '--reading', '6000000', '--credits', '150']);
|
||||
expect(low.code).toBe(2);
|
||||
expect(low.err).toContain('BUDGET_READING_REJECTED');
|
||||
const over = h.sb(['note', '--run', s.run, '--step', '9', '--reading', '6000001', '--credits', '210']);
|
||||
expect(over.code).toBe(3);
|
||||
expect(over.out).toContain('STOP: reason=budget_exhausted');
|
||||
expect(over.out).toContain('GSTACK_RESULT: skill=ship status=incomplete');
|
||||
expect(h.sb(['note', '--run', s.run, '--step', '12', '--reading', '6100000']).code).toBe(3);
|
||||
});
|
||||
|
||||
// Value: protects=credits ride every reading in admission mode too, so the ship/SKILL.md path reports both units; fails_when=check or settle refuse --credits, or the BUDGET line omits credits once a reading exists, or prints it before any;
|
||||
// why_new=credits are new; seam=none; no_claim=credits-to-dollars conversion
|
||||
test('admission mode takes --credits on start and check and prints credits only once one is read', () => {
|
||||
const h = fresh();
|
||||
const plain = start(h, ['--token-cap', '400M', '--reading', '1000000']);
|
||||
expect(plain.r.out).not.toMatch(/credits=/);
|
||||
const s = start(h, ['--token-cap', '400M', '--reading', '1000000', '--credits', '12.25']);
|
||||
const c = check(h, s.run, '6', '7', ['--reading', '1100000', '--credits', '20']);
|
||||
expect(c.code, c.out + c.err).toBe(0);
|
||||
expect(h.field(c.out, 'BUDGET', 'credits')).toBe('7.75');
|
||||
expect(h.sb(['note', '--run', s.run, '--step', '7', '--reading', '1200000', '--credits', 'x']).code).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -253,22 +253,22 @@ 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 reviewers on Capy'));
|
||||
const caps = capy.slice(capy.indexOf('Token caps on /ship'), capy.indexOf('/ship on Capy is about a dozen calls'));
|
||||
expect(caps.length).toBeGreaterThan(0);
|
||||
expect(caps).not.toContain('returning only that number');
|
||||
expect(caps).toMatch(/inside the same (?:execute )?program as the\s+`gstack-ship-budget` command/);
|
||||
for (const text of [capy, tmpl]) expect(text).not.toMatch(/read it again in your next call|meter-only call(?!s? (?:is|are) never)/);
|
||||
});
|
||||
// Value: protects=each review pass (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;
|
||||
// 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 note 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', () => {
|
||||
test('a review pass is one execute program: plan, review-log --start, BEFORE, create, poll, note with the pair, collect (Capy, observe mode); the admission-mode section keeps check and settle', () => {
|
||||
const capy = fs.readFileSync(path.join(import.meta.dir, '..', 'capy', 'skill', 'gstack', 'SKILL.md'), 'utf8');
|
||||
const pass = capy.slice(capy.indexOf('/ship reviewers on Capy'), capy.indexOf('## Plan, approve, ship'));
|
||||
expect(pass).toMatch(/one execute program per review pass, per Step 11 and for the Step 14\.5 audit/);
|
||||
const pass = capy.slice(capy.indexOf('/ship on Capy is about a dozen calls'), capy.indexOf('## Plan, approve, ship'));
|
||||
expect(pass).toMatch(/A review pass, Step 11 and the Step 14\.5 audit each run inside one program/);
|
||||
expect(pass).not.toMatch(/one execute program per batch/);
|
||||
expect(pass).toContain('its brief copied verbatim from the section that dispatches it');
|
||||
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 order = ['gstack-review-plan --base <base>', 'gstack-review-log --start', '`BEFORE`', 'tools.subagent.create', 'tools.subagent.get', 'gstack-ship-budget note', '--dispatched-at <BEFORE>', '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) {
|
||||
|
||||
@@ -62,6 +62,14 @@ describe('ship budget skill structure', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Value: protects=the budget section's Capy meter reads tokens and credits in the program that runs the command, never spends a call only to read the meter, and points a driver run at observe mode; fails_when=the section asks for a per-call meter read again (budget-handoff.md:12-13 at b617445: 11 meter-only calls at 286k each on #34's child), drops --credits, or loses the observe pointer;
|
||||
// why_new=variant G; seam=none; no_claim=the agent's adherence
|
||||
test('the budget section reads credits beside tokens, never spends a call on the meter, and points driver runs at observe mode', () => {
|
||||
const text = read('ship/sections/budget-handoff.md.tmpl').replace(/\s+/g, ' ');
|
||||
for (const s of ['--reading <cost.tokens> --credits <cost.credits>', 'never spend a call only to read the meter', 'A `ship/DRIVER.md` run starts `--mode observe` instead: no check, reservation or handoff; each program ends with `note`']) expect(text).toContain(s);
|
||||
expect(read('docs/ship-budget.md')).toContain('## Observe mode (ship/DRIVER.md)');
|
||||
});
|
||||
|
||||
test('every review-log --start in the dispatch sections takes a reservation when a cap is set', () => {
|
||||
for (const rel of ['ship/sections/review-army.md.tmpl', 'ship/sections/adversarial.md.tmpl']) expect(read(rel)).toContain('--reservation');
|
||||
});
|
||||
|
||||
+67
-20
@@ -11,7 +11,7 @@ import { afterAll, describe, expect, test } from 'bun:test';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { DELEGATE_SEGMENT_CALL, SHIP_STEPS } from '../lib/ship-budget';
|
||||
import { SHIP_STEPS } from '../lib/ship-budget';
|
||||
import { RESULT_CODES } from '../lib/result-codes';
|
||||
import { SHIP_DRIVER_STOPS } from '../scripts/resolvers/ship-driver';
|
||||
import { runGeneration, SHIP_DRIVER_RELPATH } from '../scripts/gen-skill-docs';
|
||||
@@ -91,10 +91,10 @@ describe('ship/DRIVER.md covers the skeleton', () => {
|
||||
expect([...codes].filter(c => !doc.includes(c) && !bins.includes(c))).toEqual([]);
|
||||
});
|
||||
|
||||
test('every gate the skeleton holds keeps a driver line: tests, pregate, receipt, push, queue, delegation', () => {
|
||||
test('every gate the skeleton holds keeps a driver line: tests, pregate, receipt, push, queue, the budget stop', () => {
|
||||
for (const line of ['ZERO-RUN', 'gstack-ship-release finish', 'gstack-pregate clear', 'One re-audit at most', 'gstack-ship-receipt docs-caps',
|
||||
'Step 14.5, every ship', 'never clean', 'gstack-ship-receipt write', '`SHIP_RECEIPT:`', 'gstack-ship-receipt result --pr <n>',
|
||||
'gstack-ship-queue ready --pr <n>', 'Never force-push', `--delegate measured`, `${DELEGATE_SEGMENT_CALL / 1000}k or more above the host floor`]) {
|
||||
'gstack-ship-queue ready --pr <n>', 'Never force-push', '--mode observe', 'the one stop check']) {
|
||||
expect(doc, line).toContain(line);
|
||||
}
|
||||
});
|
||||
@@ -102,7 +102,7 @@ describe('ship/DRIVER.md covers the skeleton', () => {
|
||||
// Value: protects=a driven /ship sends every reviewer the brief ship/SKILL.md sends (GSTA-167 no-loss round 3: shortened reviewer context lost 9 of 19 blockers); fails_when=a review phase paraphrases its section instead of running it, or the driver copies a reviewer prompt line it could let drift;
|
||||
// why_new=the coverage tests above check step ids and codes only; seam=none; no_claim=that the parent reads the section
|
||||
test('the review phases run their sections as written and the driver restates no reviewer prompt', () => {
|
||||
for (const [phase, sections] of [['## 3. Audits', ['test-coverage', 'plan-completion']], ['## 4. Review', ['review-army']], ['## 5. Adversarial', ['adversarial']]] as const) {
|
||||
for (const [phase, sections] of [['## 3. Audits and review', ['test-coverage', 'plan-completion', 'review-army']], ['## 4. Judgment', ['adversarial']]] as const) {
|
||||
const text = doc.slice(doc.indexOf(phase), doc.indexOf('\n## ', doc.indexOf(phase) + 1));
|
||||
for (const sec of sections) expect(text, phase).toContain(`\`sections/${sec}.md\``);
|
||||
expect(text, phase).toMatch(/as\s+(?:they are\s+)?written/);
|
||||
@@ -127,27 +127,74 @@ describe('one program per pass (GSTA-167 variant E)', () => {
|
||||
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', () => {
|
||||
test('the shared program lists its acts in order and the review pass, Step 11 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'];
|
||||
expect(block).toContain('Each review pass, Step 11 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>', 'gstack-review-log --start <skill>` once per row the step logs', '(3) `BEFORE`',
|
||||
'(4) one subagent per reader', '(5) a poll until every reader settles', '(6) `note` with the reading pair',
|
||||
'a later batch (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`');
|
||||
expect(block).not.toContain('--reservation');
|
||||
expect(phase('## 3. Audits and review')).toContain('and the pass (above)');
|
||||
expect(phase('## 4. Judgment')).toContain('Step 11 from `sections/adversarial.md` as written (above)');
|
||||
expect(phase('## 6. Notes and documentation')).toContain('read `sections/documentation.md` and run its audit as written, as one program (above)');
|
||||
});
|
||||
|
||||
// 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');
|
||||
// Value: protects=the parent reads the diff and TODOS.md once, through gstack-ship-preflight --audit's files, and the audit sections only when their reader is needed; fails_when=a phase sends the parent back to TODOS.md (346 KB on gstack, read twice per run at b617445) or the full diff (read three times), or reads test-coverage.md and plan-completion.md unconditionally;
|
||||
// why_new=variant G moves those reads into the Phase 2 program; seam=none; no_claim=the bytes a parent actually reads
|
||||
test('Phase 2 runs --audit and the reads in its program, and no phase reads TODOS.md or the full diff into the parent', () => {
|
||||
const p2 = phase('## 2. Preflight');
|
||||
for (const s of ['gstack-ship-preflight --audit --base <base> --out <run dir>', 'writes the diff once (`CONTEXT:`)', '(`TODOS:`)', 'READER: coverage=<needed|none> plan=<needed|none>',
|
||||
'only when its reader is `needed`', 'Never read TODOS.md or the full diff into the parent']) expect(p2).toContain(s);
|
||||
expect(phase('## 4. Judgment')).toContain('on the todos and commits files');
|
||||
expect(phase('## 6. Notes and documentation')).toContain('edited from the todos file');
|
||||
});
|
||||
});
|
||||
|
||||
describe('variant G: the call plan, observe budget, waits and audit readers (GSTA-167)', () => {
|
||||
const flat = (s: string) => s.replace(/\s+/g, ' ');
|
||||
// Value: protects=the driver is a fixed plan of one call per phase that covers every ship step, at 9 calls for one clean pass and at most 12; fails_when=a step loses its phase, a phase is split back into per-step calls, or the stated call count drifts from the phase count (GSTA-185 appendix: 195 literal / 70 batched calls at b617445);
|
||||
// why_new=the coverage tests check step ids anywhere in the driver, not which call runs them; seam=none; no_claim=the model's real call count
|
||||
test('each phase is one call, the phase headings cover every ship step, and the stated count is phases plus the report', () => {
|
||||
const headings = [...doc.matchAll(/^## (\d+)\. (.+)$/gm)];
|
||||
expect(headings.map(h => Number(h[1]))).toEqual([1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
const covered = new Set(headings.flatMap(h => [...stepsNamed(h[2]!, false)]));
|
||||
expect(SHIP_STEPS.filter(s => !covered.has(s))).toEqual([]);
|
||||
expect(doc).toContain('Each phase below is one call');
|
||||
const calls = flat(doc.slice(doc.indexOf('**Calls.**'), doc.indexOf('\n\n', doc.indexOf('**Calls.**'))));
|
||||
expect(calls).toContain(`One pass with no fixes is ${headings.length + 1} calls: Phases 1-${headings.length} and the report; at most 12`);
|
||||
});
|
||||
|
||||
// Value: protects=the ledger records instead of admitting: one note at each program's end with tokens and credits, the reader pair inside the dispatching program, and the cap as the single stop; fails_when=the driver brings back a check per boundary, a reservation, a meter-only call or a handoff (replay 5: 11 meter-only calls at 286k on #34's child), or drops the credits reading;
|
||||
// why_new=observe mode is new; seam=none; no_claim=the host's counter accuracy
|
||||
test('the budget paragraph runs observe mode: note with --reading and --credits, no check per step, the cap as the one stop', () => {
|
||||
const budget = flat(doc.slice(doc.indexOf('**Budget: observe.**'), doc.indexOf('\n\n', doc.indexOf('**Budget: observe.**'))));
|
||||
for (const s of ['gstack-ship-budget start --mode observe', 'gstack-ship-budget note --step <last step> --reading R --credits C', '`cost.credits`',
|
||||
'No check per step, no reservation, no meter-only call, no handoff', '--dispatched-at <R> --dispatched-credits <C> --readers <n>']) expect(budget).toContain(s);
|
||||
expect(doc).not.toMatch(/check --step|--next dispatch:|--delegate measured/);
|
||||
expect(doc).toContain('gstack-ship-budget close --reading R --credits C');
|
||||
});
|
||||
|
||||
// Value: protects=no call waits past the 5-minute prompt cache: programs return within 240 s and the next call resumes the wait, with the choice of a ping over a waiting child stated where the parent and Capy read it; fails_when=a phase waits in one program until slow work ends (cache probe: a call after a 7-minute gap cost ~2,820 credits against ~70), the cadence goes back to 270 s (16 s from the cache lifetime), or the reason is dropped;
|
||||
// why_new=wait hygiene is new; seam=none; no_claim=the host's cache lifetime
|
||||
test('waits ping every 240 s in the driver and the Capy skill, and both say why a waiting child is not used', () => {
|
||||
const waits = flat(doc.slice(doc.indexOf('**Waits.**'), doc.indexOf('\n\n', doc.indexOf('**Waits.**'))));
|
||||
for (const s of ['No call waits past 240 s', 'the next call resumes', 'A small waiting child saves nothing', 'Past 100 minutes']) expect(waits).toContain(s);
|
||||
const capy = read('capy/skill/gstack/SKILL.md');
|
||||
for (const s of ['never let 5 minutes pass between calls', '`duration_seconds: 240`', 'A small waiting child saves nothing']) expect(capy).toContain(s);
|
||||
for (const text of [doc, capy]) expect(text).not.toMatch(/\b270 ?s\b/);
|
||||
});
|
||||
|
||||
// Value: protects=audits that cannot block (Steps 7, 8, 9.2.1, 14.5) run on a small model at low effort with their sections' own briefs, and skip their reader when --audit decides them; fails_when=an audit reader inherits the lane's model and effort (#36: the coverage audit alone was 2.45M at Opus), or an audit brief is paraphrased;
|
||||
// why_new=audit model routing is new; seam=none; no_claim=the audits' recall
|
||||
test('audit readers run on a small model at low effort, named for Capy, and Phase 3 dispatches only what READER names', () => {
|
||||
expect(flat(doc)).toContain('Audit readers (Steps 7, 8, 9.2.1, 14.5) cannot block, so each runs on a small model at low effort; their findings go to the PR body');
|
||||
expect(flat(doc)).toContain('`Coverage:` and Step 8\'s line come from `--audit` when no reader ran');
|
||||
const capy = read('capy/skill/gstack/SKILL.md');
|
||||
expect(capy).toContain('model: { model_id: "anthropic/claude-sonnet-5-5", reasoning_mode: "low" }');
|
||||
expect(capy).toContain('`anthropic/claude-haiku-5-5` also fits');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -182,7 +229,7 @@ describe('shape', () => {
|
||||
expect(capy).toContain(`For \`/${s}\` driven from Capy, read \`${s}/DRIVER.md\` in the checkout (\`~/.local/share/gstack/${s}/DRIVER.md\`)`);
|
||||
expect(capy).toContain(`A checkout without that file predates it; follow its \`${s}/SKILL.md\` instead`);
|
||||
}
|
||||
expect(capy).toContain('--delegate measured');
|
||||
expect(capy).toContain('--mode observe');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -157,3 +157,38 @@ describe('gstack-ship-preflight', () => {
|
||||
expect(f.run(PREFLIGHT, ['--base', 'main', '--lane', 'nolabel']).status).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
// lib/ship-audit.ts: the --audit mode's logic.
|
||||
describe('gstack-ship-preflight --audit (GSTA-167 variant G)', () => {
|
||||
// Value: protects=one call writes the diff and the matching TODOS.md items to files the parent reads instead of TODOS.md and the diff, maps changed runtime files to tests, and names the audit readers still needed; fails_when=the diff or TODOS.md comes back whole, an untested runtime file is called covered, a changed test is missed, or a changed docs/designs/ plan is not offered;
|
||||
// why_new=the --audit mode is new (GSTA-185: TODOS.md read in full twice and the diff three times per run); seam=git, gstack-slug; no_claim=coverage quality beyond file names
|
||||
test('writes diff and todos files, maps coverage, and prints READER for the coverage and plan audits', () => {
|
||||
const todos = '# TODOS\n\n## Ship\n- fix budget rounding in src/budget.ts\n- an unrelated item\n\n## Other\n- nothing here\n';
|
||||
const f = fx({ 'TODOS.md': todos }, { 'src/budget.ts': 'export const budget = 1;\n', 'src/other.ts': 'export const o = 1;\n', 'test/budget.test.ts': "import { budget } from '../src/budget';\n" });
|
||||
const out = path.join(f.root, 'audit');
|
||||
const r = f.run(PREFLIGHT, ['--audit', '--base', 'main', '--out', out], { HOME: f.root });
|
||||
expect(r.status, r.stdout + r.stderr).toBe(0);
|
||||
expect(lineOf(r.stdout, 'CONTEXT')).toMatch(new RegExp(`^CONTEXT: diff=${out}/diff.patch bytes=\\d+ files=3 commits=1 `));
|
||||
expect(fs.readFileSync(path.join(out, 'diff.patch'), 'utf8')).toContain('+export const o = 1;');
|
||||
expect(lineOf(r.stdout, 'TODOS')).toMatch(/^TODOS: bytes=\d+ hits=1 file=/);
|
||||
expect(fs.readFileSync(path.join(out, 'todos.md'), 'utf8')).toBe('## Ship\n- fix budget rounding in src/budget.ts\n');
|
||||
expect(r.stdout).not.toContain('an unrelated item');
|
||||
expect(r.stdout).toContain('COVERAGE_MAP: src/budget.ts changed_tests=1 referencing_tests=1');
|
||||
expect(r.stdout).toContain('COVERAGE_MAP: src/other.ts changed_tests=0 referencing_tests=0');
|
||||
expect(r.stdout).toContain('Coverage: 1 of 2 changed runtime files have no changed test naming them (src/other.ts)');
|
||||
expect(r.stdout).toContain('Plan completion audit: not run (no plan is bound to this branch');
|
||||
expect(lineOf(r.stdout, 'READER')).toBe('READER: coverage=needed plan=none');
|
||||
});
|
||||
|
||||
// Value: protects=a branch whose runtime files all have changed tests skips the coverage reader, and a changed docs/designs/ plan asks for the plan reader; fails_when=the coverage reader runs on a fully tested diff, or a branch's design doc is not a candidate;
|
||||
// why_new=the --audit mode is new; seam=git; no_claim=that the plan is the right one
|
||||
test('a fully tested diff needs no coverage reader; a changed design doc needs the plan reader', () => {
|
||||
const f = fx({}, { 'src/budget.ts': 'export const budget = 1;\n', 'test/budget.test.ts': "import { budget } from '../src/budget';\n", 'docs/designs/budget.md': '# Plan\n' });
|
||||
const r = f.run(PREFLIGHT, ['--audit', '--base', 'main', '--out', path.join(f.root, 'audit')], { HOME: f.root });
|
||||
expect(r.status, r.stdout + r.stderr).toBe(0);
|
||||
expect(r.stdout).toContain('Coverage: deterministic (each of 1 changed runtime files has a changed test naming it; no audit reader)');
|
||||
expect(r.stdout).toContain('PLAN_CANDIDATE: docs/designs/budget.md');
|
||||
expect(lineOf(r.stdout, 'READER')).toBe('READER: coverage=none plan=needed');
|
||||
expect(f.run(PREFLIGHT, ['--audit', '--base', 'main']).status).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"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)"
|
||||
"bytes": 1446,
|
||||
"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); variant G: budget-handoff.md points driver runs at observe mode and reads credits beside tokens (+243)"
|
||||
}
|
||||
Reference in new issue
Block a user