bareloop 0.31.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/CHANGELOG.md +151 -0
- package/README.md +4 -1
- package/bareloop.context.md +232 -85
- package/bin/bareloop.mjs +0 -1
- package/package.json +1 -1
- package/src/authorflow.js +5 -3
- package/src/authorjob.js +3 -1
- package/src/authorrun.js +49 -11
- package/src/authorscout.js +10 -6
- package/src/bundlerun.js +266 -0
- package/src/cli.js +35 -357
- package/src/config.js +135 -0
- package/src/interviewrun.js +13 -4
- package/src/invoke.js +22 -0
- package/src/judged.js +5 -3
- package/src/keysfile.js +156 -0
- package/src/ledger.js +26 -0
- package/src/monthly.js +422 -0
- package/src/panel/authorroutes.js +61 -16
- package/src/panel/authorsession.js +46 -28
- package/src/panel/index.html +287 -13
- package/src/panel/server.js +58 -51
- package/src/panel/settingsroutes.js +230 -0
- package/src/planrun.js +7 -3
- package/src/providerrows.js +301 -0
- package/src/providers.js +3 -1
- package/src/run.js +7 -2
- package/src/runlist.js +92 -7
- package/src/source.js +8 -3
- package/src/u-watchdog.mjs +296 -0
- package/src/userrun.js +230 -54
- package/types/authorflow.d.ts +8 -2
- package/types/authorjob.d.ts +6 -1
- package/types/authorrun.d.ts +5 -0
- package/types/authorscout.d.ts +11 -2
- package/types/bundlerun.d.ts +26 -0
- package/types/cli.d.ts +2 -1
- package/types/config.d.ts +48 -0
- package/types/interviewrun.d.ts +5 -0
- package/types/invoke.d.ts +8 -0
- package/types/judged.d.ts +5 -1
- package/types/keysfile.d.ts +90 -0
- package/types/ledger.d.ts +14 -0
- package/types/monthly.d.ts +227 -0
- package/types/panel/authorroutes.d.ts +4 -2
- package/types/panel/authorsession.d.ts +7 -15
- package/types/panel/server.d.ts +10 -3
- package/types/panel/settingsroutes.d.ts +14 -0
- package/types/planrun.d.ts +13 -1
- package/types/providerrows.d.ts +229 -0
- package/types/run.d.ts +14 -1
- package/types/runlist.d.ts +66 -3
- package/types/source.d.ts +8 -3
- package/types/userrun.d.ts +54 -0
package/src/userrun.js
CHANGED
|
@@ -72,7 +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
|
+
import { keysForDoor } from './keysfile.js';
|
|
77
|
+
import { parseJsonl } from './replayio.js';
|
|
78
|
+
import { claimRun, monthlyRefusalText, legSpend } from './monthly.js';
|
|
79
|
+
import { ConfigError } from './config.js';
|
|
80
|
+
import { commandFor } from './invoke.js';
|
|
81
|
+
import { applyConfiguredKey, judgeRatesFor, keyNameFor, priceReadout, ratesFor, rowsForHome } from './providerrows.js';
|
|
76
82
|
// the banner's wall arithmetic, extracted so it is reachable by a test (F83): the
|
|
77
83
|
// end-of-run readout sits past the approval gate, so nothing could ever drive it here
|
|
78
84
|
import { wallLine, doomedResume, deathAtOf, evidencePackage, doorLines, resumeAtLines, reviewDoorPackage, runDoorLines, tokensLine, doorTimingRedLines } from './u-readout.js';
|
|
@@ -219,6 +225,10 @@ class ExitSignal extends Error {
|
|
|
219
225
|
/**
|
|
220
226
|
* @typedef {object} Deps
|
|
221
227
|
* @property {Record<string,string|undefined>} [env]
|
|
228
|
+
* @property {string} [keysHome] P4a — a test seam naming the home whose `.env` keys file is loaded
|
|
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.
|
|
222
232
|
* @property {(s: string) => void} [out]
|
|
223
233
|
* @property {(s: string) => void} [err]
|
|
224
234
|
* @property {any} [provider] the TEST SEAM: supplying this skips the real-key
|
|
@@ -231,6 +241,21 @@ class ExitSignal extends Error {
|
|
|
231
241
|
* `os.homedir()`-based default.
|
|
232
242
|
*/
|
|
233
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
|
+
|
|
234
259
|
/**
|
|
235
260
|
* @typedef {object} RunOpts
|
|
236
261
|
* @property {any} [spec] the parsed, not-yet-validated job spec — read by
|
|
@@ -258,6 +283,7 @@ class ExitSignal extends Error {
|
|
|
258
283
|
* @property {boolean} [reviewDoor] opt in to a green-class run opening a review door
|
|
259
284
|
* @property {string} [selector] how the re-invocation commands this prints should name the job (`--job x` / `--spec <path>`)
|
|
260
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`)
|
|
261
287
|
* @property {Deps} [deps]
|
|
262
288
|
*/
|
|
263
289
|
|
|
@@ -283,6 +309,7 @@ class ExitSignal extends Error {
|
|
|
283
309
|
* @property {string|null} door
|
|
284
310
|
* @property {string|null} decide
|
|
285
311
|
* @property {string|null} text
|
|
312
|
+
* @property {BundleRun} [bundle]
|
|
286
313
|
* @property {Deps} deps
|
|
287
314
|
*/
|
|
288
315
|
|
|
@@ -306,9 +333,19 @@ class ExitSignal extends Error {
|
|
|
306
333
|
*/
|
|
307
334
|
async function execute(ctx) {
|
|
308
335
|
const deps = ctx.deps ?? {};
|
|
309
|
-
const env = deps.env ?? process.env;
|
|
310
336
|
const out = deps.out ?? ((/** @type {string} */ s) => { console.log(s); });
|
|
311
337
|
const err = deps.err ?? ((/** @type {string} */ s) => { console.error(s); });
|
|
338
|
+
// P4a item 1 — the keys file (`~/.config/bareloop/.env`) fills what the shell leaves
|
|
339
|
+
// unset; the shell wins. An injected `deps.env` skips the file unless `deps.keysHome`
|
|
340
|
+
// names a home (a test never reads the real one). Values go only to provider
|
|
341
|
+
// construction below.
|
|
342
|
+
const keys = keysForDoor(deps);
|
|
343
|
+
const env = keys.env;
|
|
344
|
+
if (keys.warning && deps.env === undefined) err(`WARNING: ${keys.warning}`);
|
|
345
|
+
// config.json (monthly limit, chosen key variables) lives beside the run list. An injected
|
|
346
|
+
// `env` with no injected home means a test — the real ~/.config/bareloop is never read then.
|
|
347
|
+
const cfgHome = keys.home ?? deps.runlistHome;
|
|
348
|
+
const cfgOn = cfgHome !== undefined;
|
|
312
349
|
/** every operator/config stop this engine makes, in one exit code — thrown,
|
|
313
350
|
* never `process.exit()`'d, so this stays a library function (constraint:
|
|
314
351
|
* "never process.exit() inside the library — return an exit code").
|
|
@@ -319,6 +356,12 @@ async function execute(ctx) {
|
|
|
319
356
|
* @returns {never} */
|
|
320
357
|
const die = (m) => { err(m); throw new ExitSignal(2); };
|
|
321
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;
|
|
322
365
|
try {
|
|
323
366
|
const spec = ctx.spec;
|
|
324
367
|
const specPath = ctx.specPath;
|
|
@@ -438,10 +481,20 @@ async function execute(ctx) {
|
|
|
438
481
|
* dropping the incomplete flag would silently turn a floor into an exact
|
|
439
482
|
* figure on the next leg's own job-start). */
|
|
440
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}` : '';
|
|
441
487
|
|
|
442
488
|
const WORKDIR = target.workdir;
|
|
443
489
|
const SEED = target.seed;
|
|
444
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;
|
|
445
498
|
// MODEL (build-list #3, hamr's GO 2026-08-30): the signed spec's `model`, if
|
|
446
499
|
// present, wins outright; a --model flag naming a DIFFERENT id is refused
|
|
447
500
|
// (never silently overridden) — resolveWorkerModel is the one decision, pure
|
|
@@ -594,7 +647,7 @@ async function execute(ctx) {
|
|
|
594
647
|
* both the RESUME reader below and the live run further down need them. Two spellings of
|
|
595
648
|
* one path is how `--resume` comes to read a different directory than the run writes. */
|
|
596
649
|
const wd = resolve(WORKDIR);
|
|
597
|
-
const spineDir = join(wd, '..', target.spine);
|
|
650
|
+
const spineDir = ctx.bundle ? ctx.bundle.runDir : join(wd, '..', target.spine);
|
|
598
651
|
/** `--resume` takes the runid (the conventional `<spine dir>/u-<runid>.jsonl`) or an
|
|
599
652
|
* explicit path — the path form is what makes these gates testable without touching
|
|
600
653
|
* an operator's real spine directory. */
|
|
@@ -636,17 +689,11 @@ async function execute(ctx) {
|
|
|
636
689
|
/** @type {any} */
|
|
637
690
|
let watchdog = null;
|
|
638
691
|
if (existsSync(wdFile)) { try { watchdog = JSON.parse(readFileSync(wdFile, 'utf8')); } catch { /* an unreadable report is not evidence of life */ } }
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
try { cmdline = readFileSync(`/proc/${watchdog.pid}/cmdline`, 'utf8').replace(/\0/g, ' ').trim(); } catch { /* no /proc, or not ours */ }
|
|
645
|
-
if (cmdline === null || cmdline.includes('run-u.mjs') || cmdline.includes('u-watchdog.mjs')) {
|
|
646
|
-
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.`);
|
|
647
|
-
}
|
|
648
|
-
err(`--resume: pid ${watchdog.pid} is alive but is NOT this runner (${cmdline.slice(0, 80)}) — the pid was recycled; continuing.`);
|
|
649
|
-
}
|
|
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.`);
|
|
650
697
|
}
|
|
651
698
|
// WHEN did the dead leg stop? The watchdog's kill record is later, better evidence
|
|
652
699
|
// than the last spine event for a run that was KILLED — and worse evidence for one
|
|
@@ -900,8 +947,7 @@ async function execute(ctx) {
|
|
|
900
947
|
// and the printed commands carry the registry forward, because an accept aimed
|
|
901
948
|
// at no registry is the `no-row-for-run` refusal one hop later.
|
|
902
949
|
const previewHeld = doorRecord?.quarantined === true && heldRowFor((ctx.registry ?? null), (ctx.workflow ?? null) ?? spec.job, DOOR);
|
|
903
|
-
const
|
|
904
|
-
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}`;
|
|
905
951
|
out('');
|
|
906
952
|
if (RULING === null) {
|
|
907
953
|
for (const l of runDoorLines({
|
|
@@ -927,12 +973,30 @@ async function execute(ctx) {
|
|
|
927
973
|
throw new ExitSignal((ctx.approve ?? null) === null ? 0 : 1);
|
|
928
974
|
}
|
|
929
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
|
+
|
|
930
993
|
if ((ctx.approve ?? null) !== specHash) {
|
|
931
994
|
out(dead ? 'U — RESUME, continuing a halted run, REAL dollars' : 'U — user-mode e2e, ONE run, REAL dollars');
|
|
932
995
|
out(` spec ${SPEC_DESC} $${spec.budgetUsd} wall ${WALL_LABEL} strikeLimit=${STRIKE_LIMIT} (step ladder + close-fix progress rule)`);
|
|
933
996
|
out(` patient ${WORKDIR} @ ${SEED.slice(0, 12)}`);
|
|
934
997
|
out(` shim ${READ_SHIM_LABEL}`);
|
|
935
998
|
out(` scout ${SCOUT_LABEL}`);
|
|
999
|
+
if (workerPrice) out(` price ${priceReadout(workerPrice)}`);
|
|
936
1000
|
out(` goal "${spec.goal}"`);
|
|
937
1001
|
// F87 — the goal must state everything the close will judge, and nothing derives
|
|
938
1002
|
// one from the other or checks them against each other. So the only defence is
|
|
@@ -1083,9 +1147,9 @@ async function execute(ctx) {
|
|
|
1083
1147
|
// different provider (e.g. openai-api/DeepSeek) printed the WRONG variable
|
|
1084
1148
|
// name to set: a person pasting it verbatim hit a $0 refusal naming the
|
|
1085
1149
|
// right key only by accident of the runner's own generic error message,
|
|
1086
|
-
// never from this hint. `
|
|
1087
|
-
// the real key check at launch (
|
|
1088
|
-
const invoke = (/** @type {string} */ tail) => ` ${
|
|
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}`;
|
|
1089
1153
|
/** the door the operator has already picked, as flags — hoisted out of the else
|
|
1090
1154
|
* below so the inhibitor line at the bottom can print the WHOLE command rather
|
|
1091
1155
|
* than a shape the operator has to assemble. Empty on an ordinary run and on the
|
|
@@ -1212,7 +1276,7 @@ async function execute(ctx) {
|
|
|
1212
1276
|
} else {
|
|
1213
1277
|
out(` costs nothing, in any state — no work, no money, no allowance moved`);
|
|
1214
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`);
|
|
1215
|
-
out(` reopen
|
|
1279
|
+
out(` reopen ${INVOKE} --door ${DOOR}${REGISTRY_TAIL} --approve ${PRINT_APPROVE}`);
|
|
1216
1280
|
}
|
|
1217
1281
|
throw new ExitSignal(0);
|
|
1218
1282
|
}
|
|
@@ -1250,8 +1314,8 @@ async function execute(ctx) {
|
|
|
1250
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.`);
|
|
1251
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`);
|
|
1252
1316
|
out(' resume the SAME runid, whenever you want, with the door you pick then:');
|
|
1253
|
-
out(`
|
|
1254
|
-
out(`
|
|
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}`);
|
|
1255
1319
|
out(` read the same command with no --decide re-prints the evidence package you just looked at`);
|
|
1256
1320
|
throw new ExitSignal(0);
|
|
1257
1321
|
}
|
|
@@ -1332,6 +1396,20 @@ async function execute(ctx) {
|
|
|
1332
1396
|
// into runJob below whether this leg constructs a real judge provider or not.
|
|
1333
1397
|
const judge = resolveJudge({ specJudge: spec.judge, workerProvider: spec.provider, workerModel: MODEL });
|
|
1334
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
|
+
}
|
|
1335
1413
|
// undefined, not null: both come straight off `env[...]` lookups below, which
|
|
1336
1414
|
// (like process.env) never produce null for a missing key.
|
|
1337
1415
|
/** @type {string|undefined} */
|
|
@@ -1339,25 +1417,34 @@ async function execute(ctx) {
|
|
|
1339
1417
|
/** @type {string|undefined} */
|
|
1340
1418
|
let judgeApiKey;
|
|
1341
1419
|
// TEST SEAM (constraint: `deps.provider` skips the real-key check entirely,
|
|
1342
|
-
// the same shape src/
|
|
1420
|
+
// the same shape src/bundlerun.js's door uses) — a caller that hands in
|
|
1343
1421
|
// its own provider IS the run, so nothing here needs a real secret.
|
|
1344
1422
|
if (!deps.provider) {
|
|
1423
|
+
// P4b — the Settings row (keys file line) whose API shape + Base URL match this spec
|
|
1424
|
+
// supplies the key VARIABLE, standing in for the provider's built-in one; no matching
|
|
1425
|
+
// row = the built-in. Names only — values come from the
|
|
1426
|
+
// env / keys file. The judge follows the worker's choice when it is the same provider
|
|
1427
|
+
// (the same rule that gives it the worker's baseUrl below).
|
|
1428
|
+
const workerEnv = applyConfiguredKey(env, spec.provider, spec.baseUrl, keyCfg, spec.model);
|
|
1429
|
+
const judgeEnv = judge.provider === spec.provider ? workerEnv : applyConfiguredKey(env, judge.provider, undefined, keyCfg, judge.model);
|
|
1430
|
+
/** the variable NAME this worker's key is read from (the person's pick, else built-in) — for messages only */
|
|
1431
|
+
const workerKeyName = keyNameFor(spec.provider, spec.baseUrl, keyCfg, spec.model).name;
|
|
1345
1432
|
// A green anthropic-api job's behaviour is byte-identical to before: its
|
|
1346
1433
|
// worker key IS `ANTHROPIC_API_KEY`, demanded as ever.
|
|
1347
|
-
workerApiKey =
|
|
1348
|
-
if (!workerApiKey) { err(`${
|
|
1434
|
+
workerApiKey = workerEnv[providerEntry.envKey];
|
|
1435
|
+
if (!workerApiKey) { err(`${workerKeyName} not set (secrets load from the environment — never the tree)`); throw new ExitSignal(2); }
|
|
1349
1436
|
// F181 — a key that reads as "set" above can still carry a line break/
|
|
1350
1437
|
// control char/stray whitespace (a two-line secret-store entry, e.g.) and
|
|
1351
1438
|
// crash Node's own header-encode inside the paid span. Refuse at the SAME
|
|
1352
1439
|
// door, before any provider is constructed; never echo the value.
|
|
1353
1440
|
const workerKeyProblem = apiKeyProblem(workerApiKey);
|
|
1354
|
-
if (workerKeyProblem) { err(`${
|
|
1441
|
+
if (workerKeyProblem) { err(`${workerKeyName} ${workerKeyProblem} — refusing rather than crashing mid-call (never trimmed or repaired; fix the value at its source)`); throw new ExitSignal(2); }
|
|
1355
1442
|
// The judge's KEY follows the RESOLVED judge provider's own env var, with
|
|
1356
1443
|
// `JUDGE_API_KEY` as a role-named override in front of it (PRD item 32.3). The
|
|
1357
1444
|
// override matters most in the case this item exists for: a job whose worker and
|
|
1358
1445
|
// judge are the SAME provider but different accounts, and the anthropic case
|
|
1359
1446
|
// where one variable used to do two jobs.
|
|
1360
|
-
judgeApiKey = env.JUDGE_API_KEY ??
|
|
1447
|
+
judgeApiKey = env.JUDGE_API_KEY ?? judgeEnv[judgeEntry.envKey];
|
|
1361
1448
|
if (JUDGES && !judgeApiKey) {
|
|
1362
1449
|
err(`This job's close JUDGES (${judgedStages(spec.closeDecl).length} judged stage(s)) and its judge is ${judge.model} on ${judge.provider}${spec.judge ? ' (signed override)' : " (this job's own worker model — no `judge` override signed)"}.`);
|
|
1363
1450
|
err(`Neither JUDGE_API_KEY nor ${judgeEntry.envKey} is set — secrets load from the environment, never the tree.`);
|
|
@@ -1377,11 +1464,57 @@ async function execute(ctx) {
|
|
|
1377
1464
|
}
|
|
1378
1465
|
}
|
|
1379
1466
|
|
|
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;
|
|
1488
|
+
{
|
|
1489
|
+
/** @type {string|null} */
|
|
1490
|
+
let refusal = null;
|
|
1491
|
+
try {
|
|
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
|
+
}
|
|
1498
|
+
} catch (e) {
|
|
1499
|
+
if (!(e instanceof ConfigError)) throw e;
|
|
1500
|
+
err(`${e.message} — refusing to start rather than guess the monthly limit. Nothing spent.`);
|
|
1501
|
+
throw new ExitSignal(2);
|
|
1502
|
+
}
|
|
1503
|
+
if (refusal !== null) {
|
|
1504
|
+
err(refusal);
|
|
1505
|
+
err('Nothing spent. Lower the cap, or raise the monthly limit in Settings.');
|
|
1506
|
+
throw new ExitSignal(2);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1380
1510
|
// `wd`/`spineDir` are derived once, above the resume reader that needs them; only the
|
|
1381
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();
|
|
1382
1515
|
mkdirSync(spineDir, { recursive: true });
|
|
1383
|
-
const
|
|
1384
|
-
|
|
1516
|
+
const closeDir = ctx.bundle ? join(spineDir, 'close') : spineDir;
|
|
1517
|
+
if (ctx.bundle) mkdirSync(closeDir, { recursive: true });
|
|
1385
1518
|
|
|
1386
1519
|
// PANEL-BUILD.md P1 — one row in the run list, BEFORE any paid call (this
|
|
1387
1520
|
// leg's own `runJob` further down, and BEFORE the provider-key checks that
|
|
@@ -1395,10 +1528,14 @@ async function execute(ctx) {
|
|
|
1395
1528
|
// carries everything else through (env/out/err/provider/…): a caller that
|
|
1396
1529
|
// injects it writes this leg's row under a temp home instead of the real
|
|
1397
1530
|
// `~/.config/bareloop`; production never sets it.
|
|
1531
|
+
// When the monthly claim above already appended this run's row, there is nothing to add.
|
|
1398
1532
|
try {
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
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
|
+
}
|
|
1402
1539
|
} catch (/** @type {any} */ e) {
|
|
1403
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.`);
|
|
1404
1541
|
}
|
|
@@ -1444,13 +1581,13 @@ async function execute(ctx) {
|
|
|
1444
1581
|
if (preFile) out(`gate audit — a stale file was already sitting in the patient tree (not this run's); moved aside to ${preFile}`);
|
|
1445
1582
|
}
|
|
1446
1583
|
|
|
1447
|
-
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() }];
|
|
1448
1585
|
// spec.baseUrl (PRD item 28, ruling (d), 2026-09-09: admitted in v1) — an
|
|
1449
1586
|
// OpenAI-compatible gateway/endpoint. Undefined for anthropic-api jobs and
|
|
1450
1587
|
// for an openai-api job that names none (bare-agent's OpenAIProvider then
|
|
1451
1588
|
// defaults to api.openai.com/v1 on its own).
|
|
1452
1589
|
const baseUrl = typeof spec.baseUrl === 'string' ? spec.baseUrl : undefined;
|
|
1453
|
-
// TEST SEAM — same shape as src/
|
|
1590
|
+
// TEST SEAM — same shape as the bundle door (src/bundlerun.js): a caller that supplies
|
|
1454
1591
|
// `deps.provider` skips the real-key check above entirely and this
|
|
1455
1592
|
// construction never runs (a scripted provider IS the run).
|
|
1456
1593
|
let provider = deps.provider;
|
|
@@ -1624,9 +1761,9 @@ async function execute(ctx) {
|
|
|
1624
1761
|
// containing `%20` that does not exist, and the guard dies at startup on the one run
|
|
1625
1762
|
// it was meant to protect. Same spelling the sibling scripts use.
|
|
1626
1763
|
const watchdog = spawn(process.execPath, [
|
|
1627
|
-
//
|
|
1628
|
-
//
|
|
1629
|
-
fileURLToPath(new 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)),
|
|
1630
1767
|
'--spine', spineFile,
|
|
1631
1768
|
'--pid', String(process.pid),
|
|
1632
1769
|
'--stale-ms', String(worstCloseSilenceMs + 600_000),
|
|
@@ -1682,7 +1819,9 @@ async function execute(ctx) {
|
|
|
1682
1819
|
// used (never a second `makeSpine(spineFile)` here: two independent
|
|
1683
1820
|
// emitters against one file would both start their seq counter at 0 and
|
|
1684
1821
|
// collide the moment either one had already written a record).
|
|
1685
|
-
approvals, workdir: wd, provider, providerFor, judgeProvider, judgeModel: judge.model,
|
|
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,
|
|
1686
1825
|
// F133 (run mtqwmb9l) — `closeTimeoutMs` is deliberately NOT passed here.
|
|
1687
1826
|
// `RESOLVED_CLOSE_TIMEOUT_MS` above exists only to size the outside
|
|
1688
1827
|
// watchdog before it spawns; feeding it back into `runJob` used to make
|
|
@@ -1692,7 +1831,7 @@ async function execute(ctx) {
|
|
|
1692
1831
|
// and announces the ceiling itself now — the ONE banner/spine record
|
|
1693
1832
|
// for this run.
|
|
1694
1833
|
shellCapUsd: spec.budgetUsd, capRuns: CAP_RUNS, strikeLimit: STRIKE_LIMIT,
|
|
1695
|
-
closeDir
|
|
1834
|
+
closeDir,
|
|
1696
1835
|
readShim: READ_SHIM,
|
|
1697
1836
|
scout: SCOUT,
|
|
1698
1837
|
draftSpentUsd: DRAFT_SPENT_USD,
|
|
@@ -1770,8 +1909,18 @@ async function execute(ctx) {
|
|
|
1770
1909
|
// ── the read. Facts only: what happened, what it cost, and whether the record is
|
|
1771
1910
|
// honest. No classification into pass/fail buckets — one run classifies nothing.
|
|
1772
1911
|
const raw = readFileSync(spineFile, 'utf8');
|
|
1773
|
-
|
|
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;
|
|
1774
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
|
+
}
|
|
1775
1924
|
// the ONE spelling of the text-side scan (src/validate.js) — a hand-rolled copy
|
|
1776
1925
|
// here would be the ninth, and one that misses a shape leaks on the very output
|
|
1777
1926
|
// it guards. Only the COUNT is ever read out; the matches themselves stay here.
|
|
@@ -1781,7 +1930,7 @@ async function execute(ctx) {
|
|
|
1781
1930
|
|
|
1782
1931
|
const auditSrc = join(wd, 'gate-audit.jsonl');
|
|
1783
1932
|
let auditFile = null;
|
|
1784
|
-
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); }
|
|
1785
1934
|
const audit = auditFile ? readFileSync(auditFile, 'utf8').trimEnd().split('\n').filter(Boolean).map((l) => JSON.parse(l)) : [];
|
|
1786
1935
|
const writes = audit.filter((e) => e.decision === 'allow' && (e.action?.type === 'write' || e.action?.type === 'edit'));
|
|
1787
1936
|
|
|
@@ -1804,6 +1953,7 @@ async function execute(ctx) {
|
|
|
1804
1953
|
// THIS LEG's own tokens, off THIS LEG's own spine — same scope as `rounds` above it,
|
|
1805
1954
|
// never the folded chain (see `tokensLine`'s doc for why worker-result is excluded).
|
|
1806
1955
|
out(`tokens ${tokensLine({ events })}`);
|
|
1956
|
+
if (workerPrice) out(`price ${priceReadout(workerPrice)}`);
|
|
1807
1957
|
out(`writes ${writes.length} allowed (${new Set(writes.map((e) => e.action?.path)).size} distinct files)`);
|
|
1808
1958
|
out(`plan ${plan ? `${plan.steps?.length ?? '?'} ${plan.steps?.length === 1 ? 'step' : 'steps'}` : 'none validated'}`);
|
|
1809
1959
|
out(`checks ${events.filter((e) => e.type === 'check-run').length} runs · menu [${events.find((e) => e.type === 'check-menu')?.offered?.join(', ') ?? '-'}]`);
|
|
@@ -1829,10 +1979,28 @@ async function execute(ctx) {
|
|
|
1829
1979
|
if (mh) {
|
|
1830
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}"` : ''}.`);
|
|
1831
1981
|
out(` trend ${mh.trend} — ${mh.reading}`);
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
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
|
+
}
|
|
1836
2004
|
}
|
|
1837
2005
|
// A STALL is a checkpoint too (hamr's go, 2026-08-13). Its own escalation prints one
|
|
1838
2006
|
// line above and says *"retry the run"*, and until this line the only retry on offer
|
|
@@ -1841,7 +2009,7 @@ async function execute(ctx) {
|
|
|
1841
2009
|
// the hash already approved is the hash that resumes.
|
|
1842
2010
|
if (outcome === 'step-stalled') {
|
|
1843
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.');
|
|
1844
|
-
out(` resume
|
|
2012
|
+
out(` resume ${INVOKE} --resume ${runid}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL}${REGISTRY_TAIL} --approve ${PRINT_APPROVE}`);
|
|
1845
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)');
|
|
1846
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)');
|
|
1847
2015
|
}
|
|
@@ -1862,7 +2030,7 @@ async function execute(ctx) {
|
|
|
1862
2030
|
out(` died ${total === null ? 'before a plan was accepted — nothing paid is re-payable'
|
|
1863
2031
|
: done >= total ? `at the close — all ${total} step(s) finished`
|
|
1864
2032
|
: `in step ${done + 1} of ${total}`}`);
|
|
1865
|
-
out(` resume
|
|
2033
|
+
out(` resume ${INVOKE} --resume ${runid}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL}${REGISTRY_TAIL} --approve ${PRINT_APPROVE}`);
|
|
1866
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)');
|
|
1867
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)');
|
|
1868
2036
|
}
|
|
@@ -1883,7 +2051,7 @@ async function execute(ctx) {
|
|
|
1883
2051
|
]);
|
|
1884
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');
|
|
1885
2053
|
out('');
|
|
1886
|
-
const answer = (/** @type {string} */ tail) =>
|
|
2054
|
+
const answer = (/** @type {string} */ tail) => `${INVOKE} --resume ${runid}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL}${REGISTRY_TAIL}${tail} --approve ${PRINT_APPROVE}`;
|
|
1887
2055
|
for (const l of doorLines({
|
|
1888
2056
|
rerun: answer(' --decide rerun --text "<what you want done differently>"'),
|
|
1889
2057
|
accept: answer(' --decide accept'),
|
|
@@ -2005,7 +2173,7 @@ async function execute(ctx) {
|
|
|
2005
2173
|
// against has to travel in the printed command — an accept aimed at no registry (or at
|
|
2006
2174
|
// a different one) is the `no-row-for-run` refusal all over again, one hop later.
|
|
2007
2175
|
const doorRegistry = REGISTRY_ROW.minted ? ` --registry ${(ctx.registry ?? null)} --workflow ${REGISTRY_ROW.write.name}` : '';
|
|
2008
|
-
const answerDoor = (/** @type {string} */ tail) =>
|
|
2176
|
+
const answerDoor = (/** @type {string} */ tail) => `${INVOKE} --door ${runid}${tail}${doorRegistry} --approve ${PRINT_APPROVE}`;
|
|
2009
2177
|
// THE PROMISE READS OFF THE ROW, not off the class. `quarantined` on the door record
|
|
2010
2178
|
// says this run's credit is HELD; whether there is anything to RELEASE is a second
|
|
2011
2179
|
// question, and the honest answer is "only if a row was minted". Without `--registry`
|
|
@@ -2036,7 +2204,7 @@ async function execute(ctx) {
|
|
|
2036
2204
|
}
|
|
2037
2205
|
|
|
2038
2206
|
// the BRIDGE: on a green the agent's own plan is kept as the reusable artifact
|
|
2039
|
-
if (outcome === 'green' && plan && !leaks.length) {
|
|
2207
|
+
if (outcome === 'green' && plan && !leaks.length && !ctx.bundle) {
|
|
2040
2208
|
// one file PER GREEN, never one file per job: two cold runs of this job produced
|
|
2041
2209
|
// two DIFFERENT plans that both green (run 1: 3 steps, run 3: 2 steps), so a
|
|
2042
2210
|
// single `bridge-<job>.json` silently destroys the earlier bridge. WHICH bridge
|
|
@@ -2098,6 +2266,13 @@ async function execute(ctx) {
|
|
|
2098
2266
|
|
|
2099
2267
|
return exitCode;
|
|
2100
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
|
+
}
|
|
2101
2276
|
if (e instanceof ExitSignal) return e.code;
|
|
2102
2277
|
throw e;
|
|
2103
2278
|
}
|
|
@@ -2130,6 +2305,7 @@ function buildCtx(mode, opts) {
|
|
|
2130
2305
|
door: mode.door,
|
|
2131
2306
|
decide: mode.decide,
|
|
2132
2307
|
text: mode.text,
|
|
2308
|
+
...(opts.bundle === undefined ? {} : { bundle: opts.bundle }),
|
|
2133
2309
|
deps: opts.deps ?? {},
|
|
2134
2310
|
};
|
|
2135
2311
|
}
|
|
@@ -2239,7 +2415,7 @@ export async function main(argv, deps = {}) {
|
|
|
2239
2415
|
if (!existsSync(specPath)) {
|
|
2240
2416
|
die(`--job ${jobArg}: there is no spec at ${specPath}.\n`
|
|
2241
2417
|
+ ' The table row is the runner\'s half of a job (patient, seed, spine); the SPEC is yours — authored through\n'
|
|
2242
|
-
+ `
|
|
2418
|
+
+ ` ${commandFor('interview', deps.invokedAs)} and ${commandFor('author', deps.invokedAs)}, then signed. Nothing here can stand in for it: a job\n`
|
|
2243
2419
|
+ ' with no spec has no goal, no close, no budget and no hash to approve.');
|
|
2244
2420
|
}
|
|
2245
2421
|
try { spec = JSON.parse(readFileSync(specPath, 'utf8')); } catch (e) {
|
|
@@ -2251,7 +2427,7 @@ export async function main(argv, deps = {}) {
|
|
|
2251
2427
|
specPath = resolve(/** @type {string} */ (specArg));
|
|
2252
2428
|
if (!existsSync(specPath)) {
|
|
2253
2429
|
die(`--spec ${specPath} does not exist — a run needs the resolved-spec.json an authoring session actually wrote `
|
|
2254
|
-
+ '
|
|
2430
|
+
+ `(${commandFor('author', deps.invokedAs)}'s own \`out/resolved-spec.json\`), never a path nobody authored`);
|
|
2255
2431
|
}
|
|
2256
2432
|
let raw;
|
|
2257
2433
|
try { raw = readFileSync(specPath, 'utf8'); } catch (e) {
|
|
@@ -2291,8 +2467,8 @@ export async function main(argv, deps = {}) {
|
|
|
2291
2467
|
if (candidates.length === 0) {
|
|
2292
2468
|
die(`--spec ${specPath}: no prepared copy beside it — looked for a source-*/ directory carrying source.json in `
|
|
2293
2469
|
+ `${dirname(specPath)} and found none. A --spec run reads its workdir and seed off the copy the authoring `
|
|
2294
|
-
+
|
|
2295
|
-
+ '
|
|
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.`);
|
|
2296
2472
|
}
|
|
2297
2473
|
if (candidates.length > 1) {
|
|
2298
2474
|
die(`--spec ${specPath}: ${candidates.length} prepared copies sit beside it (${candidates.join(', ')}) — `
|
package/types/authorflow.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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.
|
package/types/authorjob.d.ts
CHANGED
|
@@ -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;
|