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.
@@ -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
@@ -0,0 +1,65 @@
1
+ /** @returns {string} a fresh per-process token — never persisted, never logged */
2
+ export function mintToken(): string;
3
+ /**
4
+ * `true` when a request may reach ANY route in this file — the token header
5
+ * plus an Origin/Host that names this exact bind address. `req.headers`
6
+ * lower-cases header names (node's own `http` behaviour), so this reads
7
+ * lower-case names throughout.
8
+ * @param {import('node:http').IncomingMessage} req
9
+ * @param {{token: string, port: number}} o
10
+ * @returns {{ok: true}|{ok: false, reason: string}}
11
+ */
12
+ export function checkHumanGuard(req: import("node:http").IncomingMessage, { token, port }: {
13
+ token: string;
14
+ port: number;
15
+ }): {
16
+ ok: true;
17
+ } | {
18
+ ok: false;
19
+ reason: string;
20
+ };
21
+ /**
22
+ * Builds this file's own request handler, holding its session store in a
23
+ * closure (one per `createPanelServer` call, exactly like `handleRequest`'s
24
+ * own module holds none — this file owns the state `src/panel/server.js`
25
+ * deliberately has none of).
26
+ * @param {{ port: number, token: string, env?: Record<string,string|undefined>,
27
+ * sessionsRoot?: string, spawnFn?: typeof realSpawn, bareloopBin?: string,
28
+ * jobsDir?: string, fetchImpl?: typeof fetch }} opts
29
+ */
30
+ export function createAuthorRoutes(opts: {
31
+ port: number;
32
+ token: string;
33
+ env?: Record<string, string | undefined>;
34
+ sessionsRoot?: string;
35
+ spawnFn?: typeof realSpawn;
36
+ bareloopBin?: string;
37
+ jobsDir?: string;
38
+ fetchImpl?: typeof fetch;
39
+ }): {
40
+ handle: (req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse, pathname: string, body: any) => boolean;
41
+ sessions: Map<string, any>;
42
+ };
43
+ /**
44
+ * THE ONLY function in this file (or anywhere in the panel) that spawns a
45
+ * run. Refuses unless the session is `prepared` AND the hash the request
46
+ * carried matches the session's OWN `signing.json` `specHash` exactly — the
47
+ * click carries the hash (per the mockup's own "the person never has to copy
48
+ * it to sign"), and this is where it is checked, not trusted.
49
+ * @param {ReturnType<typeof createSession>} session
50
+ * @param {string} claimedHash
51
+ * @param {{env: any, spawnFn: typeof realSpawn, bareloopBin: string, sessionsRoot?: string}} o
52
+ * @returns {{ok: boolean, error?: string, job?: string}}
53
+ */
54
+ export function signRun(session: ReturnType<typeof createSession>, claimedHash: string, o: {
55
+ env: any;
56
+ spawnFn: typeof realSpawn;
57
+ bareloopBin: string;
58
+ sessionsRoot?: string;
59
+ }): {
60
+ ok: boolean;
61
+ error?: string;
62
+ job?: string;
63
+ };
64
+ import { spawn as realSpawn } from 'node:child_process';
65
+ import { createSession } from './authorsession.js';
@@ -0,0 +1,61 @@
1
+ /**
2
+ * `true` when `jobs/<name>.json` already exists in the repo's own jobs/ dir
3
+ * (resolved from THIS module's own on-disk location, never `process.cwd()`,
4
+ * the same rule `src/panel/server.js`'s `jobsDir()` follows) — P3 is new-jobs-
5
+ * only (Q4=A), so a name collision refuses at $0 rather than silently
6
+ * overwriting or authoring a second spec under one name.
7
+ * @param {string} name
8
+ * @param {{jobsDir?: string}} [opts] test seam
9
+ * @returns {boolean}
10
+ */
11
+ export function jobNameTaken(name: string, opts?: {
12
+ jobsDir?: string;
13
+ }): boolean;
14
+ /**
15
+ * `$0` validation of the job card fields the server must check BEFORE it
16
+ * creates a session or spends anything — never inside the async pipeline,
17
+ * so a bad card refuses synchronously, in the same HTTP response.
18
+ * @param {any} card
19
+ * @returns {{ok: true}|{ok: false, error: string}}
20
+ */
21
+ export function validateJobCard(card: any): {
22
+ ok: true;
23
+ } | {
24
+ ok: false;
25
+ error: string;
26
+ };
27
+ /**
28
+ * ONE session's engine — created already RUNNING (the caller awaits nothing;
29
+ * the pipeline drives itself, reporting through `state`). `state.pendingAsk`
30
+ * is the ONE thing a person answers next; `answer()`/`send()`/`revise()`/
31
+ * `signPrepare()` are the only ways in.
32
+ * @param {any} card the validated job card (see {@link validateJobCard})
33
+ * @param {{env?: Record<string,string|undefined>, sessionsRoot?: string, timeoutMs?: number,
34
+ * scout?: any, generate?: Function, confirmGenerate?: Function, authorFn?: Function,
35
+ * prepareSigningFn?: Function}} [deps] the last five are TEST SEAMS ONLY — see the note
36
+ * just above where each is read, below.
37
+ * @returns {any} the session object
38
+ */
39
+ export function createSession(card: any, deps?: {
40
+ env?: Record<string, string | undefined>;
41
+ sessionsRoot?: string;
42
+ timeoutMs?: number;
43
+ scout?: any;
44
+ generate?: Function;
45
+ confirmGenerate?: Function;
46
+ authorFn?: Function;
47
+ prepareSigningFn?: Function;
48
+ }): any;
49
+ /**
50
+ * The job card's "Model" select — a fixed, small, source-grounded mapping
51
+ * from a display id to the `{provider, baseUrl?}` pair `src/interviewrun.js`
52
+ * itself documents (that script's own `--provider openai-api --base-url
53
+ * https://api.deepseek.com/v1` usage line for DeepSeek): the panel builds no
54
+ * provider table of its own, this is a UI convenience over the SAME two
55
+ * provider identities the CLI already resolves through `resolveProvider`.
56
+ * @type {Readonly<Record<string, {provider: string, baseUrl?: string}>>}
57
+ */
58
+ export const MODEL_OPTIONS: Readonly<Record<string, {
59
+ provider: string;
60
+ baseUrl?: string;
61
+ }>>;
@@ -200,13 +200,24 @@ export function getRunJob(runid: string, opts?: {
200
200
  * separately from {@link createPanelServer} so tests can drive it without a
201
201
  * real listening socket where that is simpler (most path-safety/405 tests
202
202
  * still go through a real socket, per the build spec).
203
+ *
204
+ * PANEL-BUILD.md P3: this file's own GET/HEAD-only rule stands for every
205
+ * route it owns; `/api/author/*` is the ONE family of routes that may be
206
+ * POSTed to, and it is handled entirely by {@link createAuthorRoutes}
207
+ * (`src/panel/authorroutes.js`) — a separate module so this file's own
208
+ * "read-only, by construction" header comment stays true of everything else
209
+ * in it. `opts.authorRoutes` is absent on every P1/P2 caller (read-only
210
+ * tests, and the CLI's own `panelMain` when no author routes are wired) —
211
+ * only `createPanelServer` below always supplies one.
203
212
  * @param {import('node:http').IncomingMessage} req
204
213
  * @param {import('node:http').ServerResponse} res
205
- * @param {{ home?: string, port: number }} opts
214
+ * @param {{ home?: string, port: number, token?: string, authorRoutes?: ReturnType<typeof createAuthorRoutes> }} opts
206
215
  */
207
216
  export function handleRequest(req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse, opts: {
208
217
  home?: string;
209
218
  port: number;
219
+ token?: string;
220
+ authorRoutes?: ReturnType<typeof createAuthorRoutes>;
210
221
  }): void;
211
222
  /**
212
223
  * Start the panel server. Binds `127.0.0.1` ONLY. A taken port is a LOUD,
@@ -214,15 +225,33 @@ export function handleRequest(req: import("node:http").IncomingMessage, res: imp
214
225
  * header). Resolves once actually listening; rejects on a bind error
215
226
  * (including `EADDRINUSE`) with a `.port` field on the error for the
216
227
  * caller's message.
217
- * @param {{ port?: number, home?: string }} [opts]
218
- * @returns {Promise<{ server: import('node:http').Server, port: number, close: () => Promise<void> }>}
228
+ *
229
+ * PANEL-BUILD.md P3: a fresh {@link mintToken} token and one {@link
230
+ * createAuthorRoutes} instance are built here, ONCE per server start, and
231
+ * held for the server's lifetime — `opts.env`/`opts.sessionsRoot`/
232
+ * `opts.spawnFn`/`opts.bareloopBin` are test seams the author routes take
233
+ * (a real caller passes none of them and {@link createAuthorRoutes} defaults
234
+ * each one to the real environment/spawn/binary, this file itself touching
235
+ * none of them, and the real
236
+ * `~/.config/bareloop/panel-sessions`, the real `child_process.spawn`, and
237
+ * this package's own `bin/bareloop.mjs`).
238
+ * @param {{ port?: number, home?: string, env?: Record<string,string|undefined>,
239
+ * sessionsRoot?: string, spawnFn?: (...a: any[]) => any, bareloopBin?: string,
240
+ * fetchImpl?: typeof fetch }} [opts]
241
+ * @returns {Promise<{ server: import('node:http').Server, port: number, token: string, close: () => Promise<void> }>}
219
242
  */
220
243
  export function createPanelServer(opts?: {
221
244
  port?: number;
222
245
  home?: string;
246
+ env?: Record<string, string | undefined>;
247
+ sessionsRoot?: string;
248
+ spawnFn?: (...a: any[]) => any;
249
+ bareloopBin?: string;
250
+ fetchImpl?: typeof fetch;
223
251
  }): Promise<{
224
252
  server: import("node:http").Server;
225
253
  port: number;
254
+ token: string;
226
255
  close: () => Promise<void>;
227
256
  }>;
228
257
  /**
@@ -263,3 +292,4 @@ export const DIED_MTIME_MS: number;
263
292
  * 2026-09-25). */
264
293
  export const ROUNDS_PAGE_DEFAULT: 50;
265
294
  export const ROUNDS_PAGE_MAX: 200;
295
+ import { createAuthorRoutes } from './authorroutes.js';
@@ -98,6 +98,40 @@ export function buildRunnerProviders({ providerName, apiKey, model, tierModels,
98
98
  providerFor: (tier: string) => any;
99
99
  judgeProvider: any;
100
100
  };
101
+ /**
102
+ * $0 reachability check: a GET against `providerName`'s own models-list
103
+ * endpoint, server-side, with the key in a header — NEVER a completion
104
+ * request, so it never spends a token (PANEL-BUILD.md P3 build item 7: "the
105
+ * person must get feedback BEFORE drafting if the chosen model can't
106
+ * work"). Reused, unmodified, by the panel's model-readiness route today
107
+ * (`src/panel/authorroutes.js`) and by the future Settings providers
108
+ * table's own "test" column (PANEL-BUILD.md P4) — ONE function, not two
109
+ * hand-typed copies of the same probe.
110
+ *
111
+ * NEVER returns the key, never logs it — the key only ever appears in a
112
+ * request header sent straight to the provider's own host.
113
+ * @param {object} o
114
+ * @param {string} o.providerName
115
+ * @param {string} o.apiKey
116
+ * @param {string} [o.model] when given, the response is also checked for this model id
117
+ * @param {string} [o.baseUrl]
118
+ * @param {typeof fetch} [o.fetchImpl] TEST SEAM — defaults to the real global `fetch`
119
+ * @param {number} [o.timeoutMs] defaults to 4000 — a flaky endpoint must never hang the caller
120
+ * @returns {Promise<{reachable: boolean, modelListed: boolean|null, status: string, note: string|null}>}
121
+ */
122
+ export function checkProviderReachable({ providerName, apiKey, model, baseUrl, fetchImpl, timeoutMs, }: {
123
+ providerName: string;
124
+ apiKey: string;
125
+ model?: string | undefined;
126
+ baseUrl?: string | undefined;
127
+ fetchImpl?: typeof fetch | undefined;
128
+ timeoutMs?: number | undefined;
129
+ }): Promise<{
130
+ reachable: boolean;
131
+ modelListed: boolean | null;
132
+ status: string;
133
+ note: string | null;
134
+ }>;
101
135
  /**
102
136
  * The launch-time warning lines for an ADMITTED-PENDING-PROBE provider, or
103
137
  * `null` when the provider has paid for its own end-to-end probe (and for an
package/types/replay.d.ts CHANGED
@@ -89,6 +89,8 @@ export function replayRun(spineEvents: any[], auditEvents?: any[], { runId, audi
89
89
  stopReason: string | null;
90
90
  spentUsd: any;
91
91
  spendComplete: boolean;
92
+ draftSpentUsd: any;
93
+ draftSpendComplete: boolean | null;
92
94
  floorReasons: string[];
93
95
  transportRetryCount: number;
94
96
  transportRetriesOutsideWindows: number;
@@ -239,6 +241,35 @@ export function replayRun(spineEvents: any[], auditEvents?: any[], { runId, audi
239
241
  memoryCache: any;
240
242
  skipped: number;
241
243
  };
244
+ /**
245
+ * hamr's ruling 2026-09-28 ("one cap covers drafting + run") — the ONE
246
+ * spend display for a run that may carry a drafting share, so every surface
247
+ * that shows a run's money (the CLI's own `spent` line, the panel's Run tab
248
+ * summary/counters, the Runs list row, the Audit header, the Job tab) reads
249
+ * the same string rather than each hand-composing its own. `spendCore` is
250
+ * already-formatted text (`money(...)`, or a `died`/floor line's "at least
251
+ * $X") — this only decides whether to append the `(<draft> drafting)`
252
+ * clause and the `of $<cap>` tail, never re-derives the money itself.
253
+ * Unchanged (`spendCore` alone, or `spendCore of $cap`) when there is no
254
+ * drafting share — a run without one prints exactly what it always did.
255
+ *
256
+ * hamr's ruling 2026-09-28 (2nd addendum) — an INCOMPLETE drafting fold
257
+ * (`draftSpendComplete === false`) is a floor, not a total, and F6's rule
258
+ * ("unpriced is never free, a floor never dresses as exact") applies here
259
+ * exactly as it does to `spendCore` itself: the drafting clause reads `at
260
+ * least $X drafting`, and because a reader summing the two figures would
261
+ * otherwise take the LEADING number as exact too, `spendCore` itself also
262
+ * gains the `at least ` prefix — unless it already carries one (a `died`
263
+ * floor, or an already-prefixed caller), never doubled.
264
+ * @param {string} spendCore already-formatted spend text (e.g. `$3.71` or `at least $2.00`)
265
+ * @param {number|null} draftSpentUsd the drafting share, or null when there isn't one
266
+ * @param {number|null} [budgetUsd] the signed cap, appended as `of $X.XX` when given
267
+ * @param {boolean|null} [draftSpendComplete] whether the drafting fold above was EXACT;
268
+ * `null`/`undefined`/`true` all read as exact (never a new enum value elsewhere) —
269
+ * only an explicit `false` triggers the floor wording.
270
+ * @returns {string}
271
+ */
272
+ export function moneyWithDraft(spendCore: string, draftSpentUsd: number | null, budgetUsd?: number | null, draftSpendComplete?: boolean | null): string;
242
273
  /**
243
274
  * Render a {@link replayRun} summary as one printable page: a header block
244
275
  * (goal/shape/signed/branch/outcome/spent), a timeline (steps or iterations,
@@ -42,7 +42,8 @@ export function resolveSiblings(spinePath: string): {
42
42
  * Read one run's spine (+ its gate-audit sidecar, when present) off disk and
43
43
  * hand both to {@link replayRun}. Read-only, $0, mints no verdict.
44
44
  * @param {string} spinePath absolute or cwd-relative path to a `.jsonl` spine
45
- * @param {{preParsedSpine?: {records: any[], skipped: number}, skipAudit?: boolean}} [opts]
45
+ * @param {{preParsedSpine?: {records: any[], skipped: number}, skipAudit?: boolean,
46
+ * auditPathOverride?: string|null}} [opts]
46
47
  * `preParsedSpine`: avoids a second parse of the same file when the caller
47
48
  * ({@link listSpines}) already read it once to run {@link looksLikeSpine}.
48
49
  * `skipAudit` (PR #23 review item 5, 2026-08-26, carried over): a
@@ -50,6 +51,15 @@ export function resolveSiblings(spinePath: string): {
50
51
  * `summarizeForAllLine` reads none of the fields that sidecar feeds inside
51
52
  * `replayRun`, so reading every run's sidecar in a directory listing was
52
53
  * pure unused I/O, multiplied by every spine found.
54
+ * `auditPathOverride` (build item 7, 2026-09-28): when a caller has ALREADY
55
+ * resolved the real audit path through the one shared resolver
56
+ * (`src/panel/server.js`'s `resolveAuditPathForRow`, which also covers a
57
+ * still-LIVE run's during-run sidecar location — this function's own
58
+ * `resolveSiblings` call only ever finds the FINISHED-run convention), it
59
+ * passes that path here (`null` included) instead of letting this
60
+ * function re-derive a finished-only path on its own. `undefined` (the
61
+ * default) keeps the original resolveSiblings-only behaviour — every
62
+ * existing caller is unaffected.
53
63
  * @returns {ReturnType<typeof replayRun>}
54
64
  */
55
65
  export function replayOne(spinePath: string, opts?: {
@@ -58,6 +68,7 @@ export function replayOne(spinePath: string, opts?: {
58
68
  skipped: number;
59
69
  };
60
70
  skipAudit?: boolean;
71
+ auditPathOverride?: string | null;
61
72
  }): ReturnType<typeof replayRun>;
62
73
  /**
63
74
  * List every spine found directly in `dir` (not recursive — same as the
package/types/run.d.ts CHANGED
@@ -73,6 +73,29 @@
73
73
  * load gate refuses one: a refusal returns the distinct `recipe-stale` outcome with zero
74
74
  * spend rather than silently drafting cold, because starting a paid run on a decision
75
75
  * nobody made is the same class of error as widening a cap to manufacture a green.
76
+ * @param {number} [opts.draftSpentUsd=0] hamr's ruling 2026-09-28 ("one cap covers
77
+ * drafting + run") — money the AUTHORING pipeline (`authorCloseForJob`/
78
+ * `prepareSigning`, run under the SAME signed `budgetUsd` as its ceiling) already
79
+ * spent on THIS job before it was ever signed. The signed `budgetUsd` stays the
80
+ * one number in the spec hash; this shrinks the RUN's own enforced ceiling by
81
+ * what drafting already took, so the two phases never spend more than the one
82
+ * cap together. Deliberately NOT `priorSpentUsd` — that key means "a previous
83
+ * ATTEMPT of this same run died and folded its spend forward" and is read by
84
+ * `readResume`/`replay.js`'s `resumed` flag; a drafting spend is not a resume and
85
+ * must never make an ordinary first run of a job read as one. It is also never
86
+ * folded into `spentUsd` (the run's own ledger, cumulative-so-far and what a
87
+ * resume's `priorSpentUsd` seeds) or into `engagementSpentUsd` — drafting is a
88
+ * DIFFERENT phase's spend, reported beside the run's, never inside it. A garbage
89
+ * value (non-finite, negative) is read as 0, belted exactly like `priorSpentUsd`.
90
+ * @param {boolean} [opts.draftSpendComplete=true] hamr's ruling 2026-09-28 — was the
91
+ * `draftSpentUsd` fold above EXACT? The authoring pipeline's own metered-call list can
92
+ * go unpriced (F6) exactly like a resumed attempt's can, and the unknown does not heal
93
+ * by folding forward. A NEW field beside `draftSpentUsd`, never a new state inside
94
+ * `spendComplete`/`priorSpendComplete` — those answer "is THIS run's own ledger exact,"
95
+ * this answers "was the OTHER phase's ledger exact," and the two must never merge into
96
+ * one flag or a floor drafting spend could read as an exact run. Read only when
97
+ * `draftSpentUsd` itself is emitted; a garbage value is read as `true` (the same belt
98
+ * `priorSpendComplete` gets — an unreadable flag never manufactures a false floor).
76
99
  * @param {number} [opts.priorSpentUsd=0] RESUME (module C) — money a PREVIOUS, killed
77
100
  * attempt of this same try already spent. It seeds the ledger, so the restarted
78
101
  * attempt runs under the REMAINDER of the signed `budgetUsd` and the terminal
@@ -181,7 +204,7 @@
181
204
  * deliberately NOT in the resumable/checkpoint set (`src/reuse.js`) — an
182
205
  * unknown in-flight state is not a known-safe resume point.
183
206
  */
184
- export function runJob(rawSpec: object | string, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider, judgeModel, emit, capRuns, strikeLimit, shellCapUsd, closeTimeoutMs, closeDir, layerRoot, readShim, scout, bridge, priorSpentUsd, priorSpendComplete, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun, resumable }: {
207
+ export function runJob(rawSpec: object | string, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider, judgeModel, emit, capRuns, strikeLimit, shellCapUsd, closeTimeoutMs, closeDir, layerRoot, readShim, scout, bridge, draftSpentUsd, draftSpendComplete, priorSpentUsd, priorSpendComplete, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun, resumable }: {
185
208
  approvals: unknown;
186
209
  workdir: string;
187
210
  provider: any;
@@ -201,6 +224,8 @@ export function runJob(rawSpec: object | string, { approvals, workdir, provider,
201
224
  closeTimeoutMs?: number | undefined;
202
225
  closeDir?: string | null | undefined;
203
226
  bridge?: unknown;
227
+ draftSpentUsd?: number | undefined;
228
+ draftSpendComplete?: boolean | undefined;
204
229
  priorSpentUsd?: number | undefined;
205
230
  priorSpendComplete?: boolean | undefined;
206
231
  priorWallMs?: number | undefined;
@@ -92,6 +92,21 @@ export type RunOpts = {
92
92
  * the raw `--scout` value (`on`/`off`), or null for the default
93
93
  */
94
94
  scout?: string | null | undefined;
95
+ /**
96
+ * the raw `--draft-spent-usd` value (hamr's
97
+ * ruling 2026-09-28, "one cap covers drafting + run"), or null when the job never
98
+ * drafted through the panel/`bareloop author`. Parsed and validated in `execute`
99
+ * (finite, >= 0, else refused) — never coerced silently, the same param-guard
100
+ * class `--read-shim`/`--scout` already are.
101
+ */
102
+ draftSpentUsd?: string | null | undefined;
103
+ /**
104
+ * the raw `--draft-spend-incomplete` presence-
105
+ * flag (hamr's ruling 2026-09-28, 2nd addendum) — whether the drafting fold above was a
106
+ * FLOOR, never an exact figure. A new field beside `draftSpentUsd`, same param-guard
107
+ * class, no value to misread.
108
+ */
109
+ draftSpendIncomplete?: boolean | undefined;
95
110
  /**
96
111
  * a bridge registry directory
97
112
  */
@@ -129,6 +144,8 @@ export type RunCtx = {
129
144
  model: string | null;
130
145
  readShim: string | null;
131
146
  scout: string | null;
147
+ draftSpentUsd: string | null;
148
+ draftSpendIncomplete: boolean;
132
149
  registry: string | null;
133
150
  workflow: string | null;
134
151
  approve: string | null;