bareloop 0.32.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/userrun.js CHANGED
@@ -72,11 +72,13 @@ import { answerReviewDoor, doorRecordOf, doorAgeGate } from './reviewdoor.js';
72
72
  // the cold reset, shared with the battery drivers so "cold" has one spelling
73
73
  import { coldReset, moveStaleGateAudit } from './u-patient.js';
74
74
  // PANEL-BUILD.md P1 — the one run list (`~/.config/bareloop/runs.jsonl`).
75
- import { appendRun } from './runlist.js';
75
+ import { appendRun, appendRunEvent, isLiveRunner } from './runlist.js';
76
76
  import { keysForDoor } from './keysfile.js';
77
- import { checkMonthlyRoom, monthlyRefusalText } from './monthly.js';
77
+ import { parseJsonl } from './replayio.js';
78
+ import { claimRun, monthlyRefusalText, legSpend } from './monthly.js';
78
79
  import { ConfigError } from './config.js';
79
- import { applyConfiguredKey, keyNameFor, rowsForHome } from './providerrows.js';
80
+ import { commandFor } from './invoke.js';
81
+ import { applyConfiguredKey, judgeRatesFor, keyNameFor, priceReadout, ratesFor, rowsForHome } from './providerrows.js';
80
82
  // the banner's wall arithmetic, extracted so it is reachable by a test (F83): the
81
83
  // end-of-run readout sits past the approval gate, so nothing could ever drive it here
82
84
  import { wallLine, doomedResume, deathAtOf, evidencePackage, doorLines, resumeAtLines, reviewDoorPackage, runDoorLines, tokensLine, doorTimingRedLines } from './u-readout.js';
@@ -225,6 +227,8 @@ class ExitSignal extends Error {
225
227
  * @property {Record<string,string|undefined>} [env]
226
228
  * @property {string} [keysHome] P4a — a test seam naming the home whose `.env` keys file is loaded
227
229
  * even when `env` is injected; production never sets it.
230
+ * @property {string} [invokedAs] the command this run was actually reached through (`src/invoke.js`);
231
+ * `src/cli.js` passes `'bareloop run-u'`, `scripts/run-u.mjs` passes none.
228
232
  * @property {(s: string) => void} [out]
229
233
  * @property {(s: string) => void} [err]
230
234
  * @property {any} [provider] the TEST SEAM: supplying this skips the real-key
@@ -237,6 +241,21 @@ class ExitSignal extends Error {
237
241
  * `os.homedir()`-based default.
238
242
  */
239
243
 
244
+ /**
245
+ * The one-runner door's hand-in (`src/bundlerun.js`, the exported bundle's `bareloop run`):
246
+ * where a bundle run's books live and how it prints its own re-invocation. Set ONLY by
247
+ * that caller through `startRun`/`resumeRun` opts, in-process — `main`'s argv never builds
248
+ * it, so a local job cannot reach it. Absent, every use below is today's expression.
249
+ * @typedef {object} BundleRun
250
+ * @property {string} runid the run id (also the `runs/<runid>` directory name)
251
+ * @property {string} runDir `<bundle>/runs/<runid>`: spine.jsonl, gate-audit.jsonl and close/ live here
252
+ * @property {string} invoke the bare re-invocation prefix printed in resume hints (`bareloop run <bundle> --repo <path>`), no ceiling flags
253
+ * @property {{budgetUsd?: number, wallMin?: number}} tightened the ceilings the person tightened on THIS leg; printed after `invoke` on every resume line except a money halt's (which never prints the exhausted `--budget`)
254
+ * @property {number} signedBudgetUsd the bundle's SIGNED spec budgetUsd (the tightened leg ceiling is `spec.budgetUsd`); printed only, never enforced from here
255
+ * @property {string} printApprove the hash printed after `--approve` in those hints (the bundle's, not the run spec's)
256
+ * @property {() => void} prepareTree creates the run's tree (fresh worktree) and its books; the engine calls it once every $0 refusal has passed, so a refusal leaks nothing
257
+ */
258
+
240
259
  /**
241
260
  * @typedef {object} RunOpts
242
261
  * @property {any} [spec] the parsed, not-yet-validated job spec — read by
@@ -264,6 +283,7 @@ class ExitSignal extends Error {
264
283
  * @property {boolean} [reviewDoor] opt in to a green-class run opening a review door
265
284
  * @property {string} [selector] how the re-invocation commands this prints should name the job (`--job x` / `--spec <path>`)
266
285
  * @property {string} [specDesc] how a "go edit the spec" hint should read
286
+ * @property {BundleRun} [bundle] set only by the bundle door (see `BundleRun`)
267
287
  * @property {Deps} [deps]
268
288
  */
269
289
 
@@ -289,6 +309,7 @@ class ExitSignal extends Error {
289
309
  * @property {string|null} door
290
310
  * @property {string|null} decide
291
311
  * @property {string|null} text
312
+ * @property {BundleRun} [bundle]
292
313
  * @property {Deps} deps
293
314
  */
294
315
 
@@ -335,6 +356,12 @@ async function execute(ctx) {
335
356
  * @returns {never} */
336
357
  const die = (m) => { err(m); throw new ExitSignal(2); };
337
358
  let exitCode = 0;
359
+ // Set once a monthly claim has appended this run's row: {runid, spine}. The catch below is the ONE
360
+ // owner of "a claimed run that never began" — every exit between the claim and the spine's first
361
+ // record (a refused resume patient, a throwing prepareTree, a git/reset failure...) passes there.
362
+ // Set on the no-limit path too, once the row is appended there (the same "listed, no spine yet" state).
363
+ /** @type {{ runid: string, spine: string, home: string|undefined }|null} */
364
+ let claimedRow = null;
338
365
  try {
339
366
  const spec = ctx.spec;
340
367
  const specPath = ctx.specPath;
@@ -454,10 +481,20 @@ async function execute(ctx) {
454
481
  * dropping the incomplete flag would silently turn a floor into an exact
455
482
  * figure on the next leg's own job-start). */
456
483
  const DRAFT_TAIL = DRAFT_SPENT_USD > 0 ? ` --draft-spent-usd ${DRAFT_SPENT_USD}${DRAFT_SPEND_INCOMPLETE ? ' --draft-spend-incomplete' : ''}` : '';
484
+ /** the registry the run was pointed at, as flags — every re-invocation this script PRINTS carries it
485
+ * (a paste that dropped it would green with no registry row). Empty when no --registry was named. */
486
+ const REGISTRY_TAIL = (ctx.registry ?? null) !== null ? ` --registry ${(ctx.registry ?? null)} --workflow ${(ctx.workflow ?? null) ?? spec.job}` : '';
457
487
 
458
488
  const WORKDIR = target.workdir;
459
489
  const SEED = target.seed;
460
490
  const specHash = jobSpecHash(spec);
491
+ // the re-invocation commands this engine prints: a bundle names its own door and hash,
492
+ // every other caller gets today's byte-identical strings.
493
+ const WALL_FLAG = ctx.bundle?.tightened.wallMin === undefined ? '' : ` --wall ${ctx.bundle.tightened.wallMin}`;
494
+ const INVOKE = ctx.bundle
495
+ ? `${ctx.bundle.invoke}${ctx.bundle.tightened.budgetUsd === undefined ? '' : ` --budget ${ctx.bundle.tightened.budgetUsd}`}${WALL_FLAG}`
496
+ : `${commandFor('run-u', deps.invokedAs)} ${SELECTOR}`;
497
+ const PRINT_APPROVE = ctx.bundle?.printApprove ?? specHash;
461
498
  // MODEL (build-list #3, hamr's GO 2026-08-30): the signed spec's `model`, if
462
499
  // present, wins outright; a --model flag naming a DIFFERENT id is refused
463
500
  // (never silently overridden) — resolveWorkerModel is the one decision, pure
@@ -610,7 +647,7 @@ async function execute(ctx) {
610
647
  * both the RESUME reader below and the live run further down need them. Two spellings of
611
648
  * one path is how `--resume` comes to read a different directory than the run writes. */
612
649
  const wd = resolve(WORKDIR);
613
- const spineDir = join(wd, '..', target.spine);
650
+ const spineDir = ctx.bundle ? ctx.bundle.runDir : join(wd, '..', target.spine);
614
651
  /** `--resume` takes the runid (the conventional `<spine dir>/u-<runid>.jsonl`) or an
615
652
  * explicit path — the path form is what makes these gates testable without touching
616
653
  * an operator's real spine directory. */
@@ -652,17 +689,11 @@ async function execute(ctx) {
652
689
  /** @type {any} */
653
690
  let watchdog = null;
654
691
  if (existsSync(wdFile)) { try { watchdog = JSON.parse(readFileSync(wdFile, 'utf8')); } catch { /* an unreadable report is not evidence of life */ } }
655
- if (Number.isInteger(watchdog?.pid)) {
656
- let alive = true;
657
- try { process.kill(watchdog.pid, 0); } catch { alive = false; }
658
- if (alive) {
659
- let cmdline = null;
660
- try { cmdline = readFileSync(`/proc/${watchdog.pid}/cmdline`, 'utf8').replace(/\0/g, ' ').trim(); } catch { /* no /proc, or not ours */ }
661
- if (cmdline === null || cmdline.includes('run-u.mjs') || cmdline.includes('u-watchdog.mjs')) {
662
- die(`--resume: pid ${watchdog.pid} from ${deadSpineFile} is still alive${cmdline ? ` (${cmdline.slice(0, 120)})` : ' and this process cannot read its command line'}. Two processes on one patient is unrecoverable — stop it first.`);
663
- }
664
- err(`--resume: pid ${watchdog.pid} is alive but is NOT this runner (${cmdline.slice(0, 80)}) — the pid was recycled; continuing.`);
665
- }
692
+ // ONE owner of "is that pid a live bareloop runner" (src/runlist.js `isLiveRunner`, shared with the
693
+ // monthly limit's holds): alive AND a bareloop runner refuses; a recycled pid that now runs some
694
+ // other program does not. Where /proc is unreadable an existing pid is assumed to be ours.
695
+ if (Number.isInteger(watchdog?.pid) && isLiveRunner(watchdog.pid)) {
696
+ die(`--resume: pid ${watchdog.pid} from ${deadSpineFile} is still alive (a bareloop runner). Two processes on one patient is unrecoverable — stop it first.`);
666
697
  }
667
698
  // WHEN did the dead leg stop? The watchdog's kill record is later, better evidence
668
699
  // than the last spine event for a run that was KILLED — and worse evidence for one
@@ -916,8 +947,7 @@ async function execute(ctx) {
916
947
  // and the printed commands carry the registry forward, because an accept aimed
917
948
  // at no registry is the `no-row-for-run` refusal one hop later.
918
949
  const previewHeld = doorRecord?.quarantined === true && heldRowFor((ctx.registry ?? null), (ctx.workflow ?? null) ?? spec.job, DOOR);
919
- const previewRegistry = (ctx.registry ?? null) !== null ? ` --registry ${(ctx.registry ?? null)} --workflow ${(ctx.workflow ?? null) ?? spec.job}` : '';
920
- const doorInvoke = (/** @type {string} */ tail) => ` node scripts/run-u.mjs ${SELECTOR} --door ${DOOR}${tail}${previewRegistry} --approve ${specHash}`;
950
+ const doorInvoke = (/** @type {string} */ tail) => ` ${INVOKE} --door ${DOOR}${tail}${REGISTRY_TAIL} --approve ${PRINT_APPROVE}`;
921
951
  out('');
922
952
  if (RULING === null) {
923
953
  for (const l of runDoorLines({
@@ -943,12 +973,30 @@ async function execute(ctx) {
943
973
  throw new ExitSignal((ctx.approve ?? null) === null ? 0 : 1);
944
974
  }
945
975
 
976
+ // THE CUSTOMER'S OWN PRICE (hamr 2026-09-30) — the worker's key row may carry `priceInPerM` /
977
+ // `priceOutPerM` (USD per 1M tokens); `ratesFor` is the one lookup, on the SAME row `keyNameFor` picks
978
+ // the key from. Resolved ONCE here, at $0, before the preview, the monthly claim and any worktree or
979
+ // spine, then handed down as data (`runJob`'s `rates`) — nothing deep in the run re-reads config. A bad
980
+ // price refuses here; it never falls back to the built-in guess. No price set = `null` = no `rates` key
981
+ // passed anywhere, byte-identical to before. The judge's own row is resolved below, beside its key.
982
+ const keyCfg = cfgOn ? rowsForHome(cfgHome) : [];
983
+ /** @type {ReturnType<typeof ratesFor>} */
984
+ let workerPrice = null;
985
+ try {
986
+ workerPrice = ratesFor(spec.provider, spec.baseUrl, keyCfg, spec.model);
987
+ } catch (e) {
988
+ if (!(e instanceof ConfigError)) throw e;
989
+ err(`${e.message} — refusing to start rather than guess a price. Nothing spent.`);
990
+ throw new ExitSignal(2);
991
+ }
992
+
946
993
  if ((ctx.approve ?? null) !== specHash) {
947
994
  out(dead ? 'U — RESUME, continuing a halted run, REAL dollars' : 'U — user-mode e2e, ONE run, REAL dollars');
948
995
  out(` spec ${SPEC_DESC} $${spec.budgetUsd} wall ${WALL_LABEL} strikeLimit=${STRIKE_LIMIT} (step ladder + close-fix progress rule)`);
949
996
  out(` patient ${WORKDIR} @ ${SEED.slice(0, 12)}`);
950
997
  out(` shim ${READ_SHIM_LABEL}`);
951
998
  out(` scout ${SCOUT_LABEL}`);
999
+ if (workerPrice) out(` price ${priceReadout(workerPrice)}`);
952
1000
  out(` goal "${spec.goal}"`);
953
1001
  // F87 — the goal must state everything the close will judge, and nothing derives
954
1002
  // one from the other or checks them against each other. So the only defence is
@@ -1099,9 +1147,9 @@ async function execute(ctx) {
1099
1147
  // different provider (e.g. openai-api/DeepSeek) printed the WRONG variable
1100
1148
  // name to set: a person pasting it verbatim hit a $0 refusal naming the
1101
1149
  // right key only by accident of the runner's own generic error message,
1102
- // never from this hint. `providerEntry.envKey` is the same resolved name
1103
- // the real key check at launch (`:1157-1158` below) reads.
1104
- const invoke = (/** @type {string} */ tail) => ` ${providerEntry.envKey}=... node scripts/run-u.mjs ${SELECTOR}${dead ? ` --resume ${RESUME}` : ''}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL}${tail} --approve ${specHash}`;
1150
+ // never from this hint. `keyNameFor` is the same resolved name
1151
+ // the real key check at launch reads (the person's row variable, else the provider's built-in one).
1152
+ const invoke = (/** @type {string} */ tail) => ` ${keyNameFor(spec.provider, spec.baseUrl, keyCfg, spec.model).name}=... ${INVOKE}${dead ? ` --resume ${RESUME}` : ''}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL}${REGISTRY_TAIL}${tail} --approve ${PRINT_APPROVE}`;
1105
1153
  /** the door the operator has already picked, as flags — hoisted out of the else
1106
1154
  * below so the inhibitor line at the bottom can print the WHOLE command rather
1107
1155
  * than a shape the operator has to assemble. Empty on an ordinary run and on the
@@ -1228,7 +1276,7 @@ async function execute(ctx) {
1228
1276
  } else {
1229
1277
  out(` costs nothing, in any state — no work, no money, no allowance moved`);
1230
1278
  out(` keeps ${PAUSE_TTL_MS / 86_400_000} days from the door on the record; after that it expires on its own, which is all "cancel" ever meant`);
1231
- out(` reopen node scripts/run-u.mjs ${SELECTOR} --door ${DOOR} --approve ${specHash}`);
1279
+ out(` reopen ${INVOKE} --door ${DOOR}${REGISTRY_TAIL} --approve ${PRINT_APPROVE}`);
1232
1280
  }
1233
1281
  throw new ExitSignal(0);
1234
1282
  }
@@ -1266,8 +1314,8 @@ async function execute(ctx) {
1266
1314
  out(`\nPAUSED BY YOU — nothing was run and nothing was spent. The checkpoint stands exactly as it was: the work is on the run's own branch, the plan and the money are where the paused leg left them.`);
1267
1315
  out(` keeps ${PAUSE_TTL_MS / 86_400_000} days from the pause on the record — after that the checkpoint expires on its own, and nothing has to be decided today to let that happen`);
1268
1316
  out(' resume the SAME runid, whenever you want, with the door you pick then:');
1269
- out(` node scripts/run-u.mjs ${SELECTOR} --resume ${RESUME}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL} --decide accept --approve ${specHash}`);
1270
- out(` node scripts/run-u.mjs ${SELECTOR} --resume ${RESUME}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL} --decide rerun --text "<what you want done differently>" --approve ${specHash}`);
1317
+ out(` ${INVOKE} --resume ${RESUME}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL}${REGISTRY_TAIL} --decide accept --approve ${PRINT_APPROVE}`);
1318
+ out(` ${INVOKE} --resume ${RESUME}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL}${REGISTRY_TAIL} --decide rerun --text "<what you want done differently>" --approve ${PRINT_APPROVE}`);
1271
1319
  out(` read the same command with no --decide re-prints the evidence package you just looked at`);
1272
1320
  throw new ExitSignal(0);
1273
1321
  }
@@ -1348,6 +1396,20 @@ async function execute(ctx) {
1348
1396
  // into runJob below whether this leg constructs a real judge provider or not.
1349
1397
  const judge = resolveJudge({ specJudge: spec.judge, workerProvider: spec.provider, workerModel: MODEL });
1350
1398
  const judgeEntry = resolveProvider(judge.provider);
1399
+ // …and the judge's price, from the judge MODEL's row (`judgeRatesFor`): on the worker's endpoint when the
1400
+ // judge is the same provider (falling back to the worker's price when the model names no row), else the
1401
+ // row for the judge's own provider. Only a judging close ever calls it, so only then can its row refuse the run.
1402
+ /** @type {ReturnType<typeof ratesFor>} */
1403
+ let judgePrice = null;
1404
+ if (JUDGES) {
1405
+ try {
1406
+ judgePrice = judgeRatesFor(judge, spec.provider, spec.baseUrl, workerPrice, keyCfg);
1407
+ } catch (e) {
1408
+ if (!(e instanceof ConfigError)) throw e;
1409
+ err(`${e.message} — refusing to start rather than guess a price. Nothing spent.`);
1410
+ throw new ExitSignal(2);
1411
+ }
1412
+ }
1351
1413
  // undefined, not null: both come straight off `env[...]` lookups below, which
1352
1414
  // (like process.env) never produce null for a missing key.
1353
1415
  /** @type {string|undefined} */
@@ -1355,7 +1417,7 @@ async function execute(ctx) {
1355
1417
  /** @type {string|undefined} */
1356
1418
  let judgeApiKey;
1357
1419
  // TEST SEAM (constraint: `deps.provider` skips the real-key check entirely,
1358
- // the same shape src/cli.js's `doRun` already uses) — a caller that hands in
1420
+ // the same shape src/bundlerun.js's door uses) — a caller that hands in
1359
1421
  // its own provider IS the run, so nothing here needs a real secret.
1360
1422
  if (!deps.provider) {
1361
1423
  // P4b — the Settings row (keys file line) whose API shape + Base URL match this spec
@@ -1363,7 +1425,6 @@ async function execute(ctx) {
1363
1425
  // row = the built-in. Names only — values come from the
1364
1426
  // env / keys file. The judge follows the worker's choice when it is the same provider
1365
1427
  // (the same rule that gives it the worker's baseUrl below).
1366
- const keyCfg = cfgOn ? rowsForHome(cfgHome) : [];
1367
1428
  const workerEnv = applyConfiguredKey(env, spec.provider, spec.baseUrl, keyCfg, spec.model);
1368
1429
  const judgeEnv = judge.provider === spec.provider ? workerEnv : applyConfiguredKey(env, judge.provider, undefined, keyCfg, judge.model);
1369
1430
  /** the variable NAME this worker's key is read from (the person's pick, else built-in) — for messages only */
@@ -1403,22 +1464,37 @@ async function execute(ctx) {
1403
1464
  }
1404
1465
  }
1405
1466
 
1406
- // THE MONTHLY $ LIMIT (PANEL-BUILD.md P4a, hamr 2026-09-29: "drop time keep money") — the
1407
- // ONE run-start seam. Every run this door starts (the CLI's `bareloop run-u` and the panel,
1408
- // which spawns it) passes here after the signature and the key gates and BEFORE the run
1409
- // list row, the patient reset or any token: a cap larger than what is left this month does
1410
- // not start, and nothing is spent. This leg's exposure is the whole signed cap on a cold
1411
- // start (it covers drafting + run) and only the REMAINDER on a resume/door-rerun, whose
1412
- // earlier spend is already in the month's total (a resume's killed leg counts its real spend only,
1413
- // never its unspent cap — `resumingSpine` — since the remainder asked for here covers that). No limit set = no check. An unreadable
1414
- // config.json refuses too — a broken instrument never silently reads "no limit".
1467
+ // THE MONTHLY $ LIMIT (PANEL-BUILD.md P4a; hamr 2026-09-30: "whoever runs first claims the limit and
1468
+ // holds it") — the ONE run-start seam. Every run this door starts (the CLI's `bareloop run-u`, `bareloop
1469
+ // run <bundle>` and the panel, which spawns them) passes here after the signature and the key gates and
1470
+ // BEFORE the patient reset or any token: the run CLAIMS its leg cap — it appends its own run-list row
1471
+ // FIRST (carrying its `pid` and `capUsd`, the hold), then reads the list: only the claims above its row
1472
+ // count. A cap larger than what is left is released (a `released` entry) and does not start; nothing is spent. This leg's exposure
1473
+ // is the whole signed cap on a cold start (it covers drafting + run) and only the REMAINDER on a
1474
+ // resume/door-rerun, whose earlier spend is already in the month's total (the killed leg's claim is
1475
+ // closed at its floor, with a note, once its process is gone). No limit set = no claim (the row is listed as
1476
+ // ever). An unreadable config.json, or a claim row that cannot be written, refuses too — a broken instrument
1477
+ // never silently reads "no limit".
1478
+ const runid = ctx.bundle?.runid ?? Date.now().toString(36);
1479
+ const spineFile = ctx.bundle ? join(spineDir, 'spine.jsonl') : join(spineDir, `u-${runid}.jsonl`);
1480
+ const foldedUsd = dead ? dead.restart.priorSpentUsd : (doorPrior?.spentUsd ?? 0);
1481
+ const legCapUsd = Math.max(0, spec.budgetUsd - (typeof foldedUsd === 'number' && Number.isFinite(foldedUsd) ? foldedUsd : 0));
1482
+ // every row carries its runner's pid and the leg cap, limit set or not: a limit set LATER still holds it
1483
+ /** @type {import('./runlist.js').RunRow} */
1484
+ const runRow = {
1485
+ at: new Date().toISOString(), runid, job: spec.job, spine: spineFile, patient: wd, via: ctx.bundle ? 'bundle' : 'run-u', pid: process.pid, capUsd: legCapUsd,
1486
+ };
1487
+ let claimed = false;
1415
1488
  {
1416
- const foldedUsd = dead ? dead.restart.priorSpentUsd : (doorPrior?.spentUsd ?? 0);
1417
- const legCapUsd = Math.max(0, spec.budgetUsd - (typeof foldedUsd === 'number' && Number.isFinite(foldedUsd) ? foldedUsd : 0));
1418
1489
  /** @type {string|null} */
1419
1490
  let refusal = null;
1420
1491
  try {
1421
- if (cfgOn) refusal = monthlyRefusalText(checkMonthlyRoom({ capUsd: legCapUsd, home: cfgHome, resumingSpine: deadSpineFile }));
1492
+ if (cfgOn) {
1493
+ const got = claimRun({ row: runRow, capUsd: legCapUsd, home: cfgHome });
1494
+ claimed = got.claimed;
1495
+ if (claimed) claimedRow = { runid, spine: spineFile, home: cfgHome };
1496
+ refusal = monthlyRefusalText(got.room);
1497
+ }
1422
1498
  } catch (e) {
1423
1499
  if (!(e instanceof ConfigError)) throw e;
1424
1500
  err(`${e.message} — refusing to start rather than guess the monthly limit. Nothing spent.`);
@@ -1433,9 +1509,12 @@ async function execute(ctx) {
1433
1509
 
1434
1510
  // `wd`/`spineDir` are derived once, above the resume reader that needs them; only the
1435
1511
  // directory's CREATION belongs here, after the preview/approval gates have exited.
1512
+ // A bundle's tree and books are created HERE and not earlier: every $0 refusal above
1513
+ // (keys, monthly limit, param guards) leaves no worktree behind.
1514
+ ctx.bundle?.prepareTree();
1436
1515
  mkdirSync(spineDir, { recursive: true });
1437
- const runid = Date.now().toString(36);
1438
- const spineFile = join(spineDir, `u-${runid}.jsonl`);
1516
+ const closeDir = ctx.bundle ? join(spineDir, 'close') : spineDir;
1517
+ if (ctx.bundle) mkdirSync(closeDir, { recursive: true });
1439
1518
 
1440
1519
  // PANEL-BUILD.md P1 — one row in the run list, BEFORE any paid call (this
1441
1520
  // leg's own `runJob` further down, and BEFORE the provider-key checks that
@@ -1449,10 +1528,14 @@ async function execute(ctx) {
1449
1528
  // carries everything else through (env/out/err/provider/…): a caller that
1450
1529
  // injects it writes this leg's row under a temp home instead of the real
1451
1530
  // `~/.config/bareloop`; production never sets it.
1531
+ // When the monthly claim above already appended this run's row, there is nothing to add.
1452
1532
  try {
1453
- appendRun({
1454
- at: new Date().toISOString(), runid, job: spec.job, spine: spineFile, patient: wd, via: 'run-u',
1455
- }, { home: deps.runlistHome });
1533
+ if (!claimed) {
1534
+ appendRun(runRow, { home: deps.runlistHome });
1535
+ // no limit set: the row is appended here, not by a claim — mark it so the execute catch releases it
1536
+ // the same way if the run exits before its spine has a first record (it lives in `deps.runlistHome`)
1537
+ claimedRow = { runid, spine: spineFile, home: deps.runlistHome };
1538
+ }
1456
1539
  } catch (/** @type {any} */ e) {
1457
1540
  err(`WARNING: could not add this run to ~/.config/bareloop/runs.jsonl (${e.message}) — the run continues; the panel's list will be missing this row.`);
1458
1541
  }
@@ -1498,13 +1581,13 @@ async function execute(ctx) {
1498
1581
  if (preFile) out(`gate audit — a stale file was already sitting in the patient tree (not this run's); moved aside to ${preFile}`);
1499
1582
  }
1500
1583
 
1501
- const approvals = [{ specHash, signer: env.USER ?? 'human', ts: new Date().toISOString() }];
1584
+ const approvals = [{ specHash, signer: ctx.bundle ? 'bundle' : (env.USER ?? 'human'), ts: new Date().toISOString() }];
1502
1585
  // spec.baseUrl (PRD item 28, ruling (d), 2026-09-09: admitted in v1) — an
1503
1586
  // OpenAI-compatible gateway/endpoint. Undefined for anthropic-api jobs and
1504
1587
  // for an openai-api job that names none (bare-agent's OpenAIProvider then
1505
1588
  // defaults to api.openai.com/v1 on its own).
1506
1589
  const baseUrl = typeof spec.baseUrl === 'string' ? spec.baseUrl : undefined;
1507
- // TEST SEAM — same shape as src/cli.js's `doRun`: a caller that supplies
1590
+ // TEST SEAM — same shape as the bundle door (src/bundlerun.js): a caller that supplies
1508
1591
  // `deps.provider` skips the real-key check above entirely and this
1509
1592
  // construction never runs (a scripted provider IS the run).
1510
1593
  let provider = deps.provider;
@@ -1678,9 +1761,9 @@ async function execute(ctx) {
1678
1761
  // containing `%20` that does not exist, and the guard dies at startup on the one run
1679
1762
  // it was meant to protect. Same spelling the sibling scripts use.
1680
1763
  const watchdog = spawn(process.execPath, [
1681
- // PANEL-BUILD.md P0 — this engine now lives in src/, one directory over
1682
- // from scripts/u-watchdog.mjs (it used to be a same-directory sibling).
1683
- fileURLToPath(new URL('../scripts/u-watchdog.mjs', import.meta.url)),
1764
+ // src/u-watchdog.mjs ships in the package (F205: it used to live in scripts/,
1765
+ // which the tarball omits, so an installed run spawned a guard that died at startup).
1766
+ fileURLToPath(new URL('./u-watchdog.mjs', import.meta.url)),
1684
1767
  '--spine', spineFile,
1685
1768
  '--pid', String(process.pid),
1686
1769
  '--stale-ms', String(worstCloseSilenceMs + 600_000),
@@ -1736,7 +1819,9 @@ async function execute(ctx) {
1736
1819
  // used (never a second `makeSpine(spineFile)` here: two independent
1737
1820
  // emitters against one file would both start their seq counter at 0 and
1738
1821
  // collide the moment either one had already written a record).
1739
- approvals, workdir: wd, provider, providerFor, judgeProvider, judgeModel: judge.model, emit,
1822
+ approvals, workdir: wd, provider, providerFor, judgeProvider, judgeModel: judge.model,
1823
+ // the customer's own price per key row (USD per 1K), or null = the built-in guess as before
1824
+ rates: workerPrice?.rates ?? null, judgeRates: judgePrice?.rates ?? null, emit,
1740
1825
  // F133 (run mtqwmb9l) — `closeTimeoutMs` is deliberately NOT passed here.
1741
1826
  // `RESOLVED_CLOSE_TIMEOUT_MS` above exists only to size the outside
1742
1827
  // watchdog before it spawns; feeding it back into `runJob` used to make
@@ -1746,7 +1831,7 @@ async function execute(ctx) {
1746
1831
  // and announces the ceiling itself now — the ONE banner/spine record
1747
1832
  // for this run.
1748
1833
  shellCapUsd: spec.budgetUsd, capRuns: CAP_RUNS, strikeLimit: STRIKE_LIMIT,
1749
- closeDir: spineDir,
1834
+ closeDir,
1750
1835
  readShim: READ_SHIM,
1751
1836
  scout: SCOUT,
1752
1837
  draftSpentUsd: DRAFT_SPENT_USD,
@@ -1824,8 +1909,18 @@ async function execute(ctx) {
1824
1909
  // ── the read. Facts only: what happened, what it cost, and whether the record is
1825
1910
  // honest. No classification into pass/fail buckets — one run classifies nothing.
1826
1911
  const raw = readFileSync(spineFile, 'utf8');
1827
- const events = raw.trimEnd().split('\n').filter(Boolean).map((l) => JSON.parse(l));
1912
+ // tolerant: a torn/garbled line is skipped, not a crash (the shipped bundle-run fix, now the one engine read)
1913
+ const events = parseJsonl(spineFile).records;
1828
1914
  const je = events.findLast((e) => e.type === 'job-end');
1915
+ // The claim's WRITE-BACK (hamr 2026-09-30): when the run ends it settles its own claim on the monthly
1916
+ // limit — its final spend, attributed to itself. Best-effort: a run that dies before here is settled
1917
+ // (`process gone`) by the next run that reads the list (file order: a claim yields only to the rows above it).
1918
+ if (je) {
1919
+ try {
1920
+ const fin = legSpend(events);
1921
+ appendRunEvent({ runid, type: 'settled', by: runid, spentUsd: fin.usd, spendComplete: fin.complete, at: new Date().toISOString() }, { home: cfgHome });
1922
+ } catch (/** @type {any} */ e) { err(`WARNING: could not settle this run's claim in ~/.config/bareloop/runs.jsonl (${e.message}) — the next run settles it once this process is gone.`); }
1923
+ }
1829
1924
  // the ONE spelling of the text-side scan (src/validate.js) — a hand-rolled copy
1830
1925
  // here would be the ninth, and one that misses a shape leaks on the very output
1831
1926
  // it guards. Only the COUNT is ever read out; the matches themselves stay here.
@@ -1835,7 +1930,7 @@ async function execute(ctx) {
1835
1930
 
1836
1931
  const auditSrc = join(wd, 'gate-audit.jsonl');
1837
1932
  let auditFile = null;
1838
- if (existsSync(auditSrc)) { auditFile = join(spineDir, `u-${runid}-gate-audit.jsonl`); renameSync(auditSrc, auditFile); }
1933
+ if (existsSync(auditSrc)) { auditFile = ctx.bundle ? join(spineDir, 'gate-audit.jsonl') : join(spineDir, `u-${runid}-gate-audit.jsonl`); renameSync(auditSrc, auditFile); }
1839
1934
  const audit = auditFile ? readFileSync(auditFile, 'utf8').trimEnd().split('\n').filter(Boolean).map((l) => JSON.parse(l)) : [];
1840
1935
  const writes = audit.filter((e) => e.decision === 'allow' && (e.action?.type === 'write' || e.action?.type === 'edit'));
1841
1936
 
@@ -1858,6 +1953,7 @@ async function execute(ctx) {
1858
1953
  // THIS LEG's own tokens, off THIS LEG's own spine — same scope as `rounds` above it,
1859
1954
  // never the folded chain (see `tokensLine`'s doc for why worker-result is excluded).
1860
1955
  out(`tokens ${tokensLine({ events })}`);
1956
+ if (workerPrice) out(`price ${priceReadout(workerPrice)}`);
1861
1957
  out(`writes ${writes.length} allowed (${new Set(writes.map((e) => e.action?.path)).size} distinct files)`);
1862
1958
  out(`plan ${plan ? `${plan.steps?.length ?? '?'} ${plan.steps?.length === 1 ? 'step' : 'steps'}` : 'none validated'}`);
1863
1959
  out(`checks ${events.filter((e) => e.type === 'check-run').length} runs · menu [${events.find((e) => e.type === 'check-menu')?.offered?.join(', ') ?? '-'}]`);
@@ -1883,10 +1979,28 @@ async function execute(ctx) {
1883
1979
  if (mh) {
1884
1980
  out(`\nMONEY HALT — the cap cut the run at $${mh.remainingUsd?.toFixed?.(4) ?? '?'} left of $${mh.budgetUsd}. The verdict already minted STANDS: ${mh.verdict ?? 'unknown'}${mh.stage ? ` at stage "${mh.stage}"` : ''}.`);
1885
1981
  out(` trend ${mh.trend} — ${mh.reading}`);
1886
- out(` lever ${mh.lever}`);
1887
- for (const o of mh.options ?? []) out(` · ${o}`);
1888
- out(` resume node scripts/run-u.mjs ${SELECTOR} --resume ${runid}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL} --approve <the NEW hash after you edit budgetUsd>`);
1889
- out(' (the top-up is yours to sign — nothing in the run may widen its own budget)');
1982
+ if (ctx.bundle) {
1983
+ // a bundle's spec is manifest-hashed: there is no budgetUsd to edit and no new hash to sign. The room that
1984
+ // is left is the signed ceiling, and the ONLY way to use it is the person typing a larger --budget. The
1985
+ // exhausted --budget is never echoed (pasted as printed it halts again at once: the chain folds prior spend),
1986
+ // and nothing is filled in for them.
1987
+ const signed = ctx.bundle.signedBudgetUsd;
1988
+ const spentUsd = typeof mh.budgetUsd === 'number' && typeof mh.remainingUsd === 'number' ? mh.budgetUsd - mh.remainingUsd : null;
1989
+ const spentTxt = spentUsd === null ? '?' : `${mh.spendComplete === false ? '≥' : ''}$${spentUsd.toFixed(4)}`;
1990
+ const lever = mh.trend === 'converging' ? 'the work was still converging when the money ran out; more money may finish it'
1991
+ : mh.trend === 'flat' ? 'more money is unlikely to help; this bundle\'s goal is signed and cannot be revised here'
1992
+ : 'read the last close output before deciding whether more money is worth it';
1993
+ out(` lever ${lever}`);
1994
+ out(` · choose a larger --budget on the resume line below (the room left is the signed $${signed}; it cannot be raised past that)`);
1995
+ out(' · abandon the task');
1996
+ out(` resume ${ctx.bundle.invoke}${WALL_FLAG} --resume ${runid}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL}${REGISTRY_TAIL} --budget <more than ${spentTxt}, at most $${signed}> --approve ${PRINT_APPROVE}`);
1997
+ out(' (type the --budget number yourself — nothing here fills it in, so this line does not run as printed; nothing in the run may widen its own budget)');
1998
+ } else {
1999
+ out(` lever ${mh.lever}`);
2000
+ for (const o of mh.options ?? []) out(` · ${o}`);
2001
+ out(` resume ${INVOKE} --resume ${runid}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL}${REGISTRY_TAIL} --approve <the NEW hash after you edit budgetUsd>`);
2002
+ out(' (the top-up is yours to sign — nothing in the run may widen its own budget)');
2003
+ }
1890
2004
  }
1891
2005
  // A STALL is a checkpoint too (hamr's go, 2026-08-13). Its own escalation prints one
1892
2006
  // line above and says *"retry the run"*, and until this line the only retry on offer
@@ -1895,7 +2009,7 @@ async function execute(ctx) {
1895
2009
  // the hash already approved is the hash that resumes.
1896
2010
  if (outcome === 'step-stalled') {
1897
2011
  out('\nSTALL HALT — the model stopped producing rounds and reissuing the call did not recover it. The tree, the plan and the steps already finished STAND.');
1898
- out(` resume node scripts/run-u.mjs ${SELECTOR} --resume ${runid}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL} --approve ${specHash}`);
2012
+ out(` resume ${INVOKE} --resume ${runid}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL}${REGISTRY_TAIL} --approve ${PRINT_APPROVE}`);
1899
2013
  out(' (no spec edit, so the hash is unchanged — this re-enters at the stalled step and re-pays for none of the ones before it)');
1900
2014
  out(' (if the allowance is what actually ran out underneath the stall, that preview says so and refuses — it is read there, not asserted here)');
1901
2015
  }
@@ -1916,7 +2030,7 @@ async function execute(ctx) {
1916
2030
  out(` died ${total === null ? 'before a plan was accepted — nothing paid is re-payable'
1917
2031
  : done >= total ? `at the close — all ${total} step(s) finished`
1918
2032
  : `in step ${done + 1} of ${total}`}`);
1919
- out(` resume node scripts/run-u.mjs ${SELECTOR} --resume ${runid}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL} --approve ${specHash}`);
2033
+ out(` resume ${INVOKE} --resume ${runid}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL}${REGISTRY_TAIL} --approve ${PRINT_APPROVE}`);
1920
2034
  out(' (no spec edit, so the hash is unchanged — this re-enters at the recorded step and re-pays for none of the ones before it)');
1921
2035
  out(' (if the allowance is what actually ran out underneath the transport fault, that preview says so and refuses — it is read there, not asserted here)');
1922
2036
  }
@@ -1937,7 +2051,7 @@ async function execute(ctx) {
1937
2051
  ]);
1938
2052
  out(' clock STOPPED — the wall does not run while a person is reading (W-2), and this leg\'s elapsed is what folds into the resume');
1939
2053
  out('');
1940
- const answer = (/** @type {string} */ tail) => `node scripts/run-u.mjs ${SELECTOR} --resume ${runid}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL}${tail} --approve ${specHash}`;
2054
+ const answer = (/** @type {string} */ tail) => `${INVOKE} --resume ${runid}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL}${REGISTRY_TAIL}${tail} --approve ${PRINT_APPROVE}`;
1941
2055
  for (const l of doorLines({
1942
2056
  rerun: answer(' --decide rerun --text "<what you want done differently>"'),
1943
2057
  accept: answer(' --decide accept'),
@@ -2059,7 +2173,7 @@ async function execute(ctx) {
2059
2173
  // against has to travel in the printed command — an accept aimed at no registry (or at
2060
2174
  // a different one) is the `no-row-for-run` refusal all over again, one hop later.
2061
2175
  const doorRegistry = REGISTRY_ROW.minted ? ` --registry ${(ctx.registry ?? null)} --workflow ${REGISTRY_ROW.write.name}` : '';
2062
- const answerDoor = (/** @type {string} */ tail) => `node scripts/run-u.mjs ${SELECTOR} --door ${runid}${tail}${doorRegistry} --approve ${specHash}`;
2176
+ const answerDoor = (/** @type {string} */ tail) => `${INVOKE} --door ${runid}${tail}${doorRegistry} --approve ${PRINT_APPROVE}`;
2063
2177
  // THE PROMISE READS OFF THE ROW, not off the class. `quarantined` on the door record
2064
2178
  // says this run's credit is HELD; whether there is anything to RELEASE is a second
2065
2179
  // question, and the honest answer is "only if a row was minted". Without `--registry`
@@ -2090,7 +2204,7 @@ async function execute(ctx) {
2090
2204
  }
2091
2205
 
2092
2206
  // the BRIDGE: on a green the agent's own plan is kept as the reusable artifact
2093
- if (outcome === 'green' && plan && !leaks.length) {
2207
+ if (outcome === 'green' && plan && !leaks.length && !ctx.bundle) {
2094
2208
  // one file PER GREEN, never one file per job: two cold runs of this job produced
2095
2209
  // two DIFFERENT plans that both green (run 1: 3 steps, run 3: 2 steps), so a
2096
2210
  // single `bridge-<job>.json` silently destroys the earlier bridge. WHICH bridge
@@ -2152,6 +2266,13 @@ async function execute(ctx) {
2152
2266
 
2153
2267
  return exitCode;
2154
2268
  } catch (/** @type {any} */ e) {
2269
+ // A claimed run that exited before its spine has a first record never started: it spent nothing and
2270
+ // owns no spine, so its row is released (folded out of the list, like a monthly refusal) rather than
2271
+ // left as a ghost that keeps the month reading "at least". Once the spine exists the run is a real,
2272
+ // listed run and ends by its own job-end. A failed release write never masks the original exit.
2273
+ if (claimedRow && !existsSync(claimedRow.spine)) {
2274
+ try { appendRunEvent({ runid: claimedRow.runid, type: 'released', by: claimedRow.runid, reason: 'not started', at: new Date().toISOString() }, { home: claimedRow.home }); } catch { /* the exit stands; a stranded row of a dead process is closed by the next run */ }
2275
+ }
2155
2276
  if (e instanceof ExitSignal) return e.code;
2156
2277
  throw e;
2157
2278
  }
@@ -2184,6 +2305,7 @@ function buildCtx(mode, opts) {
2184
2305
  door: mode.door,
2185
2306
  decide: mode.decide,
2186
2307
  text: mode.text,
2308
+ ...(opts.bundle === undefined ? {} : { bundle: opts.bundle }),
2187
2309
  deps: opts.deps ?? {},
2188
2310
  };
2189
2311
  }
@@ -2293,7 +2415,7 @@ export async function main(argv, deps = {}) {
2293
2415
  if (!existsSync(specPath)) {
2294
2416
  die(`--job ${jobArg}: there is no spec at ${specPath}.\n`
2295
2417
  + ' The table row is the runner\'s half of a job (patient, seed, spine); the SPEC is yours — authored through\n'
2296
- + ` scripts/run-interview.mjs and scripts/run-author.mjs, then signed. Nothing here can stand in for it: a job\n`
2418
+ + ` ${commandFor('interview', deps.invokedAs)} and ${commandFor('author', deps.invokedAs)}, then signed. Nothing here can stand in for it: a job\n`
2297
2419
  + ' with no spec has no goal, no close, no budget and no hash to approve.');
2298
2420
  }
2299
2421
  try { spec = JSON.parse(readFileSync(specPath, 'utf8')); } catch (e) {
@@ -2305,7 +2427,7 @@ export async function main(argv, deps = {}) {
2305
2427
  specPath = resolve(/** @type {string} */ (specArg));
2306
2428
  if (!existsSync(specPath)) {
2307
2429
  die(`--spec ${specPath} does not exist — a run needs the resolved-spec.json an authoring session actually wrote `
2308
- + '(scripts/run-author.mjs\'s own `out/resolved-spec.json`), never a path nobody authored');
2430
+ + `(${commandFor('author', deps.invokedAs)}'s own \`out/resolved-spec.json\`), never a path nobody authored`);
2309
2431
  }
2310
2432
  let raw;
2311
2433
  try { raw = readFileSync(specPath, 'utf8'); } catch (e) {
@@ -2345,8 +2467,8 @@ export async function main(argv, deps = {}) {
2345
2467
  if (candidates.length === 0) {
2346
2468
  die(`--spec ${specPath}: no prepared copy beside it — looked for a source-*/ directory carrying source.json in `
2347
2469
  + `${dirname(specPath)} and found none. A --spec run reads its workdir and seed off the copy the authoring `
2348
- + 'session prepared; it is never typed or guessed. Prepare one first: node scripts/run-interview.mjs (or '
2349
- + 'run-author.mjs directly against a --source) before running this spec.');
2470
+ + `session prepared; it is never typed or guessed. Prepare one first: ${commandFor('interview', deps.invokedAs)} (or `
2471
+ + `${commandFor('author', deps.invokedAs)} directly against a --source) before running this spec.`);
2350
2472
  }
2351
2473
  if (candidates.length > 1) {
2352
2474
  die(`--spec ${specPath}: ${candidates.length} prepared copies sit beside it (${candidates.join(', ')}) — `
@@ -535,12 +535,18 @@ export function makeCostBook({ ceilingUsd, onCall }?: {
535
535
  * an estimate, and it is why tool mode does not cost double what text mode did.
536
536
  *
537
537
  * `cacheMessages` (F18) and `maxTokens` (F30) are not optional on this stack.
538
- * @param {any} provider @param {{system?: string, maxTokens?: number}} [o]
538
+ * `rates` is the customer's own price for this provider's key row (USD per 1K, `ratesFor`);
539
+ * absent = bare-agent's built-in guess, exactly as before.
540
+ * @param {any} provider @param {{system?: string, maxTokens?: number, rates?: {in: number, out: number}|null}} [o]
539
541
  * @returns {(messages: any[], tools: any[], opts?: any) => Promise<any>}
540
542
  */
541
- export function makeLoopGenerate(provider: any, { system, maxTokens }?: {
543
+ export function makeLoopGenerate(provider: any, { system, maxTokens, rates }?: {
542
544
  system?: string;
543
545
  maxTokens?: number;
546
+ rates?: {
547
+ in: number;
548
+ out: number;
549
+ } | null;
544
550
  }): (messages: any[], tools: any[], opts?: any) => Promise<any>;
545
551
  /**
546
552
  * ONE structured ask, with the malformed-emission retry bounded on its OWN axis.
@@ -82,6 +82,7 @@ export function refuseLockedKind(kind: string): Refusal;
82
82
  * verdictType?: string|null,
83
83
  * questions?: Record<string|number, string>|null, generate?: Function, provider?: any,
84
84
  * seedRef?: string|null, scout?: any, listing?: any, ceilingUsd?: number|null, judgeModel?: string|null,
85
+ * rates?: {in: number, out: number}|null,
85
86
  * onPhase?: (phase: string, data?: any) => void,
86
87
  * onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void,
87
88
  * seedFn?: Function, scoutFn?: Function, listingFn?: Function,
@@ -94,7 +95,7 @@ export function refuseLockedKind(kind: string): Refusal;
94
95
  * closeDecl: any, seedRef: string|null, authoring: any, judged: any, reds: Red[],
95
96
  * stop: string|null, cost: any, interview: any, confirmed: any}>}
96
97
  */
97
- export function authorCloseForJob({ answers, repoPath, lang, verdictType, questions, generate, provider, seedRef, scout, listing, ceilingUsd, writeScope, judgeModel, onPhase, onCall, seedFn, scoutFn, listingFn, authorFn, authorOpts, signerFix, proposeFn, compileOpts, ask, confirmGenerate, isRepo, langResult, }: {
98
+ export function authorCloseForJob({ answers, repoPath, lang, verdictType, questions, generate, provider, seedRef, scout, listing, ceilingUsd, rates, writeScope, judgeModel, onPhase, onCall, seedFn, scoutFn, listingFn, authorFn, authorOpts, signerFix, proposeFn, compileOpts, ask, confirmGenerate, isRepo, langResult, }: {
98
99
  answers: Record<string | number, any>;
99
100
  repoPath?: string | null;
100
101
  lang: string;
@@ -107,6 +108,10 @@ export function authorCloseForJob({ answers, repoPath, lang, verdictType, questi
107
108
  listing?: any;
108
109
  ceilingUsd?: number | null;
109
110
  judgeModel?: string | null;
111
+ rates?: {
112
+ in: number;
113
+ out: number;
114
+ } | null;
110
115
  onPhase?: (phase: string, data?: any) => void;
111
116
  onCall?: (call: {
112
117
  label: string;
@@ -93,13 +93,14 @@ export function defaultSurveyor({ workdir, granted, auditPath, ctx }: {
93
93
  * rounds?: number, minBytes?: number, blobMax?: number, maxTokens?: number, ctx?: boolean,
94
94
  * attempts?: number, ceilingUsd?: number|null, callTimeoutMs?: number,
95
95
  * onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void,
96
- * createLoop?: (o: {system: string, policy: any, onLlmResult: any, provider: any}) => any,
96
+ * createLoop?: (o: {system: string, policy: any, onLlmResult: any, provider: any, rates?: {in: number, out: number}}) => any,
97
+ * rates?: {in: number, out: number}|null,
97
98
  * createSurveyor?: (o: {workdir: string, granted: readonly string[], ctx: boolean}) => Promise<any>}} o
98
99
  * @returns {Promise<Survey & {raw: string, raws: ReturnType<typeof scrubRaw>[],
99
100
  * calls: {label: string, costUsd: number|null, unpricedRounds: number}[],
100
101
  * budgetStop: 'cap-halt'|'pricing-red'|null}>}
101
102
  */
102
- export function runAuthorScout({ workdir, provider, rounds, minBytes, blobMax, maxTokens, ctx, attempts, ceilingUsd, callTimeoutMs, onCall, createLoop, createSurveyor, }: {
103
+ export function runAuthorScout({ workdir, provider, rounds, minBytes, blobMax, maxTokens, ctx, attempts, ceilingUsd, callTimeoutMs, onCall, createLoop, rates, createSurveyor, }: {
103
104
  workdir: string;
104
105
  provider?: any;
105
106
  rounds?: number;
@@ -120,7 +121,15 @@ export function runAuthorScout({ workdir, provider, rounds, minBytes, blobMax, m
120
121
  policy: any;
121
122
  onLlmResult: any;
122
123
  provider: any;
124
+ rates?: {
125
+ in: number;
126
+ out: number;
127
+ };
123
128
  }) => any;
129
+ rates?: {
130
+ in: number;
131
+ out: number;
132
+ } | null;
124
133
  createSurveyor?: (o: {
125
134
  workdir: string;
126
135
  granted: readonly string[];