bareloop 0.32.0 → 0.34.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 +95 -0
- package/README.md +4 -1
- package/bareloop.context.md +163 -82
- package/package.json +1 -1
- package/src/authorflow.js +5 -3
- package/src/authorjob.js +3 -1
- package/src/authorrun.js +36 -8
- package/src/authorscout.js +10 -6
- package/src/bundlerun.js +266 -0
- package/src/cli.js +24 -352
- package/src/interviewrun.js +6 -4
- package/src/invoke.js +22 -0
- package/src/job.js +2 -2
- package/src/judged.js +27 -5
- package/src/kinds.js +1 -0
- package/src/ledger.js +3 -4
- package/src/monthly.js +112 -35
- package/src/panel/authorroutes.js +12 -2
- package/src/panel/authorsession.js +24 -4
- package/src/panel/server.js +18 -13
- package/src/planrun.js +10 -8
- package/src/providerrows.js +87 -1
- package/src/providers.js +4 -4
- package/src/run.js +7 -2
- package/src/runlist.js +94 -7
- package/src/source.js +8 -3
- package/src/u-watchdog.mjs +296 -0
- package/src/userrun.js +207 -65
- package/types/authorflow.d.ts +8 -2
- package/types/authorjob.d.ts +6 -1
- package/types/authorscout.d.ts +11 -2
- package/types/bundlerun.d.ts +26 -0
- package/types/invoke.d.ts +8 -0
- package/types/judged.d.ts +10 -3
- package/types/monthly.d.ts +57 -10
- package/types/panel/authorroutes.d.ts +15 -0
- package/types/planrun.d.ts +13 -1
- package/types/providerrows.d.ts +66 -0
- package/types/providers.d.ts +1 -2
- package/types/run.d.ts +14 -1
- package/types/runlist.d.ts +79 -3
- package/types/source.d.ts +8 -3
- package/types/userrun.d.ts +49 -0
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, readRunList } from './runlist.js';
|
|
76
76
|
import { keysForDoor } from './keysfile.js';
|
|
77
|
-
import {
|
|
77
|
+
import { parseJsonl } from './replayio.js';
|
|
78
|
+
import { claimRun, monthlyRefusalText, legSpend } from './monthly.js';
|
|
78
79
|
import { ConfigError } from './config.js';
|
|
79
|
-
import {
|
|
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,19 @@ 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;
|
|
365
|
+
// Release the claimed row (a `released` entry folds it out of the list, like a monthly refusal): the run
|
|
366
|
+
// spent nothing and must not stay a ghost that keeps the month reading "at least". A failed release write
|
|
367
|
+
// never masks the original exit.
|
|
368
|
+
const releaseClaimedRow = () => {
|
|
369
|
+
if (!claimedRow) return;
|
|
370
|
+
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 */ }
|
|
371
|
+
};
|
|
338
372
|
try {
|
|
339
373
|
const spec = ctx.spec;
|
|
340
374
|
const specPath = ctx.specPath;
|
|
@@ -454,10 +488,20 @@ async function execute(ctx) {
|
|
|
454
488
|
* dropping the incomplete flag would silently turn a floor into an exact
|
|
455
489
|
* figure on the next leg's own job-start). */
|
|
456
490
|
const DRAFT_TAIL = DRAFT_SPENT_USD > 0 ? ` --draft-spent-usd ${DRAFT_SPENT_USD}${DRAFT_SPEND_INCOMPLETE ? ' --draft-spend-incomplete' : ''}` : '';
|
|
491
|
+
/** the registry the run was pointed at, as flags — every re-invocation this script PRINTS carries it
|
|
492
|
+
* (a paste that dropped it would green with no registry row). Empty when no --registry was named. */
|
|
493
|
+
const REGISTRY_TAIL = (ctx.registry ?? null) !== null ? ` --registry ${(ctx.registry ?? null)} --workflow ${(ctx.workflow ?? null) ?? spec.job}` : '';
|
|
457
494
|
|
|
458
495
|
const WORKDIR = target.workdir;
|
|
459
496
|
const SEED = target.seed;
|
|
460
497
|
const specHash = jobSpecHash(spec);
|
|
498
|
+
// the re-invocation commands this engine prints: a bundle names its own door and hash,
|
|
499
|
+
// every other caller gets today's byte-identical strings.
|
|
500
|
+
const WALL_FLAG = ctx.bundle?.tightened.wallMin === undefined ? '' : ` --wall ${ctx.bundle.tightened.wallMin}`;
|
|
501
|
+
const INVOKE = ctx.bundle
|
|
502
|
+
? `${ctx.bundle.invoke}${ctx.bundle.tightened.budgetUsd === undefined ? '' : ` --budget ${ctx.bundle.tightened.budgetUsd}`}${WALL_FLAG}`
|
|
503
|
+
: `${commandFor('run-u', deps.invokedAs)} ${SELECTOR}`;
|
|
504
|
+
const PRINT_APPROVE = ctx.bundle?.printApprove ?? specHash;
|
|
461
505
|
// MODEL (build-list #3, hamr's GO 2026-08-30): the signed spec's `model`, if
|
|
462
506
|
// present, wins outright; a --model flag naming a DIFFERENT id is refused
|
|
463
507
|
// (never silently overridden) — resolveWorkerModel is the one decision, pure
|
|
@@ -610,7 +654,7 @@ async function execute(ctx) {
|
|
|
610
654
|
* both the RESUME reader below and the live run further down need them. Two spellings of
|
|
611
655
|
* one path is how `--resume` comes to read a different directory than the run writes. */
|
|
612
656
|
const wd = resolve(WORKDIR);
|
|
613
|
-
const spineDir = join(wd, '..', target.spine);
|
|
657
|
+
const spineDir = ctx.bundle ? ctx.bundle.runDir : join(wd, '..', target.spine);
|
|
614
658
|
/** `--resume` takes the runid (the conventional `<spine dir>/u-<runid>.jsonl`) or an
|
|
615
659
|
* explicit path — the path form is what makes these gates testable without touching
|
|
616
660
|
* an operator's real spine directory. */
|
|
@@ -647,22 +691,22 @@ async function execute(ctx) {
|
|
|
647
691
|
});
|
|
648
692
|
// is the dead run actually dead? Two processes on one patient is unrecoverable; a
|
|
649
693
|
// false refusal costs one sentence. run-u's own pid is not on its spine, so the
|
|
650
|
-
// watchdog's report is the pid there is — and its absence is not evidence of life.
|
|
694
|
+
// watchdog's report or the run-list row is the pid there is — and its absence is not evidence of life.
|
|
651
695
|
const wdFile = `${deadSpineFile}.watchdog.json`;
|
|
652
696
|
/** @type {any} */
|
|
653
697
|
let watchdog = null;
|
|
654
698
|
if (existsSync(wdFile)) { try { watchdog = JSON.parse(readFileSync(wdFile, 'utf8')); } catch { /* an unreadable report is not evidence of life */ } }
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
}
|
|
699
|
+
// ONE owner of "is that pid a live bareloop runner" (src/runlist.js `isLiveRunner`, shared with the
|
|
700
|
+
// monthly limit's holds): alive AND a bareloop runner refuses; a recycled pid that now runs some
|
|
701
|
+
// other program does not. Where /proc is unreadable an existing pid is assumed to be ours.
|
|
702
|
+
// The pid is the watchdog's kill record's, or the predecessor's OWN run-list row's (every row carries
|
|
703
|
+
// its runner's pid): a run never killed by the watchdog has no record but still has its row.
|
|
704
|
+
/** @type {number|undefined} */
|
|
705
|
+
let rowPid;
|
|
706
|
+
if (cfgOn) { try { rowPid = readRunList({ home: cfgHome }).rows.findLast((r) => r.spine === deadSpineFile)?.pid; } catch { /* an unreadable list is not evidence of life */ } }
|
|
707
|
+
const alivePid = [watchdog?.pid, rowPid].find((p) => Number.isInteger(p) && isLiveRunner(p));
|
|
708
|
+
if (alivePid !== undefined) {
|
|
709
|
+
die(`--resume: pid ${alivePid} from ${deadSpineFile} is still alive (a bareloop runner). Two processes on one patient is unrecoverable — stop it first.`);
|
|
666
710
|
}
|
|
667
711
|
// WHEN did the dead leg stop? The watchdog's kill record is later, better evidence
|
|
668
712
|
// than the last spine event for a run that was KILLED — and worse evidence for one
|
|
@@ -916,8 +960,7 @@ async function execute(ctx) {
|
|
|
916
960
|
// and the printed commands carry the registry forward, because an accept aimed
|
|
917
961
|
// at no registry is the `no-row-for-run` refusal one hop later.
|
|
918
962
|
const previewHeld = doorRecord?.quarantined === true && heldRowFor((ctx.registry ?? null), (ctx.workflow ?? null) ?? spec.job, DOOR);
|
|
919
|
-
const
|
|
920
|
-
const doorInvoke = (/** @type {string} */ tail) => ` node scripts/run-u.mjs ${SELECTOR} --door ${DOOR}${tail}${previewRegistry} --approve ${specHash}`;
|
|
963
|
+
const doorInvoke = (/** @type {string} */ tail) => ` ${INVOKE} --door ${DOOR}${tail}${REGISTRY_TAIL} --approve ${PRINT_APPROVE}`;
|
|
921
964
|
out('');
|
|
922
965
|
if (RULING === null) {
|
|
923
966
|
for (const l of runDoorLines({
|
|
@@ -943,12 +986,30 @@ async function execute(ctx) {
|
|
|
943
986
|
throw new ExitSignal((ctx.approve ?? null) === null ? 0 : 1);
|
|
944
987
|
}
|
|
945
988
|
|
|
989
|
+
// THE CUSTOMER'S OWN PRICE (hamr 2026-09-30) — the worker's key row may carry `priceInPerM` /
|
|
990
|
+
// `priceOutPerM` (USD per 1M tokens); `ratesFor` is the one lookup, on the SAME row `keyNameFor` picks
|
|
991
|
+
// the key from. Resolved ONCE here, at $0, before the preview, the monthly claim and any worktree or
|
|
992
|
+
// spine, then handed down as data (`runJob`'s `rates`) — nothing deep in the run re-reads config. A bad
|
|
993
|
+
// price refuses here; it never falls back to the built-in guess. No price set = `null` = no `rates` key
|
|
994
|
+
// passed anywhere, byte-identical to before. The judge's own row is resolved below, beside its key.
|
|
995
|
+
const keyCfg = cfgOn ? rowsForHome(cfgHome) : [];
|
|
996
|
+
/** @type {ReturnType<typeof ratesFor>} */
|
|
997
|
+
let workerPrice = null;
|
|
998
|
+
try {
|
|
999
|
+
workerPrice = ratesFor(spec.provider, spec.baseUrl, keyCfg, spec.model);
|
|
1000
|
+
} catch (e) {
|
|
1001
|
+
if (!(e instanceof ConfigError)) throw e;
|
|
1002
|
+
err(`${e.message} — refusing to start rather than guess a price. Nothing spent.`);
|
|
1003
|
+
throw new ExitSignal(2);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
946
1006
|
if ((ctx.approve ?? null) !== specHash) {
|
|
947
1007
|
out(dead ? 'U — RESUME, continuing a halted run, REAL dollars' : 'U — user-mode e2e, ONE run, REAL dollars');
|
|
948
1008
|
out(` spec ${SPEC_DESC} $${spec.budgetUsd} wall ${WALL_LABEL} strikeLimit=${STRIKE_LIMIT} (step ladder + close-fix progress rule)`);
|
|
949
1009
|
out(` patient ${WORKDIR} @ ${SEED.slice(0, 12)}`);
|
|
950
1010
|
out(` shim ${READ_SHIM_LABEL}`);
|
|
951
1011
|
out(` scout ${SCOUT_LABEL}`);
|
|
1012
|
+
if (workerPrice) out(` price ${priceReadout(workerPrice)}`);
|
|
952
1013
|
out(` goal "${spec.goal}"`);
|
|
953
1014
|
// F87 — the goal must state everything the close will judge, and nothing derives
|
|
954
1015
|
// one from the other or checks them against each other. So the only defence is
|
|
@@ -1099,9 +1160,9 @@ async function execute(ctx) {
|
|
|
1099
1160
|
// different provider (e.g. openai-api/DeepSeek) printed the WRONG variable
|
|
1100
1161
|
// name to set: a person pasting it verbatim hit a $0 refusal naming the
|
|
1101
1162
|
// right key only by accident of the runner's own generic error message,
|
|
1102
|
-
// never from this hint. `
|
|
1103
|
-
// the real key check at launch (
|
|
1104
|
-
const invoke = (/** @type {string} */ tail) => ` ${
|
|
1163
|
+
// never from this hint. `keyNameFor` is the same resolved name
|
|
1164
|
+
// the real key check at launch reads (the person's row variable, else the provider's built-in one).
|
|
1165
|
+
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
1166
|
/** the door the operator has already picked, as flags — hoisted out of the else
|
|
1106
1167
|
* below so the inhibitor line at the bottom can print the WHOLE command rather
|
|
1107
1168
|
* than a shape the operator has to assemble. Empty on an ordinary run and on the
|
|
@@ -1228,7 +1289,7 @@ async function execute(ctx) {
|
|
|
1228
1289
|
} else {
|
|
1229
1290
|
out(` costs nothing, in any state — no work, no money, no allowance moved`);
|
|
1230
1291
|
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
|
|
1292
|
+
out(` reopen ${INVOKE} --door ${DOOR}${REGISTRY_TAIL} --approve ${PRINT_APPROVE}`);
|
|
1232
1293
|
}
|
|
1233
1294
|
throw new ExitSignal(0);
|
|
1234
1295
|
}
|
|
@@ -1266,8 +1327,8 @@ async function execute(ctx) {
|
|
|
1266
1327
|
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
1328
|
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
1329
|
out(' resume the SAME runid, whenever you want, with the door you pick then:');
|
|
1269
|
-
out(`
|
|
1270
|
-
out(`
|
|
1330
|
+
out(` ${INVOKE} --resume ${RESUME}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL}${REGISTRY_TAIL} --decide accept --approve ${PRINT_APPROVE}`);
|
|
1331
|
+
out(` ${INVOKE} --resume ${RESUME}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL}${REGISTRY_TAIL} --decide rerun --text "<what you want done differently>" --approve ${PRINT_APPROVE}`);
|
|
1271
1332
|
out(` read the same command with no --decide re-prints the evidence package you just looked at`);
|
|
1272
1333
|
throw new ExitSignal(0);
|
|
1273
1334
|
}
|
|
@@ -1348,6 +1409,20 @@ async function execute(ctx) {
|
|
|
1348
1409
|
// into runJob below whether this leg constructs a real judge provider or not.
|
|
1349
1410
|
const judge = resolveJudge({ specJudge: spec.judge, workerProvider: spec.provider, workerModel: MODEL });
|
|
1350
1411
|
const judgeEntry = resolveProvider(judge.provider);
|
|
1412
|
+
// …and the judge's price, from the judge MODEL's row (`judgeRatesFor`): on the worker's endpoint when the
|
|
1413
|
+
// judge is the same provider (falling back to the worker's price when the model names no row), else the
|
|
1414
|
+
// row for the judge's own provider. Only a judging close ever calls it, so only then can its row refuse the run.
|
|
1415
|
+
/** @type {ReturnType<typeof ratesFor>} */
|
|
1416
|
+
let judgePrice = null;
|
|
1417
|
+
if (JUDGES) {
|
|
1418
|
+
try {
|
|
1419
|
+
judgePrice = judgeRatesFor(judge, spec.provider, spec.baseUrl, workerPrice, keyCfg);
|
|
1420
|
+
} catch (e) {
|
|
1421
|
+
if (!(e instanceof ConfigError)) throw e;
|
|
1422
|
+
err(`${e.message} — refusing to start rather than guess a price. Nothing spent.`);
|
|
1423
|
+
throw new ExitSignal(2);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1351
1426
|
// undefined, not null: both come straight off `env[...]` lookups below, which
|
|
1352
1427
|
// (like process.env) never produce null for a missing key.
|
|
1353
1428
|
/** @type {string|undefined} */
|
|
@@ -1355,7 +1430,7 @@ async function execute(ctx) {
|
|
|
1355
1430
|
/** @type {string|undefined} */
|
|
1356
1431
|
let judgeApiKey;
|
|
1357
1432
|
// TEST SEAM (constraint: `deps.provider` skips the real-key check entirely,
|
|
1358
|
-
// the same shape src/
|
|
1433
|
+
// the same shape src/bundlerun.js's door uses) — a caller that hands in
|
|
1359
1434
|
// its own provider IS the run, so nothing here needs a real secret.
|
|
1360
1435
|
if (!deps.provider) {
|
|
1361
1436
|
// P4b — the Settings row (keys file line) whose API shape + Base URL match this spec
|
|
@@ -1363,7 +1438,6 @@ async function execute(ctx) {
|
|
|
1363
1438
|
// row = the built-in. Names only — values come from the
|
|
1364
1439
|
// env / keys file. The judge follows the worker's choice when it is the same provider
|
|
1365
1440
|
// (the same rule that gives it the worker's baseUrl below).
|
|
1366
|
-
const keyCfg = cfgOn ? rowsForHome(cfgHome) : [];
|
|
1367
1441
|
const workerEnv = applyConfiguredKey(env, spec.provider, spec.baseUrl, keyCfg, spec.model);
|
|
1368
1442
|
const judgeEnv = judge.provider === spec.provider ? workerEnv : applyConfiguredKey(env, judge.provider, undefined, keyCfg, judge.model);
|
|
1369
1443
|
/** the variable NAME this worker's key is read from (the person's pick, else built-in) — for messages only */
|
|
@@ -1403,22 +1477,37 @@ async function execute(ctx) {
|
|
|
1403
1477
|
}
|
|
1404
1478
|
}
|
|
1405
1479
|
|
|
1406
|
-
// THE MONTHLY $ LIMIT (PANEL-BUILD.md P4a
|
|
1407
|
-
// ONE run-start seam. Every run this door starts (the CLI's `bareloop run-u`
|
|
1408
|
-
// which spawns
|
|
1409
|
-
//
|
|
1410
|
-
//
|
|
1411
|
-
//
|
|
1412
|
-
//
|
|
1413
|
-
//
|
|
1414
|
-
//
|
|
1480
|
+
// THE MONTHLY $ LIMIT (PANEL-BUILD.md P4a; hamr 2026-09-30: "whoever runs first claims the limit and
|
|
1481
|
+
// holds it") — the ONE run-start seam. Every run this door starts (the CLI's `bareloop run-u`, `bareloop
|
|
1482
|
+
// run <bundle>` and the panel, which spawns them) passes here after the signature and the key gates and
|
|
1483
|
+
// BEFORE the patient reset or any token: the run CLAIMS its leg cap — it appends its own run-list row
|
|
1484
|
+
// FIRST (carrying its `pid` and `capUsd`, the hold), then reads the list: only the claims above its row
|
|
1485
|
+
// count. A cap larger than what is left is released (a `released` entry) and does not start; nothing is spent. This leg's exposure
|
|
1486
|
+
// is the whole signed cap on a cold start (it covers drafting + run) and only the REMAINDER on a
|
|
1487
|
+
// resume/door-rerun, whose earlier spend is already in the month's total (the killed leg's claim is
|
|
1488
|
+
// closed at its floor, with a note, once its process is gone). No limit set = no claim (the row is listed as
|
|
1489
|
+
// ever). An unreadable config.json, or a claim row that cannot be written, refuses too — a broken instrument
|
|
1490
|
+
// never silently reads "no limit".
|
|
1491
|
+
const runid = ctx.bundle?.runid ?? Date.now().toString(36);
|
|
1492
|
+
const spineFile = ctx.bundle ? join(spineDir, 'spine.jsonl') : join(spineDir, `u-${runid}.jsonl`);
|
|
1493
|
+
const foldedUsd = dead ? dead.restart.priorSpentUsd : (doorPrior?.spentUsd ?? 0);
|
|
1494
|
+
const legCapUsd = Math.max(0, spec.budgetUsd - (typeof foldedUsd === 'number' && Number.isFinite(foldedUsd) ? foldedUsd : 0));
|
|
1495
|
+
// every row carries its runner's pid and the leg cap, limit set or not: a limit set LATER still holds it
|
|
1496
|
+
/** @type {import('./runlist.js').RunRow} */
|
|
1497
|
+
const runRow = {
|
|
1498
|
+
at: new Date().toISOString(), runid, job: spec.job, spine: spineFile, patient: wd, via: ctx.bundle ? 'bundle' : 'run-u', pid: process.pid, capUsd: legCapUsd,
|
|
1499
|
+
};
|
|
1500
|
+
let claimed = false;
|
|
1415
1501
|
{
|
|
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
1502
|
/** @type {string|null} */
|
|
1419
1503
|
let refusal = null;
|
|
1420
1504
|
try {
|
|
1421
|
-
if (cfgOn)
|
|
1505
|
+
if (cfgOn) {
|
|
1506
|
+
const got = claimRun({ row: runRow, capUsd: legCapUsd, home: cfgHome });
|
|
1507
|
+
claimed = got.claimed;
|
|
1508
|
+
if (claimed) claimedRow = { runid, spine: spineFile, home: cfgHome };
|
|
1509
|
+
refusal = monthlyRefusalText(got.room);
|
|
1510
|
+
}
|
|
1422
1511
|
} catch (e) {
|
|
1423
1512
|
if (!(e instanceof ConfigError)) throw e;
|
|
1424
1513
|
err(`${e.message} — refusing to start rather than guess the monthly limit. Nothing spent.`);
|
|
@@ -1433,9 +1522,12 @@ async function execute(ctx) {
|
|
|
1433
1522
|
|
|
1434
1523
|
// `wd`/`spineDir` are derived once, above the resume reader that needs them; only the
|
|
1435
1524
|
// directory's CREATION belongs here, after the preview/approval gates have exited.
|
|
1525
|
+
// A bundle's tree and books are created HERE and not earlier: every $0 refusal above
|
|
1526
|
+
// (keys, monthly limit, param guards) leaves no worktree behind.
|
|
1527
|
+
ctx.bundle?.prepareTree();
|
|
1436
1528
|
mkdirSync(spineDir, { recursive: true });
|
|
1437
|
-
const
|
|
1438
|
-
|
|
1529
|
+
const closeDir = ctx.bundle ? join(spineDir, 'close') : spineDir;
|
|
1530
|
+
if (ctx.bundle) mkdirSync(closeDir, { recursive: true });
|
|
1439
1531
|
|
|
1440
1532
|
// PANEL-BUILD.md P1 — one row in the run list, BEFORE any paid call (this
|
|
1441
1533
|
// leg's own `runJob` further down, and BEFORE the provider-key checks that
|
|
@@ -1449,10 +1541,14 @@ async function execute(ctx) {
|
|
|
1449
1541
|
// carries everything else through (env/out/err/provider/…): a caller that
|
|
1450
1542
|
// injects it writes this leg's row under a temp home instead of the real
|
|
1451
1543
|
// `~/.config/bareloop`; production never sets it.
|
|
1544
|
+
// When the monthly claim above already appended this run's row, there is nothing to add.
|
|
1452
1545
|
try {
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1546
|
+
if (!claimed) {
|
|
1547
|
+
appendRun(runRow, { home: deps.runlistHome });
|
|
1548
|
+
// no limit set: the row is appended here, not by a claim — mark it so the execute catch releases it
|
|
1549
|
+
// the same way if the run exits before its spine has a first record (it lives in `deps.runlistHome`)
|
|
1550
|
+
claimedRow = { runid, spine: spineFile, home: deps.runlistHome };
|
|
1551
|
+
}
|
|
1456
1552
|
} catch (/** @type {any} */ e) {
|
|
1457
1553
|
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
1554
|
}
|
|
@@ -1498,13 +1594,13 @@ async function execute(ctx) {
|
|
|
1498
1594
|
if (preFile) out(`gate audit — a stale file was already sitting in the patient tree (not this run's); moved aside to ${preFile}`);
|
|
1499
1595
|
}
|
|
1500
1596
|
|
|
1501
|
-
const approvals = [{ specHash, signer: env.USER ?? 'human', ts: new Date().toISOString() }];
|
|
1597
|
+
const approvals = [{ specHash, signer: ctx.bundle ? 'bundle' : (env.USER ?? 'human'), ts: new Date().toISOString() }];
|
|
1502
1598
|
// spec.baseUrl (PRD item 28, ruling (d), 2026-09-09: admitted in v1) — an
|
|
1503
1599
|
// OpenAI-compatible gateway/endpoint. Undefined for anthropic-api jobs and
|
|
1504
1600
|
// for an openai-api job that names none (bare-agent's OpenAIProvider then
|
|
1505
1601
|
// defaults to api.openai.com/v1 on its own).
|
|
1506
1602
|
const baseUrl = typeof spec.baseUrl === 'string' ? spec.baseUrl : undefined;
|
|
1507
|
-
// TEST SEAM — same shape as src/
|
|
1603
|
+
// TEST SEAM — same shape as the bundle door (src/bundlerun.js): a caller that supplies
|
|
1508
1604
|
// `deps.provider` skips the real-key check above entirely and this
|
|
1509
1605
|
// construction never runs (a scripted provider IS the run).
|
|
1510
1606
|
let provider = deps.provider;
|
|
@@ -1621,6 +1717,7 @@ async function execute(ctx) {
|
|
|
1621
1717
|
emit('destination-refused', { code: sourceManifest.code, detail: sourceManifest.stop });
|
|
1622
1718
|
emit('run-end', { outcome: 'escalated' });
|
|
1623
1719
|
err(`SOURCE-MANIFEST-RED — ${sourceManifest.stop}. See ${spineFile}`);
|
|
1720
|
+
releaseClaimedRow(); // a $0 refusal after the spine exists: no ghost row, no "at least" month
|
|
1624
1721
|
throw new ExitSignal(1);
|
|
1625
1722
|
}
|
|
1626
1723
|
/** the front door's per-run instance, or null when there is none (no
|
|
@@ -1639,6 +1736,7 @@ async function execute(ctx) {
|
|
|
1639
1736
|
emit('destination-refused', { code: dp.code, detail: dp.stop });
|
|
1640
1737
|
emit('run-end', { outcome: 'escalated' });
|
|
1641
1738
|
err(`DESTINATION-RED — ${dp.stop}. See ${spineFile}`);
|
|
1739
|
+
releaseClaimedRow(); // a $0 refusal after the spine exists: no ghost row, no "at least" month
|
|
1642
1740
|
throw new ExitSignal(1);
|
|
1643
1741
|
}
|
|
1644
1742
|
}
|
|
@@ -1657,6 +1755,7 @@ async function execute(ctx) {
|
|
|
1657
1755
|
});
|
|
1658
1756
|
emit('run-end', { outcome: 'escalated' });
|
|
1659
1757
|
err(`CLOSE-TIMING-RED — stage(s) never finished the timing preflight: ${names}. See ${spineFile}`);
|
|
1758
|
+
releaseClaimedRow(); // a $0 refusal after the spine exists: no ghost row, no "at least" month
|
|
1660
1759
|
throw new ExitSignal(1);
|
|
1661
1760
|
}
|
|
1662
1761
|
const RESOLVED_CLOSE_TIMEOUT_MS = /** @type {number} */ (closeTimingResolved.closeTimeoutMs);
|
|
@@ -1678,9 +1777,9 @@ async function execute(ctx) {
|
|
|
1678
1777
|
// containing `%20` that does not exist, and the guard dies at startup on the one run
|
|
1679
1778
|
// it was meant to protect. Same spelling the sibling scripts use.
|
|
1680
1779
|
const watchdog = spawn(process.execPath, [
|
|
1681
|
-
//
|
|
1682
|
-
//
|
|
1683
|
-
fileURLToPath(new URL('
|
|
1780
|
+
// src/u-watchdog.mjs ships in the package (F205: it used to live in scripts/,
|
|
1781
|
+
// which the tarball omits, so an installed run spawned a guard that died at startup).
|
|
1782
|
+
fileURLToPath(new URL('./u-watchdog.mjs', import.meta.url)),
|
|
1684
1783
|
'--spine', spineFile,
|
|
1685
1784
|
'--pid', String(process.pid),
|
|
1686
1785
|
'--stale-ms', String(worstCloseSilenceMs + 600_000),
|
|
@@ -1736,7 +1835,9 @@ async function execute(ctx) {
|
|
|
1736
1835
|
// used (never a second `makeSpine(spineFile)` here: two independent
|
|
1737
1836
|
// emitters against one file would both start their seq counter at 0 and
|
|
1738
1837
|
// collide the moment either one had already written a record).
|
|
1739
|
-
approvals, workdir: wd, provider, providerFor, judgeProvider, judgeModel: judge.model,
|
|
1838
|
+
approvals, workdir: wd, provider, providerFor, judgeProvider, judgeModel: judge.model,
|
|
1839
|
+
// the customer's own price per key row (USD per 1K), or null = the built-in guess as before
|
|
1840
|
+
rates: workerPrice?.rates ?? null, judgeRates: judgePrice?.rates ?? null, emit,
|
|
1740
1841
|
// F133 (run mtqwmb9l) — `closeTimeoutMs` is deliberately NOT passed here.
|
|
1741
1842
|
// `RESOLVED_CLOSE_TIMEOUT_MS` above exists only to size the outside
|
|
1742
1843
|
// watchdog before it spawns; feeding it back into `runJob` used to make
|
|
@@ -1746,7 +1847,7 @@ async function execute(ctx) {
|
|
|
1746
1847
|
// and announces the ceiling itself now — the ONE banner/spine record
|
|
1747
1848
|
// for this run.
|
|
1748
1849
|
shellCapUsd: spec.budgetUsd, capRuns: CAP_RUNS, strikeLimit: STRIKE_LIMIT,
|
|
1749
|
-
closeDir
|
|
1850
|
+
closeDir,
|
|
1750
1851
|
readShim: READ_SHIM,
|
|
1751
1852
|
scout: SCOUT,
|
|
1752
1853
|
draftSpentUsd: DRAFT_SPENT_USD,
|
|
@@ -1824,8 +1925,18 @@ async function execute(ctx) {
|
|
|
1824
1925
|
// ── the read. Facts only: what happened, what it cost, and whether the record is
|
|
1825
1926
|
// honest. No classification into pass/fail buckets — one run classifies nothing.
|
|
1826
1927
|
const raw = readFileSync(spineFile, 'utf8');
|
|
1827
|
-
|
|
1928
|
+
// tolerant: a torn/garbled line is skipped, not a crash (the shipped bundle-run fix, now the one engine read)
|
|
1929
|
+
const events = parseJsonl(spineFile).records;
|
|
1828
1930
|
const je = events.findLast((e) => e.type === 'job-end');
|
|
1931
|
+
// The claim's WRITE-BACK (hamr 2026-09-30): when the run ends it settles its own claim on the monthly
|
|
1932
|
+
// limit — its final spend, attributed to itself. Best-effort: a run that dies before here is settled
|
|
1933
|
+
// (`process gone`) by the next run that reads the list (file order: a claim yields only to the rows above it).
|
|
1934
|
+
if (je) {
|
|
1935
|
+
try {
|
|
1936
|
+
const fin = legSpend(events);
|
|
1937
|
+
appendRunEvent({ runid, type: 'settled', by: runid, spentUsd: fin.usd, spendComplete: fin.complete, at: new Date().toISOString() }, { home: cfgHome });
|
|
1938
|
+
} 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.`); }
|
|
1939
|
+
}
|
|
1829
1940
|
// the ONE spelling of the text-side scan (src/validate.js) — a hand-rolled copy
|
|
1830
1941
|
// here would be the ninth, and one that misses a shape leaks on the very output
|
|
1831
1942
|
// it guards. Only the COUNT is ever read out; the matches themselves stay here.
|
|
@@ -1835,7 +1946,7 @@ async function execute(ctx) {
|
|
|
1835
1946
|
|
|
1836
1947
|
const auditSrc = join(wd, 'gate-audit.jsonl');
|
|
1837
1948
|
let auditFile = null;
|
|
1838
|
-
if (existsSync(auditSrc)) { auditFile = join(spineDir, `u-${runid}-gate-audit.jsonl`); renameSync(auditSrc, auditFile); }
|
|
1949
|
+
if (existsSync(auditSrc)) { auditFile = ctx.bundle ? join(spineDir, 'gate-audit.jsonl') : join(spineDir, `u-${runid}-gate-audit.jsonl`); renameSync(auditSrc, auditFile); }
|
|
1839
1950
|
const audit = auditFile ? readFileSync(auditFile, 'utf8').trimEnd().split('\n').filter(Boolean).map((l) => JSON.parse(l)) : [];
|
|
1840
1951
|
const writes = audit.filter((e) => e.decision === 'allow' && (e.action?.type === 'write' || e.action?.type === 'edit'));
|
|
1841
1952
|
|
|
@@ -1858,6 +1969,7 @@ async function execute(ctx) {
|
|
|
1858
1969
|
// THIS LEG's own tokens, off THIS LEG's own spine — same scope as `rounds` above it,
|
|
1859
1970
|
// never the folded chain (see `tokensLine`'s doc for why worker-result is excluded).
|
|
1860
1971
|
out(`tokens ${tokensLine({ events })}`);
|
|
1972
|
+
if (workerPrice) out(`price ${priceReadout(workerPrice)}`);
|
|
1861
1973
|
out(`writes ${writes.length} allowed (${new Set(writes.map((e) => e.action?.path)).size} distinct files)`);
|
|
1862
1974
|
out(`plan ${plan ? `${plan.steps?.length ?? '?'} ${plan.steps?.length === 1 ? 'step' : 'steps'}` : 'none validated'}`);
|
|
1863
1975
|
out(`checks ${events.filter((e) => e.type === 'check-run').length} runs · menu [${events.find((e) => e.type === 'check-menu')?.offered?.join(', ') ?? '-'}]`);
|
|
@@ -1883,10 +1995,34 @@ async function execute(ctx) {
|
|
|
1883
1995
|
if (mh) {
|
|
1884
1996
|
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
1997
|
out(` trend ${mh.trend} — ${mh.reading}`);
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1998
|
+
if (ctx.bundle) {
|
|
1999
|
+
// a bundle's spec is manifest-hashed: there is no budgetUsd to edit and no new hash to sign. The room that
|
|
2000
|
+
// is left is the signed ceiling, and the ONLY way to use it is the person typing a larger --budget. The
|
|
2001
|
+
// exhausted --budget is never echoed (pasted as printed it halts again at once: the chain folds prior spend),
|
|
2002
|
+
// and nothing is filled in for them.
|
|
2003
|
+
const signed = ctx.bundle.signedBudgetUsd;
|
|
2004
|
+
const spentUsd = typeof mh.budgetUsd === 'number' && typeof mh.remainingUsd === 'number' ? mh.budgetUsd - mh.remainingUsd : null;
|
|
2005
|
+
const spentTxt = spentUsd === null ? '?' : `${mh.spendComplete === false ? '≥' : ''}$${spentUsd.toFixed(4)}`;
|
|
2006
|
+
const lever = mh.trend === 'converging' ? 'the work was still converging when the money ran out; more money may finish it'
|
|
2007
|
+
: mh.trend === 'flat' ? 'more money is unlikely to help; this bundle\'s goal is signed and cannot be revised here'
|
|
2008
|
+
: 'read the last close output before deciding whether more money is worth it';
|
|
2009
|
+
out(` lever ${lever}`);
|
|
2010
|
+
if (spentUsd !== null && spentUsd >= signed) {
|
|
2011
|
+
// the whole signed ceiling is spent: "more than spent, at most signed" is unsatisfiable, so no resume line is printed
|
|
2012
|
+
out(` · no room left under the signed ceiling: this run spent all of the signed $${signed}, and a bundle's ceiling cannot be raised`);
|
|
2013
|
+
out(' · abandon the task');
|
|
2014
|
+
} else {
|
|
2015
|
+
out(` · choose a larger --budget on the resume line below (the room left is the signed $${signed}; it cannot be raised past that)`);
|
|
2016
|
+
out(' · abandon the task');
|
|
2017
|
+
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}`);
|
|
2018
|
+
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)');
|
|
2019
|
+
}
|
|
2020
|
+
} else {
|
|
2021
|
+
out(` lever ${mh.lever}`);
|
|
2022
|
+
for (const o of mh.options ?? []) out(` · ${o}`);
|
|
2023
|
+
out(` resume ${INVOKE} --resume ${runid}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL}${REGISTRY_TAIL} --approve <the NEW hash after you edit budgetUsd>`);
|
|
2024
|
+
out(' (the top-up is yours to sign — nothing in the run may widen its own budget)');
|
|
2025
|
+
}
|
|
1890
2026
|
}
|
|
1891
2027
|
// A STALL is a checkpoint too (hamr's go, 2026-08-13). Its own escalation prints one
|
|
1892
2028
|
// line above and says *"retry the run"*, and until this line the only retry on offer
|
|
@@ -1895,7 +2031,7 @@ async function execute(ctx) {
|
|
|
1895
2031
|
// the hash already approved is the hash that resumes.
|
|
1896
2032
|
if (outcome === 'step-stalled') {
|
|
1897
2033
|
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
|
|
2034
|
+
out(` resume ${INVOKE} --resume ${runid}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL}${REGISTRY_TAIL} --approve ${PRINT_APPROVE}`);
|
|
1899
2035
|
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
2036
|
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
2037
|
}
|
|
@@ -1916,7 +2052,7 @@ async function execute(ctx) {
|
|
|
1916
2052
|
out(` died ${total === null ? 'before a plan was accepted — nothing paid is re-payable'
|
|
1917
2053
|
: done >= total ? `at the close — all ${total} step(s) finished`
|
|
1918
2054
|
: `in step ${done + 1} of ${total}`}`);
|
|
1919
|
-
out(` resume
|
|
2055
|
+
out(` resume ${INVOKE} --resume ${runid}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL}${REGISTRY_TAIL} --approve ${PRINT_APPROVE}`);
|
|
1920
2056
|
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
2057
|
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
2058
|
}
|
|
@@ -1937,7 +2073,7 @@ async function execute(ctx) {
|
|
|
1937
2073
|
]);
|
|
1938
2074
|
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
2075
|
out('');
|
|
1940
|
-
const answer = (/** @type {string} */ tail) =>
|
|
2076
|
+
const answer = (/** @type {string} */ tail) => `${INVOKE} --resume ${runid}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL}${REGISTRY_TAIL}${tail} --approve ${PRINT_APPROVE}`;
|
|
1941
2077
|
for (const l of doorLines({
|
|
1942
2078
|
rerun: answer(' --decide rerun --text "<what you want done differently>"'),
|
|
1943
2079
|
accept: answer(' --decide accept'),
|
|
@@ -2059,7 +2195,7 @@ async function execute(ctx) {
|
|
|
2059
2195
|
// against has to travel in the printed command — an accept aimed at no registry (or at
|
|
2060
2196
|
// a different one) is the `no-row-for-run` refusal all over again, one hop later.
|
|
2061
2197
|
const doorRegistry = REGISTRY_ROW.minted ? ` --registry ${(ctx.registry ?? null)} --workflow ${REGISTRY_ROW.write.name}` : '';
|
|
2062
|
-
const answerDoor = (/** @type {string} */ tail) =>
|
|
2198
|
+
const answerDoor = (/** @type {string} */ tail) => `${INVOKE} --door ${runid}${tail}${doorRegistry} --approve ${PRINT_APPROVE}`;
|
|
2063
2199
|
// THE PROMISE READS OFF THE ROW, not off the class. `quarantined` on the door record
|
|
2064
2200
|
// says this run's credit is HELD; whether there is anything to RELEASE is a second
|
|
2065
2201
|
// question, and the honest answer is "only if a row was minted". Without `--registry`
|
|
@@ -2090,7 +2226,7 @@ async function execute(ctx) {
|
|
|
2090
2226
|
}
|
|
2091
2227
|
|
|
2092
2228
|
// the BRIDGE: on a green the agent's own plan is kept as the reusable artifact
|
|
2093
|
-
if (outcome === 'green' && plan && !leaks.length) {
|
|
2229
|
+
if (outcome === 'green' && plan && !leaks.length && !ctx.bundle) {
|
|
2094
2230
|
// one file PER GREEN, never one file per job: two cold runs of this job produced
|
|
2095
2231
|
// two DIFFERENT plans that both green (run 1: 3 steps, run 3: 2 steps), so a
|
|
2096
2232
|
// single `bridge-<job>.json` silently destroys the earlier bridge. WHICH bridge
|
|
@@ -2152,6 +2288,11 @@ async function execute(ctx) {
|
|
|
2152
2288
|
|
|
2153
2289
|
return exitCode;
|
|
2154
2290
|
} catch (/** @type {any} */ e) {
|
|
2291
|
+
// A claimed run that exited before its spine has a first record never started: it spent nothing and
|
|
2292
|
+
// owns no spine, so its row is released (folded out of the list, like a monthly refusal) rather than
|
|
2293
|
+
// left as a ghost that keeps the month reading "at least". Once the spine exists the run is a real,
|
|
2294
|
+
// listed run and ends by its own job-end. A failed release write never masks the original exit.
|
|
2295
|
+
if (claimedRow && !existsSync(claimedRow.spine)) releaseClaimedRow();
|
|
2155
2296
|
if (e instanceof ExitSignal) return e.code;
|
|
2156
2297
|
throw e;
|
|
2157
2298
|
}
|
|
@@ -2184,6 +2325,7 @@ function buildCtx(mode, opts) {
|
|
|
2184
2325
|
door: mode.door,
|
|
2185
2326
|
decide: mode.decide,
|
|
2186
2327
|
text: mode.text,
|
|
2328
|
+
...(opts.bundle === undefined ? {} : { bundle: opts.bundle }),
|
|
2187
2329
|
deps: opts.deps ?? {},
|
|
2188
2330
|
};
|
|
2189
2331
|
}
|
|
@@ -2293,7 +2435,7 @@ export async function main(argv, deps = {}) {
|
|
|
2293
2435
|
if (!existsSync(specPath)) {
|
|
2294
2436
|
die(`--job ${jobArg}: there is no spec at ${specPath}.\n`
|
|
2295
2437
|
+ ' The table row is the runner\'s half of a job (patient, seed, spine); the SPEC is yours — authored through\n'
|
|
2296
|
-
+ `
|
|
2438
|
+
+ ` ${commandFor('interview', deps.invokedAs)} and ${commandFor('author', deps.invokedAs)}, then signed. Nothing here can stand in for it: a job\n`
|
|
2297
2439
|
+ ' with no spec has no goal, no close, no budget and no hash to approve.');
|
|
2298
2440
|
}
|
|
2299
2441
|
try { spec = JSON.parse(readFileSync(specPath, 'utf8')); } catch (e) {
|
|
@@ -2305,7 +2447,7 @@ export async function main(argv, deps = {}) {
|
|
|
2305
2447
|
specPath = resolve(/** @type {string} */ (specArg));
|
|
2306
2448
|
if (!existsSync(specPath)) {
|
|
2307
2449
|
die(`--spec ${specPath} does not exist — a run needs the resolved-spec.json an authoring session actually wrote `
|
|
2308
|
-
+ '
|
|
2450
|
+
+ `(${commandFor('author', deps.invokedAs)}'s own \`out/resolved-spec.json\`), never a path nobody authored`);
|
|
2309
2451
|
}
|
|
2310
2452
|
let raw;
|
|
2311
2453
|
try { raw = readFileSync(specPath, 'utf8'); } catch (e) {
|
|
@@ -2345,8 +2487,8 @@ export async function main(argv, deps = {}) {
|
|
|
2345
2487
|
if (candidates.length === 0) {
|
|
2346
2488
|
die(`--spec ${specPath}: no prepared copy beside it — looked for a source-*/ directory carrying source.json in `
|
|
2347
2489
|
+ `${dirname(specPath)} and found none. A --spec run reads its workdir and seed off the copy the authoring `
|
|
2348
|
-
+
|
|
2349
|
-
+ '
|
|
2490
|
+
+ `session prepared; it is never typed or guessed. Prepare one first: ${commandFor('interview', deps.invokedAs)} (or `
|
|
2491
|
+
+ `${commandFor('author', deps.invokedAs)} directly against a --source) before running this spec.`);
|
|
2350
2492
|
}
|
|
2351
2493
|
if (candidates.length > 1) {
|
|
2352
2494
|
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.
|