bareloop 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/planrun.js CHANGED
@@ -3864,7 +3864,41 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
3864
3864
  blindCap: capRuns,
3865
3865
  directions: stageDirections,
3866
3866
  });
3867
- fixTrend.record({ gap: post.gap ?? '' });
3867
+ // F198: feed the STRUCTURED {stage, value} a declared close already produced
3868
+ // (`closeGrade`, src/declaredclose.js) rather than re-parsing `post.gap` through
3869
+ // `readGrade`'s `\bred\b` scan — a declared close's own gap prose never carries
3870
+ // that word, so the text path read every declared grade as an uncomparable
3871
+ // null. `post` IS `lastCloseVerdict` here (this is the opening grade, straight
3872
+ // off `judgeClose()`, before any crash-message substitution can occur), so
3873
+ // `closeGrade(post)` is exact. `readGrade` stays the fallback INSIDE
3874
+ // `trend.record` for a command close, where `closeGrade` returns `{gap}` only.
3875
+ fixTrend.record(closeGrade(post));
3876
+ // F198 item 2 — TRACE: before this, `middle` (below) handed the fix worker
3877
+ // ONLY the current gap (this iteration's raw close output, or the previous
3878
+ // attempt's — planrun.js's `await w.ask([...])` at the end of `middle`) and
3879
+ // never any history of prior grades. Nothing else about the close's own
3880
+ // numbers travelled between tries.
3881
+ //
3882
+ // The fix: a FACTS-ONLY number history, one entry per grade fixTrend has
3883
+ // already read (never advice, never model-generated text — a prompt
3884
+ // register change, `src/promptregisters.js`'s inventory, `src/planrun.js`
3885
+ // already listed there). `checksHistory` is the "N passed of M declared"
3886
+ // count per grade: N counts the satisfied stages on each declared verdict's
3887
+ // own `stages` list (first-red-wins, so that list holds only the stages that
3888
+ // RAN), and M is the DECLARED stage count (`stagedClose.length`, the same
3889
+ // one source `createTrend`'s stageOrder reads) — never the ran count, or a
3890
+ // 7-stage close red at stage 2 would read "1/2" instead of "1/7". Nothing a
3891
+ // command close's verdict carries, so it stays empty there and the line is
3892
+ // simply omitted. The per-stage number history rides on `fixTrend.report()`
3893
+ // already, so nothing new is stored for that half.
3894
+ /** @type {string[]} */
3895
+ const checksHistory = [];
3896
+ const pushChecks = (/** @type {any} */ v) => {
3897
+ if (Array.isArray(v?.stages)) {
3898
+ checksHistory.push(`${v.stages.filter((/** @type {any} */ s) => s.verdict === 'satisfied').length}/${stagedClose.length}`);
3899
+ }
3900
+ };
3901
+ pushChecks(post);
3868
3902
  /** ralph's `ladder` seam, filled by the trend reader instead of the step
3869
3903
  * ladder's repeat/write pair. ONE exhaustion terminal, two triggers (ralph's own
3870
3904
  * rule): the category stays `cap-halt` and the outcome stays `escalated`, so the
@@ -3883,7 +3917,15 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
3883
3917
  if (lastCloseVerdict?.verdict === HUMAN_PAUSE) {
3884
3918
  return { governor: 'close-trend', trend: 'unknown', reading: 'the close is waiting on a person — not a grade', iteration: o.iteration, paused: true };
3885
3919
  }
3886
- return { governor: 'close-trend', ...fixTrend.record({ gap: o.gap }), iteration: o.iteration };
3920
+ // Same structured-first read as the seed above. `lastCloseVerdict` is THIS
3921
+ // iteration's verdict (judge() ran immediately before ralph calls this
3922
+ // record) UNLESS a worker-crash overwrote `gap` with a synthetic message
3923
+ // (src/ralph.js) that no longer matches `lastCloseVerdict.gap` — guarded by
3924
+ // the equality check so a crash still reads through the text fallback on
3925
+ // its own synthetic gap, exactly as before.
3926
+ const graded = lastCloseVerdict && lastCloseVerdict.gap === o.gap ? closeGrade(lastCloseVerdict) : { gap: o.gap };
3927
+ if (lastCloseVerdict && lastCloseVerdict.gap === o.gap) pushChecks(lastCloseVerdict);
3928
+ return { governor: 'close-trend', ...fixTrend.record(graded), iteration: o.iteration };
3887
3929
  },
3888
3930
  struckOut: fixTrend.struckOut,
3889
3931
  report: fixTrend.report,
@@ -3897,9 +3939,12 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
3897
3939
  : `${rep.strikes}/${rep.limit} strikes — the fix loop stopped making progress against the close's own numbers (${t.reading}). Continue, change approach, or stop?`,
3898
3940
  options: [
3899
3941
  'revise the goal/spec so the work is reachable (a spec edit, so the new hash needs re-approval)',
3900
- resumable
3901
- ? 'top up budgetUsd and rerun with --resume, if the trend above says it was still converging'
3902
- : 'resume is `run-u`-only in v1 — top up budgetUsd and re-fire the bundle from the start, if the trend above says it was still converging',
3942
+ // F197/mul5fofw — this terminal is ONE exhaustion terminal, and ralph.js
3943
+ // hardcodes its outcome as `escalated` (a GRADED RED, src/reuse.js's
3944
+ // `REUSE_GRADED_RED`: the close rendered a verdict and it was red).
3945
+ // `escalated` is a graded red, never a checkpoint (F197) — `--resume`
3946
+ // refuses it, so the sentence states that plainly.
3947
+ 'this cannot be resumed — the close already rendered its verdict against the tree; revise the goal/spec and rerun fresh (a new hash needs re-approval)',
3903
3948
  'abandon the task',
3904
3949
  ],
3905
3950
  };
@@ -3996,12 +4041,23 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
3996
4041
  })
3997
4042
  : null;
3998
4043
  if (rootInj) emit('root-injected', { phase: 'fix', ...rootInj.event });
4044
+ // F198 item 2 — the facts-only history: JOINED numbers only, no prose the
4045
+ // model didn't already earn by writing the close's own output. `values`
4046
+ // joins with `→` (fixTrend's own vocabulary), stages join with `·`; a
4047
+ // stage this leg has read only once still shows its one number (no arrow),
4048
+ // which is still a fact the worker did not otherwise have.
4049
+ const checksFact = checksHistory.length > 0 ? `checks passed ${checksHistory.join(' → ')}` : null;
4050
+ const stageFacts = fixTrend.report().stages
4051
+ .filter((s) => s.values.length > 0)
4052
+ .map((s) => `${s.stage} ${s.values.join(' → ')}`);
4053
+ const factsLine = [checksFact, ...stageFacts].filter(Boolean).join(' · ');
3999
4054
  await w.ask([
4000
4055
  'The job\'s final verification is failing. Fix the repository so it passes.',
4001
4056
  `Repository root (absolute): ${workdir}\nEvery path you pass to a tool MUST be absolute and inside this root.`,
4002
4057
  artifacts.length > 0 && `Working context (read-only) — the plan's steps produced:\n${artifacts.map((a) => `[${a.id}] ${a.text}`).join('\n\n')}`,
4003
4058
  !gap && post.gap && `The verification's output on the tree as it stands (not an attempt of yours):\n${post.gap}`,
4004
4059
  gap && `Previous attempt failed the verification:\n${gap}`,
4060
+ factsLine && `The close's own numbers so far, oldest first (facts only — no advice):\n${factsLine}`,
4005
4061
  rootInj && rootInj.note,
4006
4062
  ].filter(Boolean).join('\n\n'));
4007
4063
  };
@@ -76,6 +76,9 @@ export const PROMPT_REGISTERS = Object.freeze([
76
76
 
77
77
  // src/planrun.js — the native-surface read-truncation strategy line
78
78
  Object.freeze({ file: 'src/planrun.js', name: 'NATIVE_READ_STRATEGY' }),
79
+ // src/planrun.js — the fix loop's facts-only close-grade history line
80
+ // (F198 item 2), built inline inside `middle` and appended to `w.ask([...])`
81
+ Object.freeze({ file: 'src/planrun.js', name: 'middle()\'s facts-only "The close\'s own numbers so far" block (inline template, not a const)' }),
79
82
 
80
83
  // src/authoring.js — the types-genre close template, interpolated verbatim
81
84
  // into the authoring prompt (src/authorflow.js's assembled system message,
package/src/providers.js CHANGED
@@ -320,6 +320,88 @@ export function buildRunnerProviders({
320
320
  return { provider, providerFor, judgeProvider };
321
321
  }
322
322
 
323
+ /**
324
+ * A `providerName`'s own models-list GET endpoint — the shape this
325
+ * function's ONE caller (`checkProviderReachable`, below) needs and nothing
326
+ * else reads: `anthropic-api` and `openai-api` (the panel's Model select,
327
+ * PANEL-BUILD.md P3 build item 7) are the only two shapes this build knows
328
+ * how to ask, both a plain unauthenticated-body GET with the key in a
329
+ * header — never a completion/chat endpoint, so this never spends a token.
330
+ * @param {string} providerName
331
+ * @param {string} [baseUrl]
332
+ * @returns {{url: string, headers: Record<string,string>}|null} `null` when
333
+ * this provider's models-list shape is not known (never guessed at)
334
+ */
335
+ function modelsListRequest(providerName, baseUrl) {
336
+ if (providerName === 'anthropic-api') {
337
+ return {
338
+ url: `${baseUrl || 'https://api.anthropic.com/v1'}/models`,
339
+ headers: { 'x-api-key': '', 'anthropic-version': '2023-06-01' },
340
+ };
341
+ }
342
+ if (providerName === 'openai-api') {
343
+ return { url: `${baseUrl || 'https://api.openai.com/v1'}/models`, headers: {} };
344
+ }
345
+ return null;
346
+ }
347
+
348
+ /**
349
+ * $0 reachability check: a GET against `providerName`'s own models-list
350
+ * endpoint, server-side, with the key in a header — NEVER a completion
351
+ * request, so it never spends a token (PANEL-BUILD.md P3 build item 7: "the
352
+ * person must get feedback BEFORE drafting if the chosen model can't
353
+ * work"). Reused, unmodified, by the panel's model-readiness route today
354
+ * (`src/panel/authorroutes.js`) and by the future Settings providers
355
+ * table's own "test" column (PANEL-BUILD.md P4) — ONE function, not two
356
+ * hand-typed copies of the same probe.
357
+ *
358
+ * NEVER returns the key, never logs it — the key only ever appears in a
359
+ * request header sent straight to the provider's own host.
360
+ * @param {object} o
361
+ * @param {string} o.providerName
362
+ * @param {string} o.apiKey
363
+ * @param {string} [o.model] when given, the response is also checked for this model id
364
+ * @param {string} [o.baseUrl]
365
+ * @param {typeof fetch} [o.fetchImpl] TEST SEAM — defaults to the real global `fetch`
366
+ * @param {number} [o.timeoutMs] defaults to 4000 — a flaky endpoint must never hang the caller
367
+ * @returns {Promise<{reachable: boolean, modelListed: boolean|null, status: string, note: string|null}>}
368
+ */
369
+ export async function checkProviderReachable({
370
+ providerName, apiKey, model, baseUrl, fetchImpl = fetch, timeoutMs = 4000,
371
+ }) {
372
+ const req = modelsListRequest(providerName, baseUrl);
373
+ if (!req) {
374
+ return {
375
+ reachable: false, modelListed: null, status: 'unsupported-provider',
376
+ note: `no models-list check known for "${providerName}"`,
377
+ };
378
+ }
379
+ const headers = { ...req.headers };
380
+ if (providerName === 'anthropic-api') headers['x-api-key'] = apiKey;
381
+ else headers.Authorization = `Bearer ${apiKey}`;
382
+
383
+ const controller = new AbortController();
384
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
385
+ try {
386
+ // GET ONLY — no body, no completion/chat call, no tokens spent.
387
+ const res = await fetchImpl(req.url, { method: 'GET', headers, signal: controller.signal });
388
+ if (!res.ok) {
389
+ return { reachable: false, modelListed: null, status: `HTTP ${res.status}`, note: `models list returned HTTP ${res.status}` };
390
+ }
391
+ /** @type {any} */
392
+ let body = null;
393
+ try { body = await res.json(); } catch { body = null; }
394
+ const ids = Array.isArray(body?.data) ? body.data.map((/** @type {any} */ m) => m?.id).filter(Boolean) : [];
395
+ const modelListed = model ? ids.includes(model) : null;
396
+ return { reachable: true, modelListed, status: 'ok', note: null };
397
+ } catch (/** @type {any} */ e) {
398
+ const cls = e?.name === 'AbortError' ? 'timeout' : (e?.code || e?.name || 'network-error');
399
+ return { reachable: false, modelListed: null, status: String(cls), note: `request failed: ${cls}` };
400
+ } finally {
401
+ clearTimeout(timer);
402
+ }
403
+ }
404
+
323
405
  /**
324
406
  * The launch-time warning lines for an ADMITTED-PENDING-PROBE provider, or
325
407
  * `null` when the provider has paid for its own end-to-end probe (and for an
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(scoutTsHi, firstTs),
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}} [opts]
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
- remainingUsd: () => Math.min(shellCapUsd, job.budgetUsd - spentUsd),
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/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';
@@ -242,6 +243,15 @@ class ExitSignal extends Error {
242
243
  * @property {string|null} [model] the raw `--model` tier name (`sonnet`/`haiku`), or null for the default
243
244
  * @property {string|null} [readShim] the raw `--read-shim` arm name, or null for the default
244
245
  * @property {string|null} [scout] the raw `--scout` value (`on`/`off`), or null for the default
246
+ * @property {string|null} [draftSpentUsd] the raw `--draft-spent-usd` value (hamr's
247
+ * ruling 2026-09-28, "one cap covers drafting + run"), or null when the job never
248
+ * drafted through the panel/`bareloop author`. Parsed and validated in `execute`
249
+ * (finite, >= 0, else refused) — never coerced silently, the same param-guard
250
+ * class `--read-shim`/`--scout` already are.
251
+ * @property {boolean} [draftSpendIncomplete] the raw `--draft-spend-incomplete` presence-
252
+ * flag (hamr's ruling 2026-09-28, 2nd addendum) — whether the drafting fold above was a
253
+ * FLOOR, never an exact figure. A new field beside `draftSpentUsd`, same param-guard
254
+ * class, no value to misread.
245
255
  * @property {string|null} [registry] a bridge registry directory
246
256
  * @property {string|null} [workflow] the registry's workflow name (defaults to `spec.job`)
247
257
  * @property {string|null} [approve] the spec hash being signed; omitted previews only
@@ -263,6 +273,8 @@ class ExitSignal extends Error {
263
273
  * @property {string|null} model
264
274
  * @property {string|null} readShim
265
275
  * @property {string|null} scout
276
+ * @property {string|null} draftSpentUsd
277
+ * @property {boolean} draftSpendIncomplete
266
278
  * @property {string|null} registry
267
279
  * @property {string|null} workflow
268
280
  * @property {string|null} approve
@@ -394,6 +406,39 @@ async function execute(ctx) {
394
406
  * so a resume never silently drops it and runs the default under the arm's label. */
395
407
  const SCOUT_TAIL = scoutArg !== null && !SCOUT ? ' --scout off' : '';
396
408
 
409
+ // --draft-spent-usd (hamr's ruling 2026-09-28, "one cap covers drafting +
410
+ // run") — runner territory exactly like --read-shim/--scout above: the
411
+ // spec's signed budgetUsd is unaffected, this only shrinks THIS leg's own
412
+ // enforced ceiling. Same param-guard class and the same reason: an
413
+ // unreadable value here would either silently widen the ceiling (garbage
414
+ // read as 0) or silently narrow it (garbage read as Infinity) — neither is
415
+ // acceptable, so this refuses at argv, before the approval gate, rather
416
+ // than coercing.
417
+ const draftSpentArg = ctx.draftSpentUsd ?? null;
418
+ let DRAFT_SPENT_USD = 0;
419
+ if (draftSpentArg !== null) {
420
+ const v = Number(draftSpentArg);
421
+ if (draftSpentArg.trim() === '' || !Number.isFinite(v) || v < 0) {
422
+ 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`);
423
+ throw new ExitSignal(2);
424
+ }
425
+ DRAFT_SPENT_USD = v;
426
+ }
427
+ // --draft-spend-incomplete (hamr's ruling 2026-09-28, 2nd addendum) — a NEW
428
+ // field beside --draft-spent-usd, never a value inside it: whether the
429
+ // drafting fold above was EXACT. Boolean presence-flag, same class as
430
+ // --review-door above (no argument, no value to misread as 0/Infinity).
431
+ // Meaningless alone (no drafting fold to qualify) but never refused for
432
+ // being passed without --draft-spent-usd — a re-invocation tail that always
433
+ // pairs the two never needs a second guard to prove it did.
434
+ const DRAFT_SPEND_INCOMPLETE = ctx.draftSpendIncomplete === true;
435
+ /** every re-invocation this script PRINTS carries the drafting fold, the
436
+ * same SHIM_TAIL/SCOUT_TAIL rule: a resume that dropped it would silently
437
+ * WIDEN the ceiling back up by the drafting spend on its next leg (and
438
+ * dropping the incomplete flag would silently turn a floor into an exact
439
+ * figure on the next leg's own job-start). */
440
+ const DRAFT_TAIL = DRAFT_SPENT_USD > 0 ? ` --draft-spent-usd ${DRAFT_SPENT_USD}${DRAFT_SPEND_INCOMPLETE ? ' --draft-spend-incomplete' : ''}` : '';
441
+
397
442
  const WORKDIR = target.workdir;
398
443
  const SEED = target.seed;
399
444
  const specHash = jobSpecHash(spec);
@@ -1040,7 +1085,7 @@ async function execute(ctx) {
1040
1085
  // right key only by accident of the runner's own generic error message,
1041
1086
  // never from this hint. `providerEntry.envKey` is the same resolved name
1042
1087
  // 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}`;
1088
+ 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
1089
  /** the door the operator has already picked, as flags — hoisted out of the else
1045
1090
  * below so the inhibitor line at the bottom can print the WHOLE command rather
1046
1091
  * than a shape the operator has to assemble. Empty on an ordinary run and on the
@@ -1205,8 +1250,8 @@ async function execute(ctx) {
1205
1250
  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
1251
  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
1252
  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}`);
1253
+ out(` node scripts/run-u.mjs ${SELECTOR} --resume ${RESUME}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL} --decide accept --approve ${specHash}`);
1254
+ 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
1255
  out(` read the same command with no --decide re-prints the evidence package you just looked at`);
1211
1256
  throw new ExitSignal(0);
1212
1257
  }
@@ -1458,6 +1503,14 @@ async function execute(ctx) {
1458
1503
  // driver's plan is a row nobody can audit from its own log.
1459
1504
  out(` ${READ_SHIM_LABEL}`);
1460
1505
  out(` ${SCOUT_LABEL}`);
1506
+ // hamr's ruling 2026-09-28 — advertised = enforced: the banner states the
1507
+ // SIGNED cap above unconditionally (it never changes, same spec hash), and
1508
+ // this line states the drafting fold whenever there is one, so the number
1509
+ // this run actually enforces (Cap $ − drafting spent) is never a second,
1510
+ // silent figure a reader has to compute themselves.
1511
+ if (DRAFT_SPENT_USD > 0) {
1512
+ 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}`);
1513
+ }
1461
1514
 
1462
1515
  // F67 — the OUTSIDE watchdog, started before the run and sharing nothing with it.
1463
1516
  // Every guard bareloop had lived inside this process, and ms3197n8/ms3jh76q proved
@@ -1642,6 +1695,8 @@ async function execute(ctx) {
1642
1695
  closeDir: spineDir,
1643
1696
  readShim: READ_SHIM,
1644
1697
  scout: SCOUT,
1698
+ draftSpentUsd: DRAFT_SPENT_USD,
1699
+ draftSpendComplete: !DRAFT_SPEND_INCOMPLETE,
1645
1700
  // RESUME: the money and the wall the halted run already burned are FOLDED IN (so
1646
1701
  // the signed ceiling cannot widen by being re-invoked), and the checkpoint it
1647
1702
  // reached is handed over so the plan is reloaded rather than re-drafted and the
@@ -1731,7 +1786,15 @@ async function execute(ctx) {
1731
1786
  const writes = audit.filter((e) => e.decision === 'allow' && (e.action?.type === 'write' || e.action?.type === 'edit'));
1732
1787
 
1733
1788
  out(`\noutcome ${outcome}`);
1734
- out(`spent ${je?.spentUsd == null ? 'UNKNOWN' : `${je.spendComplete === false ? '≥' : ''}$${je.spentUsd.toFixed(4)}`} of $${spec.budgetUsd}`);
1789
+ // hamr's ruling 2026-09-28 — the drafting share (this leg's own job-start
1790
+ // record) rides beside the run's own spend via the ONE shared formatter,
1791
+ // so this line never disagrees with the panel's Run tab/Runs list about
1792
+ // what "$X of $Y" means for a job that drafted through the panel/author.
1793
+ const jobStartRec = events.find((e) => e.type === 'job-start');
1794
+ const legDraftSpentUsd = typeof jobStartRec?.draftSpentUsd === 'number' && jobStartRec.draftSpentUsd > 0 ? jobStartRec.draftSpentUsd : null;
1795
+ const legDraftSpendComplete = legDraftSpentUsd === null ? null : jobStartRec.draftSpendComplete !== false;
1796
+ const spendCore = je?.spentUsd == null ? 'UNKNOWN' : `${je.spendComplete === false ? '≥' : ''}$${je.spentUsd.toFixed(4)}`;
1797
+ out(`spent ${moneyWithDraft(spendCore, legDraftSpentUsd, spec.budgetUsd, legDraftSpendComplete)}`);
1735
1798
  // FOLDED, exactly like the money line above it: on a resume the cap governs both legs
1736
1799
  // together, and a leg-only wall next to a folded spend is two framings on one cap with
1737
1800
  // no label to tell them apart (F83). The leg stays on the line beside it.
@@ -1768,7 +1831,7 @@ async function execute(ctx) {
1768
1831
  out(` trend ${mh.trend} — ${mh.reading}`);
1769
1832
  out(` lever ${mh.lever}`);
1770
1833
  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>`);
1834
+ 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
1835
  out(' (the top-up is yours to sign — nothing in the run may widen its own budget)');
1773
1836
  }
1774
1837
  // A STALL is a checkpoint too (hamr's go, 2026-08-13). Its own escalation prints one
@@ -1778,7 +1841,7 @@ async function execute(ctx) {
1778
1841
  // the hash already approved is the hash that resumes.
1779
1842
  if (outcome === 'step-stalled') {
1780
1843
  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}`);
1844
+ out(` resume node scripts/run-u.mjs ${SELECTOR} --resume ${runid}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL} --approve ${specHash}`);
1782
1845
  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
1846
  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
1847
  }
@@ -1799,7 +1862,7 @@ async function execute(ctx) {
1799
1862
  out(` died ${total === null ? 'before a plan was accepted — nothing paid is re-payable'
1800
1863
  : done >= total ? `at the close — all ${total} step(s) finished`
1801
1864
  : `in step ${done + 1} of ${total}`}`);
1802
- out(` resume node scripts/run-u.mjs ${SELECTOR} --resume ${runid}${SHIM_TAIL}${SCOUT_TAIL} --approve ${specHash}`);
1865
+ out(` resume node scripts/run-u.mjs ${SELECTOR} --resume ${runid}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL} --approve ${specHash}`);
1803
1866
  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
1867
  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
1868
  }
@@ -1820,7 +1883,7 @@ async function execute(ctx) {
1820
1883
  ]);
1821
1884
  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
1885
  out('');
1823
- const answer = (/** @type {string} */ tail) => `node scripts/run-u.mjs ${SELECTOR} --resume ${runid}${SHIM_TAIL}${SCOUT_TAIL}${tail} --approve ${specHash}`;
1886
+ const answer = (/** @type {string} */ tail) => `node scripts/run-u.mjs ${SELECTOR} --resume ${runid}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL}${tail} --approve ${specHash}`;
1824
1887
  for (const l of doorLines({
1825
1888
  rerun: answer(' --decide rerun --text "<what you want done differently>"'),
1826
1889
  accept: answer(' --decide accept'),
@@ -2057,6 +2120,8 @@ function buildCtx(mode, opts) {
2057
2120
  model: opts.model ?? null,
2058
2121
  readShim: opts.readShim ?? null,
2059
2122
  scout: opts.scout ?? null,
2123
+ draftSpentUsd: opts.draftSpentUsd ?? null,
2124
+ draftSpendIncomplete: opts.draftSpendIncomplete === true,
2060
2125
  registry: opts.registry ?? null,
2061
2126
  workflow: opts.workflow ?? null,
2062
2127
  approve: opts.approve ?? null,
@@ -2275,6 +2340,8 @@ export async function main(argv, deps = {}) {
2275
2340
  model: argFlag('model'),
2276
2341
  readShim: argFlag('read-shim'),
2277
2342
  scout: argFlag('scout'),
2343
+ draftSpentUsd: argFlag('draft-spent-usd'),
2344
+ draftSpendIncomplete: argv.includes('--draft-spend-incomplete'),
2278
2345
  registry: argFlag('registry'),
2279
2346
  workflow: argFlag('workflow'),
2280
2347
  approve: argFlag('approve'),