bareloop 0.30.0 → 0.32.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 +154 -0
- package/bareloop.context.md +205 -10
- package/bin/bareloop.mjs +0 -1
- package/package.json +1 -1
- package/src/authorflow.js +22 -42
- package/src/authorjob.js +5 -8
- package/src/authorrun.js +42 -16
- package/src/cli.js +12 -6
- package/src/config.js +135 -0
- package/src/interviewrun.js +10 -2
- package/src/keysfile.js +156 -0
- package/src/ledger.js +26 -0
- package/src/monthly.js +352 -0
- package/src/panel/authorroutes.js +328 -0
- package/src/panel/authorsession.js +555 -0
- package/src/panel/index.html +1247 -50
- package/src/panel/server.js +449 -51
- package/src/panel/settingsroutes.js +230 -0
- package/src/planrun.js +61 -5
- package/src/promptregisters.js +3 -0
- package/src/providerrows.js +217 -0
- package/src/providers.js +84 -0
- package/src/replay.js +80 -2
- package/src/replayio.js +14 -3
- package/src/run.js +48 -2
- package/src/runlist.js +13 -0
- package/src/userrun.js +134 -13
- package/types/authorflow.d.ts +4 -21
- package/types/authorrun.d.ts +5 -0
- package/types/cli.d.ts +2 -1
- package/types/config.d.ts +48 -0
- package/types/interviewrun.d.ts +5 -0
- package/types/keysfile.d.ts +90 -0
- package/types/ledger.d.ts +14 -0
- package/types/monthly.d.ts +192 -0
- package/types/panel/authorroutes.d.ts +67 -0
- package/types/panel/authorsession.d.ts +53 -0
- package/types/panel/server.d.ts +42 -5
- package/types/panel/settingsroutes.d.ts +14 -0
- package/types/providerrows.d.ts +164 -0
- package/types/providers.d.ts +34 -0
- package/types/replay.d.ts +31 -0
- package/types/replayio.d.ts +12 -1
- package/types/run.d.ts +26 -1
- package/types/runlist.d.ts +1 -0
- package/types/userrun.d.ts +22 -0
package/src/replay.js
CHANGED
|
@@ -462,6 +462,24 @@ export function replayRun(spineEvents, auditEvents = [], { runId = null, auditAv
|
|
|
462
462
|
|
|
463
463
|
const spentUsd = jobEnd && typeof jobEnd.spentUsd === 'number' && Number.isFinite(jobEnd.spentUsd) ? jobEnd.spentUsd : null;
|
|
464
464
|
const spendComplete = jobEnd ? jobEnd.spendComplete === true && spentUsd !== null : false;
|
|
465
|
+
|
|
466
|
+
// draftSpentUsd (hamr's ruling 2026-09-28, "one cap covers drafting +
|
|
467
|
+
// run") — the AUTHORING pipeline's own spend on this job before it was
|
|
468
|
+
// signed, off `job-start`'s dedicated field (never `priorSpentUsd`, which
|
|
469
|
+
// is the RESUME fold `resumed` above keys on). `null` when the run never
|
|
470
|
+
// carried one (the common case, and every run predating this field) —
|
|
471
|
+
// never a decorative 0 a reader could mistake for "drafting spent
|
|
472
|
+
// nothing".
|
|
473
|
+
const draftSpentUsd = jobStart && typeof jobStart.draftSpentUsd === 'number' && Number.isFinite(jobStart.draftSpentUsd) && jobStart.draftSpentUsd > 0
|
|
474
|
+
? jobStart.draftSpentUsd : null;
|
|
475
|
+
// draftSpendComplete (hamr's ruling 2026-09-28) — was the drafting fold above
|
|
476
|
+
// EXACT? `null` when there is no drafting fold at all (the common case, and
|
|
477
|
+
// every run predating both fields) — never paired with a `draftSpentUsd` of
|
|
478
|
+
// `null`. An archive whose `job-start` carries `draftSpentUsd` but predates
|
|
479
|
+
// this companion field (src/run.js's own belt) defaults to `true` — it was
|
|
480
|
+
// exact then, the field simply didn't exist yet to say so.
|
|
481
|
+
const draftSpendComplete = draftSpentUsd === null ? null
|
|
482
|
+
: (jobStart.draftSpendComplete !== false);
|
|
465
483
|
const floorReasonList = floorReasons(spine, jobStart, spendComplete);
|
|
466
484
|
|
|
467
485
|
const wallMs = windowWallMs(parseTs(jobStart?.ts), parseTs(jobEnd?.ts));
|
|
@@ -1112,6 +1130,26 @@ export function replayRun(spineEvents, auditEvents = [], { runId = null, auditAv
|
|
|
1112
1130
|
if (planRoundsInitialArr.length > 0) {
|
|
1113
1131
|
const { spentUsd, unpricedRounds } = windowSpend(planRoundsInitialArr);
|
|
1114
1132
|
const b = partBehaviour(scoutTsHi, firstTs);
|
|
1133
|
+
// F: the plan part's own wall time (build item 8, 2026-09-28, run
|
|
1134
|
+
// mul5fofw: a $0.28/20623-token paid call read as "0.0s"). Root cause:
|
|
1135
|
+
// `scoutTsHi` is literally `planRoundsInitialArr[0].ts` — the plan
|
|
1136
|
+
// round's OWN completion timestamp (every round record is stamped
|
|
1137
|
+
// AFTER the call returns, never before) — so using it as plan's own
|
|
1138
|
+
// START boundary measures the call against ITSELF, leaving only the
|
|
1139
|
+
// few-ms gap to the next logged record (`firstTs`, the following
|
|
1140
|
+
// step-start) as the "duration". Measured on mul5fofw's real spine:
|
|
1141
|
+
// scout's own last round ts 11:15:00.335Z -> the plan round's ts
|
|
1142
|
+
// 11:16:22.667Z is 82.3s, the plan call's real wall time; the old
|
|
1143
|
+
// window (11:16:22.667Z -> 11:16:22.679Z, the step-start 12ms later)
|
|
1144
|
+
// could never show anything but ~0. Fixed HERE ONLY (the reader/
|
|
1145
|
+
// display side, never the seq-scoped `b`/`attempts` windows above,
|
|
1146
|
+
// which were already correctly bounded and are left untouched): the
|
|
1147
|
+
// wall window now runs from scout's own true end (its last round's
|
|
1148
|
+
// ts, or job-start when there was no scout) to plan's own true end
|
|
1149
|
+
// (its last round's ts) — never the NEXT phase's timestamps on either
|
|
1150
|
+
// side, which is what made this unmeasurable by construction.
|
|
1151
|
+
const scoutEndTs = scoutRoundsArr.length ? parseTs(scoutRoundsArr[scoutRoundsArr.length - 1].ts) : jobStartTs;
|
|
1152
|
+
const planEndTs = parseTs(planRoundsInitialArr[planRoundsInitialArr.length - 1].ts);
|
|
1115
1153
|
parts.push({
|
|
1116
1154
|
id: 'plan',
|
|
1117
1155
|
kind: 'plan',
|
|
@@ -1122,7 +1160,7 @@ export function replayRun(spineEvents, auditEvents = [], { runId = null, auditAv
|
|
|
1122
1160
|
toolCalls: b ? b.totalCalls : null,
|
|
1123
1161
|
byTool: b ? b.byTool : null,
|
|
1124
1162
|
blocked: b ? b.denied : null,
|
|
1125
|
-
wallMs: windowWallMs(
|
|
1163
|
+
wallMs: windowWallMs(scoutEndTs, planEndTs),
|
|
1126
1164
|
spentUsd,
|
|
1127
1165
|
unpricedRounds,
|
|
1128
1166
|
outcome: null,
|
|
@@ -1328,6 +1366,8 @@ export function replayRun(spineEvents, auditEvents = [], { runId = null, auditAv
|
|
|
1328
1366
|
stopReason,
|
|
1329
1367
|
spentUsd,
|
|
1330
1368
|
spendComplete,
|
|
1369
|
+
draftSpentUsd,
|
|
1370
|
+
draftSpendComplete,
|
|
1331
1371
|
floorReasons: floorReasonList,
|
|
1332
1372
|
transportRetryCount: transportRetries.length,
|
|
1333
1373
|
transportRetriesOutsideWindows,
|
|
@@ -1384,6 +1424,44 @@ function money2(n) {
|
|
|
1384
1424
|
return n === null ? 'unknown' : `$${n.toFixed(2)}`;
|
|
1385
1425
|
}
|
|
1386
1426
|
|
|
1427
|
+
/**
|
|
1428
|
+
* hamr's ruling 2026-09-28 ("one cap covers drafting + run") — the ONE
|
|
1429
|
+
* spend display for a run that may carry a drafting share, so every surface
|
|
1430
|
+
* that shows a run's money (the CLI's own `spent` line, the panel's Run tab
|
|
1431
|
+
* summary/counters, the Runs list row, the Audit header, the Job tab) reads
|
|
1432
|
+
* the same string rather than each hand-composing its own. `spendCore` is
|
|
1433
|
+
* already-formatted text (`money(...)`, or a `died`/floor line's "at least
|
|
1434
|
+
* $X") — this only decides whether to append the `(<draft> drafting)`
|
|
1435
|
+
* clause and the `of $<cap>` tail, never re-derives the money itself.
|
|
1436
|
+
* Unchanged (`spendCore` alone, or `spendCore of $cap`) when there is no
|
|
1437
|
+
* drafting share — a run without one prints exactly what it always did.
|
|
1438
|
+
*
|
|
1439
|
+
* hamr's ruling 2026-09-28 (2nd addendum) — an INCOMPLETE drafting fold
|
|
1440
|
+
* (`draftSpendComplete === false`) is a floor, not a total, and F6's rule
|
|
1441
|
+
* ("unpriced is never free, a floor never dresses as exact") applies here
|
|
1442
|
+
* exactly as it does to `spendCore` itself: the drafting clause reads `at
|
|
1443
|
+
* least $X drafting`, and because a reader summing the two figures would
|
|
1444
|
+
* otherwise take the LEADING number as exact too, `spendCore` itself also
|
|
1445
|
+
* gains the `at least ` prefix — unless it already carries one (a `died`
|
|
1446
|
+
* floor, or an already-prefixed caller), never doubled.
|
|
1447
|
+
* @param {string} spendCore already-formatted spend text (e.g. `$3.71` or `at least $2.00`)
|
|
1448
|
+
* @param {number|null} draftSpentUsd the drafting share, or null when there isn't one
|
|
1449
|
+
* @param {number|null} [budgetUsd] the signed cap, appended as `of $X.XX` when given
|
|
1450
|
+
* @param {boolean|null} [draftSpendComplete] whether the drafting fold above was EXACT;
|
|
1451
|
+
* `null`/`undefined`/`true` all read as exact (never a new enum value elsewhere) —
|
|
1452
|
+
* only an explicit `false` triggers the floor wording.
|
|
1453
|
+
* @returns {string}
|
|
1454
|
+
*/
|
|
1455
|
+
export function moneyWithDraft(spendCore, draftSpentUsd, budgetUsd, draftSpendComplete) {
|
|
1456
|
+
const hasDraft = draftSpentUsd !== null && draftSpentUsd !== undefined;
|
|
1457
|
+
const draftFloor = hasDraft && draftSpendComplete === false;
|
|
1458
|
+
const coreAlreadyFloor = /^(at least |≥)/.test(spendCore);
|
|
1459
|
+
const core = draftFloor && !coreAlreadyFloor ? `at least ${spendCore}` : spendCore;
|
|
1460
|
+
const draftPart = hasDraft ? ` (${draftFloor ? 'at least ' : ''}${money(draftSpentUsd)} drafting)` : '';
|
|
1461
|
+
const capPart = budgetUsd === undefined ? '' : ` of ${money2(budgetUsd)}`;
|
|
1462
|
+
return `${core}${draftPart}${capPart}`;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1387
1465
|
/**
|
|
1388
1466
|
* The `$`/`spend` column for one occurrence/iteration row: `unpricedRounds >
|
|
1389
1467
|
* 0` reads `unknown (N unpriced rounds)` — never `$0`, never a partial sum
|
|
@@ -1827,7 +1905,7 @@ export function summarizeForAllLine(summary) {
|
|
|
1827
1905
|
// shown on a run with zero retries.
|
|
1828
1906
|
outcome: `${summary.outcome ?? 'unknown'}${summary.transportRetryCount > 0 ? ` ⟲${summary.transportRetryCount}` : ''}`,
|
|
1829
1907
|
hadTransportRetries: summary.transportRetryCount > 0,
|
|
1830
|
-
spend: money(summary.spentUsd),
|
|
1908
|
+
spend: moneyWithDraft(money(summary.spentUsd), summary.draftSpentUsd, undefined, summary.draftSpendComplete),
|
|
1831
1909
|
wall: duration(summary.wallMs),
|
|
1832
1910
|
steps: stepsCol,
|
|
1833
1911
|
reason,
|
package/src/replayio.js
CHANGED
|
@@ -95,7 +95,8 @@ export function resolveSiblings(spinePath) {
|
|
|
95
95
|
* Read one run's spine (+ its gate-audit sidecar, when present) off disk and
|
|
96
96
|
* hand both to {@link replayRun}. Read-only, $0, mints no verdict.
|
|
97
97
|
* @param {string} spinePath absolute or cwd-relative path to a `.jsonl` spine
|
|
98
|
-
* @param {{preParsedSpine?: {records: any[], skipped: number}, skipAudit?: boolean
|
|
98
|
+
* @param {{preParsedSpine?: {records: any[], skipped: number}, skipAudit?: boolean,
|
|
99
|
+
* auditPathOverride?: string|null}} [opts]
|
|
99
100
|
* `preParsedSpine`: avoids a second parse of the same file when the caller
|
|
100
101
|
* ({@link listSpines}) already read it once to run {@link looksLikeSpine}.
|
|
101
102
|
* `skipAudit` (PR #23 review item 5, 2026-08-26, carried over): a
|
|
@@ -103,11 +104,21 @@ export function resolveSiblings(spinePath) {
|
|
|
103
104
|
* `summarizeForAllLine` reads none of the fields that sidecar feeds inside
|
|
104
105
|
* `replayRun`, so reading every run's sidecar in a directory listing was
|
|
105
106
|
* pure unused I/O, multiplied by every spine found.
|
|
107
|
+
* `auditPathOverride` (build item 7, 2026-09-28): when a caller has ALREADY
|
|
108
|
+
* resolved the real audit path through the one shared resolver
|
|
109
|
+
* (`src/panel/server.js`'s `resolveAuditPathForRow`, which also covers a
|
|
110
|
+
* still-LIVE run's during-run sidecar location — this function's own
|
|
111
|
+
* `resolveSiblings` call only ever finds the FINISHED-run convention), it
|
|
112
|
+
* passes that path here (`null` included) instead of letting this
|
|
113
|
+
* function re-derive a finished-only path on its own. `undefined` (the
|
|
114
|
+
* default) keeps the original resolveSiblings-only behaviour — every
|
|
115
|
+
* existing caller is unaffected.
|
|
106
116
|
* @returns {ReturnType<typeof replayRun>}
|
|
107
117
|
*/
|
|
108
118
|
export function replayOne(spinePath, opts = {}) {
|
|
109
|
-
const { preParsedSpine, skipAudit = false } = opts;
|
|
110
|
-
const { runId, auditPath } = resolveSiblings(spinePath);
|
|
119
|
+
const { preParsedSpine, skipAudit = false, auditPathOverride } = opts;
|
|
120
|
+
const { runId, auditPath: resolvedAuditPath } = resolveSiblings(spinePath);
|
|
121
|
+
const auditPath = auditPathOverride !== undefined ? auditPathOverride : resolvedAuditPath;
|
|
111
122
|
const spine = preParsedSpine ?? parseJsonl(spinePath);
|
|
112
123
|
const audit = (!skipAudit && auditPath) ? parseJsonl(auditPath) : { records: [], skipped: 0 };
|
|
113
124
|
// `auditAvailable`: true only when a sidecar was actually FOUND AND READ.
|
package/src/run.js
CHANGED
|
@@ -134,6 +134,29 @@ async function primitiveSmoke(workdir) {
|
|
|
134
134
|
* load gate refuses one: a refusal returns the distinct `recipe-stale` outcome with zero
|
|
135
135
|
* spend rather than silently drafting cold, because starting a paid run on a decision
|
|
136
136
|
* nobody made is the same class of error as widening a cap to manufacture a green.
|
|
137
|
+
* @param {number} [opts.draftSpentUsd=0] hamr's ruling 2026-09-28 ("one cap covers
|
|
138
|
+
* drafting + run") — money the AUTHORING pipeline (`authorCloseForJob`/
|
|
139
|
+
* `prepareSigning`, run under the SAME signed `budgetUsd` as its ceiling) already
|
|
140
|
+
* spent on THIS job before it was ever signed. The signed `budgetUsd` stays the
|
|
141
|
+
* one number in the spec hash; this shrinks the RUN's own enforced ceiling by
|
|
142
|
+
* what drafting already took, so the two phases never spend more than the one
|
|
143
|
+
* cap together. Deliberately NOT `priorSpentUsd` — that key means "a previous
|
|
144
|
+
* ATTEMPT of this same run died and folded its spend forward" and is read by
|
|
145
|
+
* `readResume`/`replay.js`'s `resumed` flag; a drafting spend is not a resume and
|
|
146
|
+
* must never make an ordinary first run of a job read as one. It is also never
|
|
147
|
+
* folded into `spentUsd` (the run's own ledger, cumulative-so-far and what a
|
|
148
|
+
* resume's `priorSpentUsd` seeds) or into `engagementSpentUsd` — drafting is a
|
|
149
|
+
* DIFFERENT phase's spend, reported beside the run's, never inside it. A garbage
|
|
150
|
+
* value (non-finite, negative) is read as 0, belted exactly like `priorSpentUsd`.
|
|
151
|
+
* @param {boolean} [opts.draftSpendComplete=true] hamr's ruling 2026-09-28 — was the
|
|
152
|
+
* `draftSpentUsd` fold above EXACT? The authoring pipeline's own metered-call list can
|
|
153
|
+
* go unpriced (F6) exactly like a resumed attempt's can, and the unknown does not heal
|
|
154
|
+
* by folding forward. A NEW field beside `draftSpentUsd`, never a new state inside
|
|
155
|
+
* `spendComplete`/`priorSpendComplete` — those answer "is THIS run's own ledger exact,"
|
|
156
|
+
* this answers "was the OTHER phase's ledger exact," and the two must never merge into
|
|
157
|
+
* one flag or a floor drafting spend could read as an exact run. Read only when
|
|
158
|
+
* `draftSpentUsd` itself is emitted; a garbage value is read as `true` (the same belt
|
|
159
|
+
* `priorSpendComplete` gets — an unreadable flag never manufactures a false floor).
|
|
137
160
|
* @param {number} [opts.priorSpentUsd=0] RESUME (module C) — money a PREVIOUS, killed
|
|
138
161
|
* attempt of this same try already spent. It seeds the ledger, so the restarted
|
|
139
162
|
* attempt runs under the REMAINDER of the signed `budgetUsd` and the terminal
|
|
@@ -242,7 +265,7 @@ async function primitiveSmoke(workdir) {
|
|
|
242
265
|
* deliberately NOT in the resumable/checkpoint set (`src/reuse.js`) — an
|
|
243
266
|
* unknown in-flight state is not a known-safe resume point.
|
|
244
267
|
*/
|
|
245
|
-
export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider = null, judgeModel = null, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, closeDir = null, layerRoot = false, readShim = false, scout = true, bridge = null, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null, heldRuling = null, reviewDoor = null, doorRerun = null, resumable = true }) {
|
|
268
|
+
export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider = null, judgeModel = null, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, closeDir = null, layerRoot = false, readShim = false, scout = true, bridge = null, draftSpentUsd = 0, draftSpendComplete = true, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null, heldRuling = null, reviewDoor = null, doorRerun = null, resumable = true }) {
|
|
246
269
|
// THE READ SHIM's ARM, resolved at the door — the FIRST thing this entry does,
|
|
247
270
|
// before the ledger, before the approval gate, before a byte of the spec is read.
|
|
248
271
|
// An unrecognised spelling throws here at zero cost instead of being coerced by
|
|
@@ -270,6 +293,18 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
270
293
|
* ENGAGEMENT's spend can be stated side by side on every terminal. Two things
|
|
271
294
|
* counted by one number will eventually be asked to disagree. */
|
|
272
295
|
const chainFoldUsd = spentUsd;
|
|
296
|
+
// hamr's ruling 2026-09-28 — the DRAFTING fold, kept apart from the RESUME
|
|
297
|
+
// fold above on purpose (see the param's own doc): it shrinks the run's
|
|
298
|
+
// enforced ceiling below but never seeds `spentUsd`/`chainFoldUsd`, so it
|
|
299
|
+
// can never be read as "a previous attempt of THIS run died" by `readResume`
|
|
300
|
+
// or by `replay.js`'s `resumed` flag (which keys on `priorSpentUsd`'s
|
|
301
|
+
// presence alone). Belted exactly like `priorSpentUsd`: a garbage value
|
|
302
|
+
// (non-finite, negative) reads as 0 rather than poisoning the ceiling
|
|
303
|
+
// arithmetic into "no cap" or "already over".
|
|
304
|
+
const draftFoldUsd = typeof draftSpentUsd === 'number' && Number.isFinite(draftSpentUsd) && draftSpentUsd > 0 ? draftSpentUsd : 0;
|
|
305
|
+
// Belted exactly like `priorSpendComplete`'s own read below: a garbage value
|
|
306
|
+
// (non-boolean) reads as `true` rather than manufacturing a false floor.
|
|
307
|
+
const draftFoldComplete = draftSpendComplete !== false;
|
|
273
308
|
let unpriced = false;
|
|
274
309
|
// RESUME (v1.46 §3): was the FOLD itself exact? `readResume` marks it false when any
|
|
275
310
|
// round inside the dead attempt came back unpriced, and that unknown does not heal by
|
|
@@ -379,6 +414,11 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
379
414
|
// A fold of $0 that is NOT exact is still a fold — the unknown is the whole of what
|
|
380
415
|
// it inherited, and it is the only field that can carry it forward.
|
|
381
416
|
...(spentUsd > 0 || priorFloor ? { priorSpentUsd: spentUsd, priorSpendComplete: !priorFloor } : {}),
|
|
417
|
+
// …and the DRAFTING fold, on its own dedicated field — never `priorSpentUsd`
|
|
418
|
+
// (see the param's own doc above: that key means "resumed", and this run
|
|
419
|
+
// was not). Emitted only when there is one, same "a decorative 0 is
|
|
420
|
+
// indistinguishable from none" rule every other fold field here follows.
|
|
421
|
+
...(draftFoldUsd > 0 ? { draftSpentUsd: draftFoldUsd, draftSpendComplete: draftFoldComplete } : {}),
|
|
382
422
|
// …and the TIME fold, which F103 splits in two. `priorWallMs` is what the next
|
|
383
423
|
// reader ADDS to (the bound), and it is engagement-scoped: a rerun opens a fresh
|
|
384
424
|
// engagement, so the minutes the person did not commission are not a bound on
|
|
@@ -493,7 +533,13 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
493
533
|
{
|
|
494
534
|
const outcome = await runPlan(job, {
|
|
495
535
|
workdir, provider, nativeProvider, providerFor, judgeProvider, judgeModel, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, closeDir, layerRoot, readShim, scout, bridge, priorWallMs: chainWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun, priorSpentUsd: chainFoldUsd, resumable,
|
|
496
|
-
|
|
536
|
+
// hamr's ruling 2026-09-28 — ONE cap covers drafting + run: the run's
|
|
537
|
+
// enforced ceiling is the signed `budgetUsd` (unchanged, still in the
|
|
538
|
+
// spec hash) MINUS whatever drafting already spent on this job before
|
|
539
|
+
// signing. Advertised = enforced: this is the ONE place the ceiling
|
|
540
|
+
// arithmetic lives, so every readout of "what's left" is this same
|
|
541
|
+
// subtraction, never a second one computed elsewhere.
|
|
542
|
+
remainingUsd: () => Math.min(shellCapUsd, job.budgetUsd - draftFoldUsd - spentUsd),
|
|
497
543
|
isUnpriced: () => unpriced, // F6: let the plan flow bail in-flight, not just after it returns
|
|
498
544
|
spendComplete, // …and let its money-halt readout say whether the remaining it quotes is exact
|
|
499
545
|
});
|
package/src/runlist.js
CHANGED
|
@@ -128,6 +128,19 @@ function runidForSpine(spinePath) {
|
|
|
128
128
|
return resolveSiblings(spinePath).runId;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
// DIED (hamr's ruling B, 2026-09-25): a run with no `job-end` never shares
|
|
132
|
+
// the failed glyph — [✗] stays reserved for a run whose close/arbiter
|
|
133
|
+
// actually rendered a "no" (a real result). A spine that just stops, with
|
|
134
|
+
// no ending ever recorded, is a DIFFERENT fact (killed, crashed, or the
|
|
135
|
+
// machine slept) and gets its own [?] glyph. The distinguishing signal is
|
|
136
|
+
// the spine FILE's own mtime, never wall-clock "now minus job-start" (a
|
|
137
|
+
// resumed/paused run can legitimately sit quiet for a long time without
|
|
138
|
+
// having died): still fresh (written to within this window) reads as the
|
|
139
|
+
// existing `running` [▶] state; older than this reads as died. Tighten-only,
|
|
140
|
+
// named so a future change is a deliberate, visible edit. Lives here (not in
|
|
141
|
+
// the panel) because the monthly limit reads the same rule (src/monthly.js).
|
|
142
|
+
export const DIED_MTIME_MS = 10 * 60 * 1000;
|
|
143
|
+
|
|
131
144
|
// A backfill scan of a real patients directory can be arbitrarily deep
|
|
132
145
|
// (person-path runs archive several levels down: `<dir>/<proj>/out/
|
|
133
146
|
// source-<x>/<proj>-bareloop/u-<id>.jsonl`, 4 levels under the dir a person
|
package/src/userrun.js
CHANGED
|
@@ -30,6 +30,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
30
30
|
import { execFileSync, spawn } from 'node:child_process';
|
|
31
31
|
import { join, resolve, dirname } from 'node:path';
|
|
32
32
|
import { runJob } from './run.js';
|
|
33
|
+
import { moneyWithDraft } from './replay.js';
|
|
33
34
|
import { jobSpecHash, resolveWorkerModel, validateJob } from './job.js';
|
|
34
35
|
import { readShimArm } from './readshim.js';
|
|
35
36
|
import { closeStagesOf } from './plan.js';
|
|
@@ -72,6 +73,10 @@ import { answerReviewDoor, doorRecordOf, doorAgeGate } from './reviewdoor.js';
|
|
|
72
73
|
import { coldReset, moveStaleGateAudit } from './u-patient.js';
|
|
73
74
|
// PANEL-BUILD.md P1 — the one run list (`~/.config/bareloop/runs.jsonl`).
|
|
74
75
|
import { appendRun } from './runlist.js';
|
|
76
|
+
import { keysForDoor } from './keysfile.js';
|
|
77
|
+
import { checkMonthlyRoom, monthlyRefusalText } from './monthly.js';
|
|
78
|
+
import { ConfigError } from './config.js';
|
|
79
|
+
import { applyConfiguredKey, keyNameFor, rowsForHome } from './providerrows.js';
|
|
75
80
|
// the banner's wall arithmetic, extracted so it is reachable by a test (F83): the
|
|
76
81
|
// end-of-run readout sits past the approval gate, so nothing could ever drive it here
|
|
77
82
|
import { wallLine, doomedResume, deathAtOf, evidencePackage, doorLines, resumeAtLines, reviewDoorPackage, runDoorLines, tokensLine, doorTimingRedLines } from './u-readout.js';
|
|
@@ -218,6 +223,8 @@ class ExitSignal extends Error {
|
|
|
218
223
|
/**
|
|
219
224
|
* @typedef {object} Deps
|
|
220
225
|
* @property {Record<string,string|undefined>} [env]
|
|
226
|
+
* @property {string} [keysHome] P4a — a test seam naming the home whose `.env` keys file is loaded
|
|
227
|
+
* even when `env` is injected; production never sets it.
|
|
221
228
|
* @property {(s: string) => void} [out]
|
|
222
229
|
* @property {(s: string) => void} [err]
|
|
223
230
|
* @property {any} [provider] the TEST SEAM: supplying this skips the real-key
|
|
@@ -242,6 +249,15 @@ class ExitSignal extends Error {
|
|
|
242
249
|
* @property {string|null} [model] the raw `--model` tier name (`sonnet`/`haiku`), or null for the default
|
|
243
250
|
* @property {string|null} [readShim] the raw `--read-shim` arm name, or null for the default
|
|
244
251
|
* @property {string|null} [scout] the raw `--scout` value (`on`/`off`), or null for the default
|
|
252
|
+
* @property {string|null} [draftSpentUsd] the raw `--draft-spent-usd` value (hamr's
|
|
253
|
+
* ruling 2026-09-28, "one cap covers drafting + run"), or null when the job never
|
|
254
|
+
* drafted through the panel/`bareloop author`. Parsed and validated in `execute`
|
|
255
|
+
* (finite, >= 0, else refused) — never coerced silently, the same param-guard
|
|
256
|
+
* class `--read-shim`/`--scout` already are.
|
|
257
|
+
* @property {boolean} [draftSpendIncomplete] the raw `--draft-spend-incomplete` presence-
|
|
258
|
+
* flag (hamr's ruling 2026-09-28, 2nd addendum) — whether the drafting fold above was a
|
|
259
|
+
* FLOOR, never an exact figure. A new field beside `draftSpentUsd`, same param-guard
|
|
260
|
+
* class, no value to misread.
|
|
245
261
|
* @property {string|null} [registry] a bridge registry directory
|
|
246
262
|
* @property {string|null} [workflow] the registry's workflow name (defaults to `spec.job`)
|
|
247
263
|
* @property {string|null} [approve] the spec hash being signed; omitted previews only
|
|
@@ -263,6 +279,8 @@ class ExitSignal extends Error {
|
|
|
263
279
|
* @property {string|null} model
|
|
264
280
|
* @property {string|null} readShim
|
|
265
281
|
* @property {string|null} scout
|
|
282
|
+
* @property {string|null} draftSpentUsd
|
|
283
|
+
* @property {boolean} draftSpendIncomplete
|
|
266
284
|
* @property {string|null} registry
|
|
267
285
|
* @property {string|null} workflow
|
|
268
286
|
* @property {string|null} approve
|
|
@@ -294,9 +312,19 @@ class ExitSignal extends Error {
|
|
|
294
312
|
*/
|
|
295
313
|
async function execute(ctx) {
|
|
296
314
|
const deps = ctx.deps ?? {};
|
|
297
|
-
const env = deps.env ?? process.env;
|
|
298
315
|
const out = deps.out ?? ((/** @type {string} */ s) => { console.log(s); });
|
|
299
316
|
const err = deps.err ?? ((/** @type {string} */ s) => { console.error(s); });
|
|
317
|
+
// P4a item 1 — the keys file (`~/.config/bareloop/.env`) fills what the shell leaves
|
|
318
|
+
// unset; the shell wins. An injected `deps.env` skips the file unless `deps.keysHome`
|
|
319
|
+
// names a home (a test never reads the real one). Values go only to provider
|
|
320
|
+
// construction below.
|
|
321
|
+
const keys = keysForDoor(deps);
|
|
322
|
+
const env = keys.env;
|
|
323
|
+
if (keys.warning && deps.env === undefined) err(`WARNING: ${keys.warning}`);
|
|
324
|
+
// config.json (monthly limit, chosen key variables) lives beside the run list. An injected
|
|
325
|
+
// `env` with no injected home means a test — the real ~/.config/bareloop is never read then.
|
|
326
|
+
const cfgHome = keys.home ?? deps.runlistHome;
|
|
327
|
+
const cfgOn = cfgHome !== undefined;
|
|
300
328
|
/** every operator/config stop this engine makes, in one exit code — thrown,
|
|
301
329
|
* never `process.exit()`'d, so this stays a library function (constraint:
|
|
302
330
|
* "never process.exit() inside the library — return an exit code").
|
|
@@ -394,6 +422,39 @@ async function execute(ctx) {
|
|
|
394
422
|
* so a resume never silently drops it and runs the default under the arm's label. */
|
|
395
423
|
const SCOUT_TAIL = scoutArg !== null && !SCOUT ? ' --scout off' : '';
|
|
396
424
|
|
|
425
|
+
// --draft-spent-usd (hamr's ruling 2026-09-28, "one cap covers drafting +
|
|
426
|
+
// run") — runner territory exactly like --read-shim/--scout above: the
|
|
427
|
+
// spec's signed budgetUsd is unaffected, this only shrinks THIS leg's own
|
|
428
|
+
// enforced ceiling. Same param-guard class and the same reason: an
|
|
429
|
+
// unreadable value here would either silently widen the ceiling (garbage
|
|
430
|
+
// read as 0) or silently narrow it (garbage read as Infinity) — neither is
|
|
431
|
+
// acceptable, so this refuses at argv, before the approval gate, rather
|
|
432
|
+
// than coercing.
|
|
433
|
+
const draftSpentArg = ctx.draftSpentUsd ?? null;
|
|
434
|
+
let DRAFT_SPENT_USD = 0;
|
|
435
|
+
if (draftSpentArg !== null) {
|
|
436
|
+
const v = Number(draftSpentArg);
|
|
437
|
+
if (draftSpentArg.trim() === '' || !Number.isFinite(v) || v < 0) {
|
|
438
|
+
err(`--draft-spent-usd "${draftSpentArg}" is not a finite number >= 0 — refusing rather than coercing it into $0 (which would widen the ceiling) or letting a NaN poison the ceiling arithmetic`);
|
|
439
|
+
throw new ExitSignal(2);
|
|
440
|
+
}
|
|
441
|
+
DRAFT_SPENT_USD = v;
|
|
442
|
+
}
|
|
443
|
+
// --draft-spend-incomplete (hamr's ruling 2026-09-28, 2nd addendum) — a NEW
|
|
444
|
+
// field beside --draft-spent-usd, never a value inside it: whether the
|
|
445
|
+
// drafting fold above was EXACT. Boolean presence-flag, same class as
|
|
446
|
+
// --review-door above (no argument, no value to misread as 0/Infinity).
|
|
447
|
+
// Meaningless alone (no drafting fold to qualify) but never refused for
|
|
448
|
+
// being passed without --draft-spent-usd — a re-invocation tail that always
|
|
449
|
+
// pairs the two never needs a second guard to prove it did.
|
|
450
|
+
const DRAFT_SPEND_INCOMPLETE = ctx.draftSpendIncomplete === true;
|
|
451
|
+
/** every re-invocation this script PRINTS carries the drafting fold, the
|
|
452
|
+
* same SHIM_TAIL/SCOUT_TAIL rule: a resume that dropped it would silently
|
|
453
|
+
* WIDEN the ceiling back up by the drafting spend on its next leg (and
|
|
454
|
+
* dropping the incomplete flag would silently turn a floor into an exact
|
|
455
|
+
* figure on the next leg's own job-start). */
|
|
456
|
+
const DRAFT_TAIL = DRAFT_SPENT_USD > 0 ? ` --draft-spent-usd ${DRAFT_SPENT_USD}${DRAFT_SPEND_INCOMPLETE ? ' --draft-spend-incomplete' : ''}` : '';
|
|
457
|
+
|
|
397
458
|
const WORKDIR = target.workdir;
|
|
398
459
|
const SEED = target.seed;
|
|
399
460
|
const specHash = jobSpecHash(spec);
|
|
@@ -1040,7 +1101,7 @@ async function execute(ctx) {
|
|
|
1040
1101
|
// right key only by accident of the runner's own generic error message,
|
|
1041
1102
|
// never from this hint. `providerEntry.envKey` is the same resolved name
|
|
1042
1103
|
// the real key check at launch (`:1157-1158` below) reads.
|
|
1043
|
-
const invoke = (/** @type {string} */ tail) => ` ${providerEntry.envKey}=... node scripts/run-u.mjs ${SELECTOR}${dead ? ` --resume ${RESUME}` : ''}${SHIM_TAIL}${SCOUT_TAIL}${tail} --approve ${specHash}`;
|
|
1104
|
+
const invoke = (/** @type {string} */ tail) => ` ${providerEntry.envKey}=... node scripts/run-u.mjs ${SELECTOR}${dead ? ` --resume ${RESUME}` : ''}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL}${tail} --approve ${specHash}`;
|
|
1044
1105
|
/** the door the operator has already picked, as flags — hoisted out of the else
|
|
1045
1106
|
* below so the inhibitor line at the bottom can print the WHOLE command rather
|
|
1046
1107
|
* than a shape the operator has to assemble. Empty on an ordinary run and on the
|
|
@@ -1205,8 +1266,8 @@ async function execute(ctx) {
|
|
|
1205
1266
|
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.`);
|
|
1206
1267
|
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`);
|
|
1207
1268
|
out(' resume the SAME runid, whenever you want, with the door you pick then:');
|
|
1208
|
-
out(` node scripts/run-u.mjs ${SELECTOR} --resume ${RESUME}${SHIM_TAIL}${SCOUT_TAIL} --decide accept --approve ${specHash}`);
|
|
1209
|
-
out(` node scripts/run-u.mjs ${SELECTOR} --resume ${RESUME}${SHIM_TAIL}${SCOUT_TAIL} --decide rerun --text "<what you want done differently>" --approve ${specHash}`);
|
|
1269
|
+
out(` node scripts/run-u.mjs ${SELECTOR} --resume ${RESUME}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL} --decide accept --approve ${specHash}`);
|
|
1270
|
+
out(` node scripts/run-u.mjs ${SELECTOR} --resume ${RESUME}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL} --decide rerun --text "<what you want done differently>" --approve ${specHash}`);
|
|
1210
1271
|
out(` read the same command with no --decide re-prints the evidence package you just looked at`);
|
|
1211
1272
|
throw new ExitSignal(0);
|
|
1212
1273
|
}
|
|
@@ -1297,22 +1358,32 @@ async function execute(ctx) {
|
|
|
1297
1358
|
// the same shape src/cli.js's `doRun` already uses) — a caller that hands in
|
|
1298
1359
|
// its own provider IS the run, so nothing here needs a real secret.
|
|
1299
1360
|
if (!deps.provider) {
|
|
1361
|
+
// P4b — the Settings row (keys file line) whose API shape + Base URL match this spec
|
|
1362
|
+
// supplies the key VARIABLE, standing in for the provider's built-in one; no matching
|
|
1363
|
+
// row = the built-in. Names only — values come from the
|
|
1364
|
+
// env / keys file. The judge follows the worker's choice when it is the same provider
|
|
1365
|
+
// (the same rule that gives it the worker's baseUrl below).
|
|
1366
|
+
const keyCfg = cfgOn ? rowsForHome(cfgHome) : [];
|
|
1367
|
+
const workerEnv = applyConfiguredKey(env, spec.provider, spec.baseUrl, keyCfg, spec.model);
|
|
1368
|
+
const judgeEnv = judge.provider === spec.provider ? workerEnv : applyConfiguredKey(env, judge.provider, undefined, keyCfg, judge.model);
|
|
1369
|
+
/** the variable NAME this worker's key is read from (the person's pick, else built-in) — for messages only */
|
|
1370
|
+
const workerKeyName = keyNameFor(spec.provider, spec.baseUrl, keyCfg, spec.model).name;
|
|
1300
1371
|
// A green anthropic-api job's behaviour is byte-identical to before: its
|
|
1301
1372
|
// worker key IS `ANTHROPIC_API_KEY`, demanded as ever.
|
|
1302
|
-
workerApiKey =
|
|
1303
|
-
if (!workerApiKey) { err(`${
|
|
1373
|
+
workerApiKey = workerEnv[providerEntry.envKey];
|
|
1374
|
+
if (!workerApiKey) { err(`${workerKeyName} not set (secrets load from the environment — never the tree)`); throw new ExitSignal(2); }
|
|
1304
1375
|
// F181 — a key that reads as "set" above can still carry a line break/
|
|
1305
1376
|
// control char/stray whitespace (a two-line secret-store entry, e.g.) and
|
|
1306
1377
|
// crash Node's own header-encode inside the paid span. Refuse at the SAME
|
|
1307
1378
|
// door, before any provider is constructed; never echo the value.
|
|
1308
1379
|
const workerKeyProblem = apiKeyProblem(workerApiKey);
|
|
1309
|
-
if (workerKeyProblem) { err(`${
|
|
1380
|
+
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); }
|
|
1310
1381
|
// The judge's KEY follows the RESOLVED judge provider's own env var, with
|
|
1311
1382
|
// `JUDGE_API_KEY` as a role-named override in front of it (PRD item 32.3). The
|
|
1312
1383
|
// override matters most in the case this item exists for: a job whose worker and
|
|
1313
1384
|
// judge are the SAME provider but different accounts, and the anthropic case
|
|
1314
1385
|
// where one variable used to do two jobs.
|
|
1315
|
-
judgeApiKey = env.JUDGE_API_KEY ??
|
|
1386
|
+
judgeApiKey = env.JUDGE_API_KEY ?? judgeEnv[judgeEntry.envKey];
|
|
1316
1387
|
if (JUDGES && !judgeApiKey) {
|
|
1317
1388
|
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)"}.`);
|
|
1318
1389
|
err(`Neither JUDGE_API_KEY nor ${judgeEntry.envKey} is set — secrets load from the environment, never the tree.`);
|
|
@@ -1332,6 +1403,34 @@ async function execute(ctx) {
|
|
|
1332
1403
|
}
|
|
1333
1404
|
}
|
|
1334
1405
|
|
|
1406
|
+
// THE MONTHLY $ LIMIT (PANEL-BUILD.md P4a, hamr 2026-09-29: "drop time keep money") — the
|
|
1407
|
+
// ONE run-start seam. Every run this door starts (the CLI's `bareloop run-u` and the panel,
|
|
1408
|
+
// which spawns it) passes here after the signature and the key gates and BEFORE the run
|
|
1409
|
+
// list row, the patient reset or any token: a cap larger than what is left this month does
|
|
1410
|
+
// not start, and nothing is spent. This leg's exposure is the whole signed cap on a cold
|
|
1411
|
+
// start (it covers drafting + run) and only the REMAINDER on a resume/door-rerun, whose
|
|
1412
|
+
// earlier spend is already in the month's total (a resume's killed leg counts its real spend only,
|
|
1413
|
+
// never its unspent cap — `resumingSpine` — since the remainder asked for here covers that). No limit set = no check. An unreadable
|
|
1414
|
+
// config.json refuses too — a broken instrument never silently reads "no limit".
|
|
1415
|
+
{
|
|
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
|
+
/** @type {string|null} */
|
|
1419
|
+
let refusal = null;
|
|
1420
|
+
try {
|
|
1421
|
+
if (cfgOn) refusal = monthlyRefusalText(checkMonthlyRoom({ capUsd: legCapUsd, home: cfgHome, resumingSpine: deadSpineFile }));
|
|
1422
|
+
} catch (e) {
|
|
1423
|
+
if (!(e instanceof ConfigError)) throw e;
|
|
1424
|
+
err(`${e.message} — refusing to start rather than guess the monthly limit. Nothing spent.`);
|
|
1425
|
+
throw new ExitSignal(2);
|
|
1426
|
+
}
|
|
1427
|
+
if (refusal !== null) {
|
|
1428
|
+
err(refusal);
|
|
1429
|
+
err('Nothing spent. Lower the cap, or raise the monthly limit in Settings.');
|
|
1430
|
+
throw new ExitSignal(2);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1335
1434
|
// `wd`/`spineDir` are derived once, above the resume reader that needs them; only the
|
|
1336
1435
|
// directory's CREATION belongs here, after the preview/approval gates have exited.
|
|
1337
1436
|
mkdirSync(spineDir, { recursive: true });
|
|
@@ -1458,6 +1557,14 @@ async function execute(ctx) {
|
|
|
1458
1557
|
// driver's plan is a row nobody can audit from its own log.
|
|
1459
1558
|
out(` ${READ_SHIM_LABEL}`);
|
|
1460
1559
|
out(` ${SCOUT_LABEL}`);
|
|
1560
|
+
// hamr's ruling 2026-09-28 — advertised = enforced: the banner states the
|
|
1561
|
+
// SIGNED cap above unconditionally (it never changes, same spec hash), and
|
|
1562
|
+
// this line states the drafting fold whenever there is one, so the number
|
|
1563
|
+
// this run actually enforces (Cap $ − drafting spent) is never a second,
|
|
1564
|
+
// silent figure a reader has to compute themselves.
|
|
1565
|
+
if (DRAFT_SPENT_USD > 0) {
|
|
1566
|
+
out(` drafting spent $${DRAFT_SPENT_USD.toFixed(4)} — this run's own enforced ceiling is $${(spec.budgetUsd - DRAFT_SPENT_USD).toFixed(4)} of the signed $${spec.budgetUsd}`);
|
|
1567
|
+
}
|
|
1461
1568
|
|
|
1462
1569
|
// F67 — the OUTSIDE watchdog, started before the run and sharing nothing with it.
|
|
1463
1570
|
// Every guard bareloop had lived inside this process, and ms3197n8/ms3jh76q proved
|
|
@@ -1642,6 +1749,8 @@ async function execute(ctx) {
|
|
|
1642
1749
|
closeDir: spineDir,
|
|
1643
1750
|
readShim: READ_SHIM,
|
|
1644
1751
|
scout: SCOUT,
|
|
1752
|
+
draftSpentUsd: DRAFT_SPENT_USD,
|
|
1753
|
+
draftSpendComplete: !DRAFT_SPEND_INCOMPLETE,
|
|
1645
1754
|
// RESUME: the money and the wall the halted run already burned are FOLDED IN (so
|
|
1646
1755
|
// the signed ceiling cannot widen by being re-invoked), and the checkpoint it
|
|
1647
1756
|
// reached is handed over so the plan is reloaded rather than re-drafted and the
|
|
@@ -1731,7 +1840,15 @@ async function execute(ctx) {
|
|
|
1731
1840
|
const writes = audit.filter((e) => e.decision === 'allow' && (e.action?.type === 'write' || e.action?.type === 'edit'));
|
|
1732
1841
|
|
|
1733
1842
|
out(`\noutcome ${outcome}`);
|
|
1734
|
-
|
|
1843
|
+
// hamr's ruling 2026-09-28 — the drafting share (this leg's own job-start
|
|
1844
|
+
// record) rides beside the run's own spend via the ONE shared formatter,
|
|
1845
|
+
// so this line never disagrees with the panel's Run tab/Runs list about
|
|
1846
|
+
// what "$X of $Y" means for a job that drafted through the panel/author.
|
|
1847
|
+
const jobStartRec = events.find((e) => e.type === 'job-start');
|
|
1848
|
+
const legDraftSpentUsd = typeof jobStartRec?.draftSpentUsd === 'number' && jobStartRec.draftSpentUsd > 0 ? jobStartRec.draftSpentUsd : null;
|
|
1849
|
+
const legDraftSpendComplete = legDraftSpentUsd === null ? null : jobStartRec.draftSpendComplete !== false;
|
|
1850
|
+
const spendCore = je?.spentUsd == null ? 'UNKNOWN' : `${je.spendComplete === false ? '≥' : ''}$${je.spentUsd.toFixed(4)}`;
|
|
1851
|
+
out(`spent ${moneyWithDraft(spendCore, legDraftSpentUsd, spec.budgetUsd, legDraftSpendComplete)}`);
|
|
1735
1852
|
// FOLDED, exactly like the money line above it: on a resume the cap governs both legs
|
|
1736
1853
|
// together, and a leg-only wall next to a folded spend is two framings on one cap with
|
|
1737
1854
|
// no label to tell them apart (F83). The leg stays on the line beside it.
|
|
@@ -1768,7 +1885,7 @@ async function execute(ctx) {
|
|
|
1768
1885
|
out(` trend ${mh.trend} — ${mh.reading}`);
|
|
1769
1886
|
out(` lever ${mh.lever}`);
|
|
1770
1887
|
for (const o of mh.options ?? []) out(` · ${o}`);
|
|
1771
|
-
out(` resume node scripts/run-u.mjs ${SELECTOR} --resume ${runid}${SHIM_TAIL}${SCOUT_TAIL} --approve <the NEW hash after you edit budgetUsd>`);
|
|
1888
|
+
out(` resume node scripts/run-u.mjs ${SELECTOR} --resume ${runid}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL} --approve <the NEW hash after you edit budgetUsd>`);
|
|
1772
1889
|
out(' (the top-up is yours to sign — nothing in the run may widen its own budget)');
|
|
1773
1890
|
}
|
|
1774
1891
|
// A STALL is a checkpoint too (hamr's go, 2026-08-13). Its own escalation prints one
|
|
@@ -1778,7 +1895,7 @@ async function execute(ctx) {
|
|
|
1778
1895
|
// the hash already approved is the hash that resumes.
|
|
1779
1896
|
if (outcome === 'step-stalled') {
|
|
1780
1897
|
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.');
|
|
1781
|
-
out(` resume node scripts/run-u.mjs ${SELECTOR} --resume ${runid}${SHIM_TAIL}${SCOUT_TAIL} --approve ${specHash}`);
|
|
1898
|
+
out(` resume node scripts/run-u.mjs ${SELECTOR} --resume ${runid}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL} --approve ${specHash}`);
|
|
1782
1899
|
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)');
|
|
1783
1900
|
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)');
|
|
1784
1901
|
}
|
|
@@ -1799,7 +1916,7 @@ async function execute(ctx) {
|
|
|
1799
1916
|
out(` died ${total === null ? 'before a plan was accepted — nothing paid is re-payable'
|
|
1800
1917
|
: done >= total ? `at the close — all ${total} step(s) finished`
|
|
1801
1918
|
: `in step ${done + 1} of ${total}`}`);
|
|
1802
|
-
out(` resume node scripts/run-u.mjs ${SELECTOR} --resume ${runid}${SHIM_TAIL}${SCOUT_TAIL} --approve ${specHash}`);
|
|
1919
|
+
out(` resume node scripts/run-u.mjs ${SELECTOR} --resume ${runid}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL} --approve ${specHash}`);
|
|
1803
1920
|
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)');
|
|
1804
1921
|
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)');
|
|
1805
1922
|
}
|
|
@@ -1820,7 +1937,7 @@ async function execute(ctx) {
|
|
|
1820
1937
|
]);
|
|
1821
1938
|
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');
|
|
1822
1939
|
out('');
|
|
1823
|
-
const answer = (/** @type {string} */ tail) => `node scripts/run-u.mjs ${SELECTOR} --resume ${runid}${SHIM_TAIL}${SCOUT_TAIL}${tail} --approve ${specHash}`;
|
|
1940
|
+
const answer = (/** @type {string} */ tail) => `node scripts/run-u.mjs ${SELECTOR} --resume ${runid}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL}${tail} --approve ${specHash}`;
|
|
1824
1941
|
for (const l of doorLines({
|
|
1825
1942
|
rerun: answer(' --decide rerun --text "<what you want done differently>"'),
|
|
1826
1943
|
accept: answer(' --decide accept'),
|
|
@@ -2057,6 +2174,8 @@ function buildCtx(mode, opts) {
|
|
|
2057
2174
|
model: opts.model ?? null,
|
|
2058
2175
|
readShim: opts.readShim ?? null,
|
|
2059
2176
|
scout: opts.scout ?? null,
|
|
2177
|
+
draftSpentUsd: opts.draftSpentUsd ?? null,
|
|
2178
|
+
draftSpendIncomplete: opts.draftSpendIncomplete === true,
|
|
2060
2179
|
registry: opts.registry ?? null,
|
|
2061
2180
|
workflow: opts.workflow ?? null,
|
|
2062
2181
|
approve: opts.approve ?? null,
|
|
@@ -2275,6 +2394,8 @@ export async function main(argv, deps = {}) {
|
|
|
2275
2394
|
model: argFlag('model'),
|
|
2276
2395
|
readShim: argFlag('read-shim'),
|
|
2277
2396
|
scout: argFlag('scout'),
|
|
2397
|
+
draftSpentUsd: argFlag('draft-spent-usd'),
|
|
2398
|
+
draftSpendIncomplete: argv.includes('--draft-spend-incomplete'),
|
|
2278
2399
|
registry: argFlag('registry'),
|
|
2279
2400
|
workflow: argFlag('workflow'),
|
|
2280
2401
|
approve: argFlag('approve'),
|
package/types/authorflow.d.ts
CHANGED
|
@@ -649,7 +649,10 @@ export function confirmPrompt({ answers, questions, labels, facts, listing, writ
|
|
|
649
649
|
* generate: Function, book: ReturnType<typeof makeCostBook>,
|
|
650
650
|
* ask: (step: {kind: string, [k: string]: any}) => Promise<string|null>,
|
|
651
651
|
* onPhase?: (phase: string, data?: any) => void, mode?: 'tool'|'text',
|
|
652
|
-
* worseThanBefore?: string}} o
|
|
652
|
+
* worseThanBefore?: string}} o retired field (hamr's ruling 2026-09-28):
|
|
653
|
+
* `worseThanBefore` is never asked any more and defaults to `''` — a
|
|
654
|
+
* caller may still pass a string through (kept for a future, non-ask
|
|
655
|
+
* source of the same constraint), it just never comes from an ask() step.
|
|
653
656
|
* @returns {Promise<{ok: boolean,
|
|
654
657
|
* stop: null|'cap-halt'|'pricing-red'|'provider-red'|'artifact-red'|'confirm-abandoned'|'confirm-restart',
|
|
655
658
|
* rounds: number,
|
|
@@ -1013,26 +1016,6 @@ export const QUESTION_SETS: Record<string, {
|
|
|
1013
1016
|
* @type {Record<string, string|null>}
|
|
1014
1017
|
*/
|
|
1015
1018
|
export const CLASS_STATEMENTS: Record<string, string | null>;
|
|
1016
|
-
/**
|
|
1017
|
-
* Asked ONLY for a repo source (ruling 2 addendum's D7) — a plain folder or a
|
|
1018
|
-
* URL has no "before" a repo's own history gives it to compare against. This
|
|
1019
|
-
* is the OLD Q5 wording, verbatim ("What would make you say this came back
|
|
1020
|
-
* worse than before?"), which M3 piece 3's reshape folded into Guardrails —
|
|
1021
|
-
* it survives here as its OWN field because the confirm turn (ruling 5's
|
|
1022
|
-
* 2026-09-13 addendum) includes it as a constraint only when the person
|
|
1023
|
-
* actually gave one, and folding it silently into the Guardrails free-text
|
|
1024
|
-
* slot would lose that "present or absent" distinction. An empty answer is
|
|
1025
|
-
* "nothing beyond what Guardrails already said" and is not required — see
|
|
1026
|
-
* the frozen-wording test below: this phrase appears NOWHERE in
|
|
1027
|
-
* {@link questionsFor}'s own sets, so a caller cannot double-ask it by
|
|
1028
|
-
* reading the wrong table.
|
|
1029
|
-
*/
|
|
1030
|
-
export const WORSE_THAN_BEFORE_FIELD: Readonly<{
|
|
1031
|
-
id: "worseThanBefore";
|
|
1032
|
-
kind: "mechanical";
|
|
1033
|
-
label: "Worse than before";
|
|
1034
|
-
prompt: "What would make you say this came back worse than before?";
|
|
1035
|
-
}>;
|
|
1036
1019
|
/**
|
|
1037
1020
|
* Asked ONLY when `src/detectlang.js`'s `detectLanguage` reports its
|
|
1038
1021
|
* `ambiguous` outcome — two supported languages' manifests at the same
|
package/types/authorrun.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @typedef {object} Deps
|
|
3
3
|
* @property {Record<string,string|undefined>} [env]
|
|
4
|
+
* @property {string} [keysHome] P4a — test seam: load this home's `.env` keys file even when `env` is injected.
|
|
4
5
|
* @property {NodeJS.ReadStream} [stdin]
|
|
5
6
|
* @property {NodeJS.WritableStream} [stdout]
|
|
6
7
|
* @property {NodeJS.WritableStream} [stderr]
|
|
@@ -23,6 +24,10 @@
|
|
|
23
24
|
export function main(argv: string[], deps?: Deps): Promise<number>;
|
|
24
25
|
export type Deps = {
|
|
25
26
|
env?: Record<string, string | undefined> | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* P4a — test seam: load this home's `.env` keys file even when `env` is injected.
|
|
29
|
+
*/
|
|
30
|
+
keysHome?: string | undefined;
|
|
26
31
|
stdin?: NodeJS.ReadStream | undefined;
|
|
27
32
|
stdout?: NodeJS.WritableStream | undefined;
|
|
28
33
|
stderr?: NodeJS.WritableStream | undefined;
|
package/types/cli.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* code, so `bin/bareloop.mjs` can set `process.exitCode` and let node flush
|
|
4
4
|
* queued stdout on its own (F-something: `process.exit()` can discard it).
|
|
5
5
|
* @param {string[]} argv
|
|
6
|
-
* @param {{ env?: Record<string,string|undefined>, stdout?: any, stderr?: any, cwd?: string, provider?: any, providerFor?: any, judgeProvider?: any, judgeModel?: string|null, now?: () => number, stdin?: any, runlistHome?: string }} deps
|
|
6
|
+
* @param {{ env?: Record<string,string|undefined>, stdout?: any, stderr?: any, cwd?: string, provider?: any, providerFor?: any, judgeProvider?: any, judgeModel?: string|null, now?: () => number, stdin?: any, runlistHome?: string, keysHome?: string }} deps
|
|
7
7
|
* @returns {Promise<number>}
|
|
8
8
|
*/
|
|
9
9
|
export function main(argv: string[], deps?: {
|
|
@@ -18,4 +18,5 @@ export function main(argv: string[], deps?: {
|
|
|
18
18
|
now?: () => number;
|
|
19
19
|
stdin?: any;
|
|
20
20
|
runlistHome?: string;
|
|
21
|
+
keysHome?: string;
|
|
21
22
|
}): Promise<number>;
|