bareloop 0.6.0 → 0.7.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/reuse.js ADDED
@@ -0,0 +1,1602 @@
1
+ // Layer 3 — the D7 ENVELOPE and the REUSE RUNNER (design record
2
+ // docs/plans/2026-08-01-layer-3-reuse-design.md, D3/D5/D6/D7 + R1).
3
+ //
4
+ // hamr's sentence is the whole specification of this file, verbatim:
5
+ //
6
+ // *"part of reuse we ask user for cost/time and how many workflows to try before
7
+ // starting new like $5 and 30 mins x2 then start anew if both red"*
8
+ //
9
+ // So: three operator numbers signed up front, N tries at a stored workflow, then a cold
10
+ // draft — and every stop decision-ready. This module COMPOSES; it decides nothing the
11
+ // arbiter owns. The envelope is operator input, selection never edits a spec, and a
12
+ // bridge is minted only by this runner's own graded green.
13
+ //
14
+ // Four rules are structural here rather than conventional:
15
+ //
16
+ // - **The envelope may only TIGHTEN.** The advertised budget and the enforced budget are
17
+ // the same number, and the agent never widens its own cap. An envelope whose per-try
18
+ // numbers exceed the SIGNED spec's is a validation red — never a silent raise. And
19
+ // because tightening produces a genuinely different spec, it produces a different
20
+ // spec HASH: the tightened run is a new spec VERSION and the approval gate refuses it
21
+ // until the operator signs that version. This module cannot forge that signature and
22
+ // deliberately has no way to try (it hands `approvals` straight through).
23
+ // - **No defaults, anywhere in the envelope.** All three numbers are required and
24
+ // explicit, for `maxWallMs`'s reason: a defaulted cap is a silent second ceiling, and
25
+ // an unbounded run must be a VISIBLE operator choice.
26
+ // - **R1 — only a green writes the box.** A green appends a VERSION (the plan AS
27
+ // EXECUTED, read from the run's own spine) plus a history row. A graded red appends a
28
+ // history row and nothing else: the recipe that greened stays exactly as it greened.
29
+ // A CASUALTY is not a red — it keeps its own outcome name, and `deriveStatus` already
30
+ // refuses to demote on anything but a literal `'red'`.
31
+ // - **D3 — the operator's pick is the operator's.** A pin does not skip the selection
32
+ // call, it enters it; a model that will not use the pin must refuse it by name, and
33
+ // the run then STOPS and hands the decision back. Neither side gets to override the
34
+ // other silently.
35
+ //
36
+ // **F45, stated because it cannot be enforced from here:** a try's budget must fund the
37
+ // attempt PLUS its close, and nothing in this process can know what a close costs — the
38
+ // close is an operator command whose price is its own. So no threshold is invented (a
39
+ // threshold is arbiter territory, set from a measured base rate, never fitted to the
40
+ // sample in hand). What this module does instead is make the bind VISIBLE: every try row
41
+ // carries `spentUsd` against `capUsd`, `wallMs` against `wallCapMs`, `capBound`, and
42
+ // `closeReached` — a try whose money ran out before the close ever rendered a verdict is
43
+ // a fact on the row, not an inference the reader has to make.
44
+
45
+ import { existsSync } from 'node:fs';
46
+ import { join } from 'node:path';
47
+ import { createHash } from 'node:crypto';
48
+ import { createRequire } from 'node:module';
49
+ import { loadRegistry, saveBridge, appendGreen, appendRed, mintBridge, registryExists } from './bridges.js';
50
+ import { renderListing, selectionPrompt } from './selection.js';
51
+ import { stageClose } from './plan.js';
52
+ import { MIN_WALL_MS, jobSpecHash } from './job.js';
53
+ import { isObj, isNonEmptyString } from './validate.js';
54
+ import { readGrade } from './trend.js';
55
+ import { extractArtifact, priceOf } from './text.js';
56
+ import { runJob as shippedRunJob } from './run.js';
57
+
58
+ const require = createRequire(import.meta.url);
59
+ const { Loop } = require('bare-agent');
60
+
61
+ /** @typedef {{code: string, path: string, detail?: string}} Red */
62
+
63
+ /**
64
+ * The GRADED-RED set: the runJob outcomes in which the operator's close actually
65
+ * rendered a verdict on the tree and that verdict was RED. Only these demote (D6).
66
+ *
67
+ * `escalated` is the one. It is the terminal runPlan returns when the close judged the
68
+ * tree, the bounded fix loop ran its attempts with money still on the table, and the
69
+ * close was still red — i.e. the recipe was graded by the job's own truth and failed.
70
+ *
71
+ * Everything else non-green is a CASUALTY and keeps its own name:
72
+ * - `cap-halt` / `wall-halt` — governance stops. A money cut is never a capability read
73
+ * (F45) and the wall keeps the verdict already minted (W-2).
74
+ * - `provider-red` / `step-stalled` / `pricing-red` — transport and metering.
75
+ * - `close-red` / `close-unsupported` — the close FAULTED (broken, timed out, killed,
76
+ * crashed). It rendered no judgment, so its exit says nothing about the tree.
77
+ * - `recipe-stale` — the load gate refused the recipe at the door, $0 spent.
78
+ * - `plan-red` / `step-red:<id>` / `check-red` — the plan flow stopped before the close
79
+ * ever judged. Read as evidence about the RECIPE these would be a guess; they are
80
+ * recorded in full on the history row, which is where D6 puts the detail a human
81
+ * reads, and the coarse status ladder is left alone.
82
+ * - `unapproved-spec` / `job-red` / `smoke-red` / `interpreter-red` — the run never
83
+ * started.
84
+ */
85
+ export const REUSE_GRADED_RED = Object.freeze(['escalated']);
86
+
87
+ /**
88
+ * v1's definition of "the same KIND of recipe" — and it is the LOAD GATE's own: the
89
+ * close-stage names, in the same order (`bridges.loadGate` rule 2). One predicate,
90
+ * shared with the gate rather than spelled a second time here: a cold green appending
91
+ * to an entry the gate would refuse at the door is the two-transforms class pointed
92
+ * at the registry.
93
+ * @param {any} entry a registry entry @param {string[]} stageNames this job's close
94
+ */
95
+ function sameCloseShape(entry, stageNames) {
96
+ const stored = Array.isArray(entry?.closeStageNames) ? entry.closeStageNames : null;
97
+ return !!stored && stored.length === stageNames.length
98
+ && stored.every((/** @type {unknown} */ s, /** @type {number} */ i) => s === stageNames[i]);
99
+ }
100
+
101
+ /**
102
+ * The name a cold green FORKS to when the entry of the job's own name greened a
103
+ * different close. Derived from the shape itself, so it is deterministic: the same
104
+ * close always lands in the same file, and the next green of that shape appends there
105
+ * instead of minting a third entry.
106
+ *
107
+ * The separator is a HYPHEN, not the `~` first proposed: the registry name IS the
108
+ * filename and `validateBridge` holds it to the same kebab alphabet the job validator
109
+ * uses (`^[a-z0-9][a-z0-9-]*$`), so a `~` name would be refused by the entry's own
110
+ * validator and the green would be lost — which is the exact failure this fork exists
111
+ * to prevent.
112
+ * @param {string} name @param {string[]} stageNames @returns {string}
113
+ */
114
+ function shapeForkName(name, stageNames) {
115
+ return `${name}-${createHash('sha256').update(stageNames.join('\n')).digest('hex').slice(0, 8)}`;
116
+ }
117
+
118
+ /** the selection call's token ceiling. The answer is one small JSON object, but the cap
119
+ * is sized for the drafter's reason, not the answer's: an adaptive-thinking model can
120
+ * think past a tight budget and return EMPTY text at `stop=max_tokens` (measured, ~$0.40
121
+ * wasted per call), and F30 is the same class one step down. Same number the plan drafter
122
+ * uses — one spelling, so the two cannot drift. */
123
+ const SELECTION_MAX_TOKENS = 32_000;
124
+
125
+ // ── D7: the envelope ────────────────────────────────────────────────────────
126
+
127
+ /**
128
+ * Validate the reuse envelope — the three numbers the operator signs before the run.
129
+ *
130
+ * `{ perTryBudgetUsd, perTryWallMs, bridgeTries }`, all REQUIRED and explicit. There is
131
+ * no default for any of them and there will not be one: hamr's ruling on `maxWallMs`
132
+ * generalises — a defaulted cap is a silent second ceiling, and a cap the operator did
133
+ * not state is a cap nobody chose.
134
+ *
135
+ * `bridgeTries: 0` is legal and means FORCE COLD: zero stored workflows are tried, the
136
+ * run drafts from scratch, and no selection call is made (nothing to select among).
137
+ *
138
+ * With a `job`, the composition rule is enforced: the per-try numbers may only TIGHTEN
139
+ * the signed spec. A per-try budget above `job.budgetUsd`, or a per-try wall above
140
+ * `job.maxWallMs`, is `envelope-widens` — the runner may narrow the operator's cap, never
141
+ * widen it. A spec that sets NO `maxWallMs` is time-unbounded by explicit choice, so any
142
+ * wall is a tightening of it.
143
+ *
144
+ * @param {unknown} input the operator's envelope
145
+ * @param {{job?: unknown}} [opts] the SIGNED spec, when the composition is being checked
146
+ * @returns {{ ok: boolean, reds: Red[], envelope: any }}
147
+ */
148
+ export function validateEnvelope(input, { job } = {}) {
149
+ /** @type {Red[]} */
150
+ const reds = [];
151
+ /** @type {(code: string, path: string, detail?: string) => void} */
152
+ const red = (code, path, detail) => { reds.push(detail ? { code, path, detail } : { code, path }); };
153
+ if (!isObj(input)) {
154
+ return { ok: false, reds: [{ code: 'invalid-value', path: '$', detail: 'the reuse envelope is an object { perTryBudgetUsd, perTryWallMs, bridgeTries }' }], envelope: null };
155
+ }
156
+ const e = /** @type {Record<string, any>} */ (input);
157
+ for (const key of Object.keys(e)) {
158
+ if (!['perTryBudgetUsd', 'perTryWallMs', 'bridgeTries'].includes(key)) red('unknown-field', key, 'fields: perTryBudgetUsd, perTryWallMs, bridgeTries');
159
+ }
160
+
161
+ // Each number is checked for PRESENCE first and separately: an absent cap and an
162
+ // out-of-bounds one are different operator mistakes, and collapsing them would let a
163
+ // missing number be reported as if a number had been read.
164
+ if (e.perTryBudgetUsd === undefined) red('missing-required', 'perTryBudgetUsd', 'the per-try money cap, in USD — explicit, never defaulted');
165
+ else if (!(typeof e.perTryBudgetUsd === 'number' && Number.isFinite(e.perTryBudgetUsd) && e.perTryBudgetUsd > 0)) {
166
+ red('bounds', 'perTryBudgetUsd', 'a number greater than 0 (cap-not-estimate)');
167
+ }
168
+
169
+ if (e.perTryWallMs === undefined) red('missing-required', 'perTryWallMs', 'the per-try wall-clock cap, in ms — explicit, never defaulted');
170
+ else if (!(typeof e.perTryWallMs === 'number' && Number.isInteger(e.perTryWallMs) && e.perTryWallMs >= MIN_WALL_MS)) {
171
+ red('bounds', 'perTryWallMs', `integer milliseconds >= ${MIN_WALL_MS} (one close timeout — a try that cannot fund its own close produces an unreadable row, and an unreadable row is a casualty, not evidence: F45)`);
172
+ }
173
+
174
+ if (e.bridgeTries === undefined) red('missing-required', 'bridgeTries', 'how many stored workflows to try before drafting cold — 0 means force-cold');
175
+ else if (!(typeof e.bridgeTries === 'number' && Number.isInteger(e.bridgeTries) && e.bridgeTries >= 0)) {
176
+ red('bounds', 'bridgeTries', 'a non-negative integer (0 = force-cold)');
177
+ }
178
+
179
+ // the composition with the SIGNED spec — tighten only, in both dimensions
180
+ if (isObj(job)) {
181
+ const j = /** @type {Record<string, any>} */ (job);
182
+ if (typeof e.perTryBudgetUsd === 'number' && typeof j.budgetUsd === 'number' && e.perTryBudgetUsd > j.budgetUsd) {
183
+ red('envelope-widens', 'perTryBudgetUsd', `${e.perTryBudgetUsd} exceeds the signed budget ${j.budgetUsd} — the envelope may only TIGHTEN a signed cap; raising one is a spec edit the operator signs, never a runner's silent raise`);
184
+ }
185
+ // an ABSENT maxWallMs is time-unbounded by explicit operator choice, so every wall
186
+ // tightens it — the only widening direction that exists is a bigger number over a
187
+ // stated one
188
+ if (typeof e.perTryWallMs === 'number' && typeof j.maxWallMs === 'number' && e.perTryWallMs > j.maxWallMs) {
189
+ red('envelope-widens', 'perTryWallMs', `${e.perTryWallMs} exceeds the signed wall ${j.maxWallMs} — the envelope may only TIGHTEN a signed cap`);
190
+ }
191
+ }
192
+
193
+ return reds.length === 0
194
+ ? { ok: true, reds: [], envelope: { perTryBudgetUsd: e.perTryBudgetUsd, perTryWallMs: e.perTryWallMs, bridgeTries: e.bridgeTries } }
195
+ : { ok: false, reds, envelope: null };
196
+ }
197
+
198
+ /**
199
+ * The PER-TRY spec: the signed job with the envelope's caps written onto it. Pure — the
200
+ * input spec is never mutated.
201
+ *
202
+ * This is what every try actually runs, cold leg included, so "$5 and 30 mins x2 then
203
+ * start anew" means the same numbers all the way down.
204
+ *
205
+ * **It is a spec VERSION, not a spec edit made behind the operator's back.** When the
206
+ * envelope equals the spec's own numbers the result is hash-IDENTICAL and the existing
207
+ * approval covers it. When it tightens, the hash MOVES — and `runJob`'s approval gate
208
+ * refuses the run until the operator has signed that hash. That refusal is the feature:
209
+ * the tightened numbers are the ones enforced, so they are the ones signed.
210
+ *
211
+ * @param {object} job the signed spec
212
+ * @param {{perTryBudgetUsd: number, perTryWallMs: number}} envelope a validated envelope
213
+ * @returns {object} the per-try spec
214
+ */
215
+ export function resolveTrySpec(job, envelope) {
216
+ return { ...job, budgetUsd: envelope.perTryBudgetUsd, maxWallMs: envelope.perTryWallMs };
217
+ }
218
+
219
+ /**
220
+ * The RESOLVED REUSE ARTIFACT — the thing the operator actually signs.
221
+ *
222
+ * The envelope is THREE numbers, and the third one is a MULTIPLIER: the worst case a
223
+ * reuse run authorizes is `perTryBudgetUsd × (bridgeTries + 1)`. A signature taken over
224
+ * the per-try spec alone covers only two of the three — `--tries 0` and `--tries 9` hash
225
+ * identically while authorizing ten times the spend. So the signed artifact is this
226
+ * WRAPPER, and `reuseSpecHash` is what an approval gate prints and compares.
227
+ *
228
+ * It is a wrapper rather than a field on the spec because `validateJob` reds every
229
+ * unknown top-level field: a `bridgeTries` written onto the per-try spec would flip the
230
+ * hash and then make every try a `job-red`. The spec half stays exactly the spec `runJob`
231
+ * executes, unchanged in kind.
232
+ *
233
+ * `bridgeTries` is READ ONCE, here, and the run's loop bound is read back off this same
234
+ * object — the number enforced and the number signed are the same number from the same
235
+ * place, never two reads of the envelope that a later edit could separate.
236
+ *
237
+ * @param {object} job the signed spec
238
+ * @param {{perTryBudgetUsd: number, perTryWallMs: number, bridgeTries: number}} envelope
239
+ * @returns {{schema: string, spec: any, bridgeTries: number}}
240
+ */
241
+ export function resolveReuse(job, envelope) {
242
+ return { schema: 'reuse-v1', spec: resolveTrySpec(job, envelope), bridgeTries: envelope.bridgeTries };
243
+ }
244
+
245
+ /**
246
+ * The APPROVAL HASH: one number covering all three envelope numbers.
247
+ *
248
+ * Composed from the repo's ONE spec hasher rather than re-canonizing the wrapper, so the
249
+ * per-try spec half is hashed exactly as `jobSpecHash` hashes it — MED-1's resolved-`tools`
250
+ * pinning included, and with no second canonicalizer that could drift from it. The
251
+ * `reuse-v1` prefix is domain separation: a reuse approval can never collide with a bare
252
+ * job approval.
253
+ *
254
+ * **This is a signing-SCHEME change, and it invalidates every hash signed before it — by
255
+ * design.** A signature that did not cover the try count never covered the run's worst
256
+ * case, so it is not carried forward: there is no legacy acceptance path, and re-signing
257
+ * once is the whole migration.
258
+ *
259
+ * @param {{spec: any, bridgeTries: number}} resolved a `resolveReuse` artifact
260
+ * @returns {string} sha256 hex
261
+ */
262
+ export function reuseSpecHash(resolved) {
263
+ return createHash('sha256')
264
+ .update(`reuse-v1\nspec:${jobSpecHash(resolved.spec)}\nbridgeTries:${JSON.stringify(resolved.bridgeTries)}\n`)
265
+ .digest('hex');
266
+ }
267
+
268
+ // ── D3: the selection call ──────────────────────────────────────────────────
269
+
270
+ /**
271
+ * Ask the model which stored workflow (if any) this run should start from — ONE call, on
272
+ * the drafter-tier provider the CALLER hands in. No provider is constructed here: the
273
+ * provider binding is shell territory, exactly as it is for every other call in this
274
+ * repo.
275
+ *
276
+ * The three operator overrides ride into the same call rather than around it:
277
+ * - `forceCold` skips everything ($0, no tokens) — the operator has already decided.
278
+ * - `shortlist` restricts the LISTING to those names, so the model chooses among the
279
+ * operator's set and cannot reach past it.
280
+ * - `pinned` does NOT bypass the call. D3: the model may refuse a pin only EXPLICITLY,
281
+ * with a reason. So the pin is stated in the prompt, and if the answer names anything
282
+ * other than the pin the result is `{refused: true}` — the substitute is NOT adopted,
283
+ * and the decision goes back to the operator. Neither direction is silent.
284
+ *
285
+ * An EMPTY candidate set also skips the call: paying a model to pick from an empty
286
+ * listing buys nothing, and "there is nothing to reuse" is not a judgment call.
287
+ *
288
+ * The answer is parsed with the repo's ONE model-output parser (`extractArtifact`) and
289
+ * then strictly: an object with a `choice` that is null or a name FROM THE LISTING, plus
290
+ * a `reason`. A name that is not on the listing is a named red, never used — a picker
291
+ * that can name a workflow nobody offered is a picker that can reach outside the
292
+ * operator's set.
293
+ *
294
+ * Cost is metered and reported (F6): `costUsd` is the priced figure or an explicit null,
295
+ * and `spendComplete` says whether that figure is exact. A skipped call is a real 0.
296
+ *
297
+ * @param {object} opts
298
+ * @param {unknown} opts.registry `loadRegistry`'s result, or a bare array of entries
299
+ * @param {object} opts.job the job this run is signed against (only its goal is used here)
300
+ * @param {string} opts.ask the ask, in the operator's own words
301
+ * @param {any} opts.provider the drafter-tier provider binding (caller-owned)
302
+ * @param {string|null} [opts.pinned] the operator's pin
303
+ * @param {string[]|null} [opts.shortlist] the operator's shortlist
304
+ * @param {boolean} [opts.forceCold]
305
+ * @param {string[]} [opts.exclude] names already tried this run
306
+ * @returns {Promise<any>} `{ choice, reason, called, refused, forcedCold, red, costUsd,
307
+ * spendComplete, candidates, listing }`
308
+ */
309
+ export async function selectBridge({ registry, job, ask, provider, pinned = null, shortlist = null, forceCold = false, exclude = [] }) {
310
+ const r = /** @type {any} */ (registry);
311
+ const all = (Array.isArray(r) ? r : (Array.isArray(r?.bridges) ? r.bridges : [])).filter(isObj);
312
+ const skipReds = Array.isArray(r?.reds) ? r.reds : [];
313
+ const excluded = new Set(exclude);
314
+ const shortSet = Array.isArray(shortlist) ? new Set(shortlist) : null;
315
+ const candidates = all.filter((/** @type {any} */ b) => !excluded.has(b.name) && (!shortSet || shortSet.has(b.name)));
316
+
317
+ /** the shape every return here holds, so a caller never branches on field presence */
318
+ const base = { choice: null, reason: '', called: false, refused: false, forcedCold: false, red: null, costUsd: 0, spendComplete: true, candidates: candidates.map((/** @type {any} */ b) => b.name), listing: null, pinned };
319
+
320
+ if (forceCold) {
321
+ return { ...base, forcedCold: true, reason: 'the operator forced a cold draft — no workflow was offered and no selection call was made' };
322
+ }
323
+ if (candidates.length === 0) {
324
+ return { ...base, reason: exclude.length || shortSet ? 'no workflow remains to offer (every candidate was excluded or outside the shortlist)' : 'the registry holds no reusable workflow' };
325
+ }
326
+
327
+ // the listing the MODEL reads is the listing a human reads — same function, same text,
328
+ // and the skip reds travel with it so a picker is never handed a set shorter than the
329
+ // directory without being told
330
+ const listing = renderListing({ ok: skipReds.length === 0, reds: skipReds, bridges: candidates });
331
+ const prompt = selectionPrompt(listing, ask, pinned);
332
+
333
+ let result;
334
+ try {
335
+ const loop = new Loop({ provider });
336
+ result = await loop.run([{ role: 'user', content: prompt }], [], { maxTokens: SELECTION_MAX_TOKENS });
337
+ } catch (e) {
338
+ // a throw out of the selection call is TRANSPORT — a casualty, and the run stops on
339
+ // it rather than quietly drafting cold on a decision that was never made
340
+ return { ...base, called: true, listing, costUsd: null, spendComplete: false, red: { code: 'provider-red', path: 'selection', detail: String(/** @type {Error} */ (e)?.message ?? e) } };
341
+ }
342
+ const { costUsd, unpricedRounds } = priceOf(result);
343
+ const met = { called: true, listing, costUsd, spendComplete: costUsd !== null && unpricedRounds === 0 };
344
+
345
+ if (result?.error) {
346
+ // bare-agent short-circuits an API-truncated round into `error` — provider-red by
347
+ // standing doctrine (never a config-red blaming the picker)
348
+ return { ...base, ...met, red: { code: 'provider-red', path: 'selection', detail: String(result.error) } };
349
+ }
350
+
351
+ const { code } = extractArtifact(result?.text);
352
+ let parsed;
353
+ try { parsed = JSON.parse(code ?? ''); } catch (e) {
354
+ return { ...base, ...met, red: { code: 'selection-unparseable', path: 'selection', detail: `the answer was not the one JSON object the prompt asks for: ${String(/** @type {Error} */ (e)?.message ?? e)}` } };
355
+ }
356
+ if (!isObj(parsed)) {
357
+ return { ...base, ...met, red: { code: 'selection-unparseable', path: 'selection', detail: 'the answer parsed to something other than a JSON object' } };
358
+ }
359
+ const p = /** @type {Record<string, any>} */ (parsed);
360
+ const reason = isNonEmptyString(p.reason) ? p.reason : '(no reason given)';
361
+ if (p.choice !== null && !isNonEmptyString(p.choice)) {
362
+ return { ...base, ...met, reason, red: { code: 'selection-invalid', path: 'choice', detail: 'choice must be an exact name from the listing, or null' } };
363
+ }
364
+ if (p.choice !== null && !base.candidates.includes(p.choice)) {
365
+ return { ...base, ...met, reason, red: { code: 'selection-invalid', path: 'choice', detail: `"${p.choice}" is not on the listing it was given — a pick outside the offered set is never used` } };
366
+ }
367
+
368
+ // D3's pin rule, both directions. The pin is honoured when confirmed; when it is not,
369
+ // the ANSWER is not adopted — a refusal is a decision for the operator, and adopting
370
+ // the model's substitute would be exactly the silent override the rule forbids.
371
+ if (pinned && p.choice !== pinned) {
372
+ return { ...base, ...met, refused: true, reason };
373
+ }
374
+ return { ...base, ...met, choice: p.choice, reason };
375
+ }
376
+
377
+ // ── the try loop ────────────────────────────────────────────────────────────
378
+
379
+ /** what the run's own spine says about a try, read back after it returns
380
+ * @param {any[]} events @returns {any} */
381
+ function readTry(events) {
382
+ const last = (/** @type {string} */ t) => events.filter((e) => e.type === t).at(-1) ?? null;
383
+ const je = last('job-end');
384
+ const oc = last('outer-close');
385
+ const pa = last('plan-accepted');
386
+ // The stage that rendered the LAST verdict. `outer-close` is emitted ONCE, BEFORE the
387
+ // close-fix loop — and `escalated`, the only outcome that demotes, is decided AFTER
388
+ // it — so reading the stage from `outer-close` names a stale wall whenever the fix
389
+ // loop moved it. Every later verdict arrives as ralph's `close-verdict`; a STEP's
390
+ // exit-loop verdict comes through the same event type but carries no stage (its judge
391
+ // is the form evaluator, not the close), which is exactly what disqualifies it here.
392
+ const cv = events.filter((e) => e.type === 'close-verdict' && isNonEmptyString(e.stage)).at(-1) ?? null;
393
+ const stage = cv ?? oc;
394
+ const turns = events.filter((e) => (e.type === 'worker-round' && e.kind === 'turn') || e.type === 'worker-turn').length;
395
+ const pricedWork = events.some((e) => e.type === 'worker-round' || e.type === 'worker-turn')
396
+ || (typeof je?.spentUsd === 'number' && je.spentUsd > 0);
397
+ return {
398
+ // F6: a job-end that never landed is an UNKNOWN spend, not a zero. `spentUsd` is
399
+ // read as a number or explicit null, and `spendComplete` can never be true without
400
+ // a figure to complete.
401
+ spentUsd: typeof je?.spentUsd === 'number' && Number.isFinite(je.spentUsd) ? je.spentUsd : null,
402
+ spendComplete: typeof je?.spentUsd === 'number' && je?.spendComplete === true,
403
+ // the plan AS EXECUTED (R1): the last accepted plan is the one that ran — a replan
404
+ // emits its own `plan-accepted`, and it is the post-replan artifact that inherits
405
+ plan: pa?.plan ?? null,
406
+ // The worker's turn count, across BOTH metering surfaces. The Loop path emits one
407
+ // `worker-round` of kind 'turn' per turn; the NATIVE path emits `worker-turn` per
408
+ // turn plus ONE `worker-round` of kind 'session' carrying the authoritative cost —
409
+ // so counting only the first spelling reads every native try as 0. And a slice that
410
+ // shows priced work with no turn signal at all is an UNKNOWN count, never a zero:
411
+ // a zero there reads as "the worker did nothing" for money that was spent (F6).
412
+ rounds: turns > 0 ? turns : (pricedWork ? null : 0),
413
+ // which stage rendered the verdict, and whether the close was reached AT ALL — the
414
+ // second is F45's visible half: a try whose cap bound before any grading is a
415
+ // casualty by shape, and the row says so instead of leaving it to be inferred
416
+ failingStage: isNonEmptyString(stage?.stage) ? stage.stage : null,
417
+ closeReached: oc !== null,
418
+ };
419
+ }
420
+
421
+ /** the three-way verdict read of a runJob outcome, in ONE place: a green, the one
422
+ * GRADED red that demotes (D6), or a casualty that keeps its own name. Both the live
423
+ * try loop and the resume reader classify through this — two spellings of "what kind
424
+ * of answer was that" is how a casualty ends up demoting a recipe.
425
+ * @param {string} outcome @returns {'green'|'red'|'casualty'} */
426
+ function verdictClassOf(outcome) {
427
+ if (outcome === 'green' || outcome === 'already-green') return 'green';
428
+ return REUSE_GRADED_RED.includes(outcome) ? 'red' : 'casualty';
429
+ }
430
+
431
+ // ── module C: RESUME AFTER KILL ─────────────────────────────────────────────
432
+
433
+ /**
434
+ * The STEP-LEVEL checkpoint inside a killed try's window — the ONE reader, so the seed
435
+ * is derived from the spine and from nothing else (no parallel bookkeeping).
436
+ *
437
+ * hamr's ruling supersedes the try-restart reading, verbatim: *"even if it gets killed by
438
+ * outside, it should allow resume and start last step instead from the beginning, why
439
+ * would i want to waste more money on something i already started, our goal is to find
440
+ * ways to save money and time"*. A try-level restart re-pays for the scout, the draft and
441
+ * every step that had already finished; this reads the FINEST checkpoint the spine can
442
+ * actually prove, and refuses to guess past it.
443
+ *
444
+ * Three readings, and each is grounded in one event the executor emits:
445
+ *
446
+ * - **No `plan-accepted` in the window** → `null`. The kill landed in the scout or the
447
+ * draft, nothing durable was produced, and the existing try-restart is already right.
448
+ * - **`plan-accepted` and no `outer-close` after it** → `phase: 'steps'`. The plan is the
449
+ * LAST accepted one (a replan emits its own, and it is the post-replan plan that was
450
+ * executing), and the completed steps are the ones judged after it.
451
+ * - **`outer-close` after it** → `phase: 'close'`. Every step finished; what remains is
452
+ * the close and its fix loop. The close re-runs for no tokens — it is a command over
453
+ * the tree, stateless by construction.
454
+ *
455
+ * **Completion is `step-end{outcome:'green'}` OR `step-skipped`.** The second is not a
456
+ * detail: a resumed leg records its inherited steps as skips, so a reader that counted
457
+ * only `step-end` would make every resume-of-a-resume re-run and re-pay for exactly the
458
+ * work the previous resume correctly skipped. Both carry the `seq` of the record they
459
+ * rest on, so a skip can always be traced to the event that licensed it.
460
+ *
461
+ * A green under a plan the run later REPLANNED away is deliberately not counted: the
462
+ * executor resets its own index and artifacts at a replan, so those greens belong to a
463
+ * plan nobody is executing any more.
464
+ *
465
+ * @param {any[]} seen the open try's own events, in file order
466
+ * @returns {{phase: string, plan: any, completedSteps: {id: string, seq: number|null, by: string}[], planSeq: number|null}|null}
467
+ */
468
+ function readStepCheckpoint(seen) {
469
+ let at = -1;
470
+ for (let i = 0; i < seen.length; i += 1) if (seen[i].type === 'plan-accepted') at = i;
471
+ if (at === -1) return null;
472
+ const plan = seen[at].plan;
473
+ // A plan-accepted whose plan is unreadable cannot be reloaded — and a HALF plan is
474
+ // worse than none, because the steps read against it would skip by index into
475
+ // something nobody validated. No seed is the honest answer, and it degrades to
476
+ // exactly the try-restart this run already had.
477
+ //
478
+ // UNREACHABLE through the runner today and deliberately kept (the `runCheck` precedent):
479
+ // a kill mid-append leaves a truncated final LINE, which the runner drops before
480
+ // parsing, so a partial plan-accepted never reaches this reader from a real spine. It is
481
+ // defence against a second caller, not a live path.
482
+ if (!isObj(plan) || !Array.isArray(/** @type {any} */ (plan).steps)) return null;
483
+ const after = seen.slice(at + 1);
484
+ /** @type {{id: string, seq: number|null, by: string}[]} */
485
+ const completedSteps = [];
486
+ for (const e of after) {
487
+ const done = (e.type === 'step-end' && e.outcome === 'green') || e.type === 'step-skipped';
488
+ if (done && isNonEmptyString(e.step)) {
489
+ completedSteps.push({ id: e.step, seq: typeof e.seq === 'number' ? e.seq : null, by: e.type });
490
+ }
491
+ }
492
+ return {
493
+ phase: after.some((e) => e.type === 'outer-close') ? 'close' : 'steps',
494
+ plan,
495
+ completedSteps,
496
+ // the record this reading rests on, so a reach-back is traceable to its own evidence
497
+ planSeq: typeof seen[at].seq === 'number' ? seen[at].seq : null,
498
+ };
499
+ }
500
+
501
+ /**
502
+ * The dead leg's CLOSE GRADES, in order — the trend baselines a resumed leg
503
+ * inherits (PRD v1.46 §3, and hamr's approved #2: *a resumed run's trend judges the
504
+ * whole tree*).
505
+ *
506
+ * Without this, a resumed leg's halt readout restarts the trend at its own first
507
+ * close: a leg cut after one grade reads `unknown` while the run it continues had a
508
+ * measured direction, and a leg that is flat on its own evidence reads `flat` on a
509
+ * chain that was converging — which recommends rewriting a goal that was working.
510
+ *
511
+ * WHAT COUNTS AS A CLOSE GRADE is exactly what the live reader counts (planrun's
512
+ * `runTrend` is fed at `judgeClose`, from every RED verdict the arbiter renders):
513
+ * the precheck, the outer close, and each of the fix loop's own closes. Nothing
514
+ * else. Read from the four records the executor already emits, in file order:
515
+ *
516
+ * - `close-precheck` / `outer-close` — the two closes that ride under their own
517
+ * record types. `needs_revision` only: a green ends the run, and a close FAULT
518
+ * rendered no judgment at all, so folding either would put a non-grade in the
519
+ * series (the same rule `judgeClose` applies).
520
+ * - `ladder` with `governor: 'close-trend'` — the PRIMARY source for the fix loop.
521
+ * It carries `{stage, value}` already read by the live instrument, so nothing is
522
+ * parsed twice and the seam cannot disagree with the reading it inherits.
523
+ * - `close-verdict` — the FALLBACK, for a spine written before that governor
524
+ * existed (the fix loop was bounded by the retired count and emitted no ladder
525
+ * reading at all). Used only when the spine carries no `close-trend` record.
526
+ *
527
+ * THE FALLBACK IS GUARDED, and the guard is the whole difficulty. `close-verdict` is
528
+ * ralph's record, and the STEP loop is a ralph loop too — a step's failing exit
529
+ * emits one, with a detail spelled `check "clean-run" red: …`. Parsed blind that
530
+ * line donates a number that is not a close grade at all, into a bucket the close
531
+ * never named: precisely the axis-merging src/trend.js's accuracy law forbids. So
532
+ * the fallback reads only records AFTER the `fix-loop` marker, which is exactly the
533
+ * population the primary source covers — the two sources are substitutable rather
534
+ * than merely similar.
535
+ *
536
+ * KNOWN LIMIT, stated rather than discovered later. This reads ONE spine, so a
537
+ * resume of a resume inherits leg 2's grades and not leg 1's: the seeded prefix is
538
+ * not re-emitted onto the new spine (it is history, not this leg's evidence, and a
539
+ * leg that re-emitted its inheritance would double-count it on the next read). The
540
+ * chain therefore shortens by one leg per resume. Fail-safe in the direction that
541
+ * matters: a shorter chain can only ever under-claim a direction.
542
+ *
543
+ * @param {any[]} seen the open window's own events, in file order
544
+ * @returns {{stage: string|null, value: number}[]} counts and stage names ONLY —
545
+ * never a gap byte. The spine is append-only forever, so a close byte that
546
+ * crosses this seam crosses it for good.
547
+ */
548
+ function readGradeSeed(seen) {
549
+ const hasGovernor = seen.some((e) => e.type === 'ladder' && e.governor === 'close-trend');
550
+ /** @type {{stage: string|null, value: number}[]} */
551
+ const out = [];
552
+ let inFixLoop = false;
553
+ for (const e of seen) {
554
+ if (e.type === 'fix-loop') { inFixLoop = true; continue; }
555
+ /** @type {{stage: string|null, value: number|null}|null} */
556
+ let read = null;
557
+ if (e.type === 'close-precheck' || e.type === 'outer-close') {
558
+ if (e.verdict === 'needs_revision') read = readGrade(e.gap);
559
+ } else if (e.type === 'ladder' && e.governor === 'close-trend') {
560
+ // already read by the live instrument — taken verbatim, never re-parsed
561
+ read = { stage: isNonEmptyString(e.stage) ? e.stage : null, value: typeof e.value === 'number' ? e.value : null };
562
+ } else if (!hasGovernor && inFixLoop && e.type === 'close-verdict' && e.verdict === 'needs_revision') {
563
+ read = readGrade(e.gap);
564
+ }
565
+ // an unreadable grade donates NOTHING — not a zero, not a placeholder (F6)
566
+ if (read && typeof read.value === 'number' && Number.isFinite(read.value)) out.push({ stage: read.stage, value: read.value });
567
+ }
568
+ return out;
569
+ }
570
+
571
+ /**
572
+ * Read a killed reuse run's OWN SPINE back into the state a resume continues from.
573
+ *
574
+ * hamr's checkpoint ruling is the whole contract: *"money, signature and checkpoint
575
+ * (starts from where it stopped) if mid loop, restart that loop"*. So this reader
576
+ * answers exactly four questions, and nothing else:
577
+ *
578
+ * 1. **Which tries COMPLETED** — they are never re-run, and their bridges stay
579
+ * spoken for exactly as if the loop had carried on.
580
+ * 2. **Was the process mid-try** — a `try-start` with no matching `try-end`. That
581
+ * try was never graded, so it is not consumed: it RESTARTS from its beginning.
582
+ * 3. **What that dead attempt already spent, in money and in wall time** — folded
583
+ * into the restart so it runs under the REMAINDER of its signed per-try numbers.
584
+ * 4. **Which envelope this spine belongs to** — the `reuse-start` record's own
585
+ * approval hash, so a resume can prove it is continuing the SAME signed run.
586
+ *
587
+ * **F45 governs the arithmetic here.** A window's spend is summed from `worker-round`
588
+ * ONLY — the same event `runJob`'s own ledger accounts, no other. The selection calls
589
+ * are real money and are counted, but as the RUN's cost and never as a try's: a
590
+ * picker's tokens are not a worker's, and attributing them to a try would misstate
591
+ * both the fold and the row. `worker-turn` (native attribution, cost null by design)
592
+ * is deliberately not summed, exactly as the live ledger does not sum it.
593
+ *
594
+ * **The declared fold, not a re-derivation.** A restarted try's own `try-start`
595
+ * carries the fold it inherited (`priorSpentUsd`/`priorWallMs`), so a resume OF a
596
+ * resume adds only its own new rounds. Re-deriving from the whole file would bill an
597
+ * abandoned attempt twice.
598
+ *
599
+ * **KNOWN LIMIT, stated rather than discovered later.** A call that was killed BEFORE it
600
+ * returned left no event at all — no `worker-round`, no `selection-result` — so money it
601
+ * may already have been billed for is not on the spine and cannot be folded. This reader
602
+ * can only account writers that WROTE (the same limit the runner's own guards have: a
603
+ * hung `generate()` leaves zero trace). The fold is therefore a floor in exactly the case
604
+ * `priorSpendComplete` already marks, and one lost selection call is re-paid by the
605
+ * resumed pick rather than silently attributed to a try.
606
+ *
607
+ * **A try whose `job-end` landed is COMPLETE, not mid-flight.** The close already
608
+ * rendered its verdict; restarting would pay a second time for an answer already in
609
+ * hand. When the death fell between that verdict and the registry write, the row that
610
+ * was lost is NAMED (`r1Missing`) rather than silently absent — and never re-derived,
611
+ * because a green's version has to be the plan AS EXECUTED and this reader is not
612
+ * where that is decided.
613
+ *
614
+ * **THE HALT AMENDMENT (PRD v1.46 §3).** "A landed `job-end` is COMPLETE" is true of
615
+ * a VERDICT, and a governance halt is not one: a money cap-halt (and, W-2-symmetric,
616
+ * a wall-halt) means the run ran out of an operator-owned allowance with its work on
617
+ * disk and its plan on the spine — the exact case hamr's ruling was about ("why would
618
+ * i want to waste more money on something i already started"). `resumableOutcomes`
619
+ * names which terminals get read that way. It is EMPTY by default, so the reuse
620
+ * loop's own semantics are byte-unchanged: there, a cap-halted try was graded, its
621
+ * registry row was written, and turning it into a restart would re-run a try whose
622
+ * fold leaves it no money at all and duplicate its row. Green and every red stay
623
+ * non-resumable under any setting — a verdict already rendered is never re-bought.
624
+ *
625
+ * **DIRECT SPINES (`direct`).** A `runJob` spine has no `reuse-start` and no try
626
+ * windows, so this reader saw one as "not a reuse run" and every priced round in it
627
+ * as a STRAY. Opted in, the run itself is read as ONE implicit try opened at
628
+ * `job-start` — the same window machinery, never a second reader, because the whole
629
+ * failure class this reader exists to survive (F45) is the writer nobody modelled.
630
+ * The fold it declares comes off `job-start`'s own `priorSpentUsd`, exactly as a
631
+ * restarted try's comes off its `try-start`: re-deriving from the file would bill an
632
+ * abandoned attempt twice in a chain of resumes.
633
+ *
634
+ * @param {any[]} events the dead spine's events, parsed, in file order
635
+ * @param {{deathAt?: number|null, direct?: boolean, resumableOutcomes?: string[]}} [opts]
636
+ * `deathAt`: when the process is judged to have died (the watchdog's kill record,
637
+ * which is later and better evidence than the last event). Defaults to the last
638
+ * event's own timestamp — the last sign of life there is.
639
+ * @returns {any} `{ started, approvalHash, specHash, patient, job, bridgeTries,
640
+ * perTryBudgetUsd, perTryWallMs, ended, endOutcome, greened, completed, tried,
641
+ * selectionCostUsd, selectionComplete, strayRounds, r1Missing, restart, deathAtKnown,
642
+ * spentUsd, spendComplete, carrySpentUsd, carrySpendComplete }`
643
+ */
644
+ export function readResume(events, { deathAt = null, direct = false, resumableOutcomes = [] } = {}) {
645
+ const list = Array.isArray(events) ? events.filter(isObj) : [];
646
+ /** which landed terminals are a CHECKPOINT rather than a graded row */
647
+ const resumableHalt = new Set(Array.isArray(resumableOutcomes) ? resumableOutcomes : []);
648
+ // the FIRST reuse-start is the envelope this spine was opened under. A resumed spine
649
+ // holds one per process; the signature gate compares against the ORIGINAL, because
650
+ // that is the run being continued.
651
+ const head = list.find((e) => e.type === 'reuse-start') ?? null;
652
+ const end = list.filter((e) => e.type === 'reuse-end').at(-1) ?? null;
653
+ /** A DIRECT spine is one runJob with no envelope around it. It is read as a single
654
+ * implicit try ONLY when the caller asked for that AND there is genuinely no reuse
655
+ * envelope on the file — never inferred from the absence alone, because "this spine
656
+ * carries no reuse-start" is also the honest refusal a reuse runner needs. */
657
+ const directRun = direct && head === null && !list.some((e) => e.type === 'try-start');
658
+
659
+ /** @type {any[]} */
660
+ const completed = [];
661
+ /** @type {string[]} */
662
+ const tried = [];
663
+ let selectionCostUsd = 0;
664
+ let selectionComplete = true;
665
+ let strayRounds = 0;
666
+ let strayCostUsd = 0;
667
+ /** @type {any} the try currently open: a `try-start` with no `try-end` yet */
668
+ let open = null;
669
+ /** @type {{n: any, seen: any[]}[]} windows a kill abandoned, oldest first — kept ONLY
670
+ * so a checkpoint can be reached back through an attempt that died before re-accepting
671
+ * its own plan. Their money is never re-read from here (the restart declares its fold). */
672
+ const abandoned = [];
673
+
674
+ /** @param {any} ev */
675
+ const openTry = (ev) => ({
676
+ n: ev.n, mode: ev.mode, bridge: ev.bridge ?? null,
677
+ startedAtMs: Date.parse(String(ev.ts)),
678
+ // the fold this attempt was ALREADY carrying (a restart declares it on its own
679
+ // try-start) — never re-derived from the file
680
+ // `> 0` belt matches the four sibling fold sites (tail-review F-4): a negative
681
+ // declared fold on a corrupt spine must never cancel real rounds and widen the cap
682
+ declaredSpentUsd: typeof ev.priorSpentUsd === 'number' && Number.isFinite(ev.priorSpentUsd) && ev.priorSpentUsd > 0 ? ev.priorSpentUsd : 0,
683
+ declaredWallMs: typeof ev.priorWallMs === 'number' && Number.isFinite(ev.priorWallMs) && ev.priorWallMs > 0 ? ev.priorWallMs : 0,
684
+ declaredComplete: ev.priorSpendComplete !== false,
685
+ // the ROUND fold, and the one field where absence and zero are different answers.
686
+ // A declared `null` is the predecessor saying it could not count its own turns, and
687
+ // it stays unknown forever after (F6); a MISSING field is a spine written before the
688
+ // declaration existed, or a try nobody resumed — both are honestly zero.
689
+ declaredRounds: !('priorRounds' in ev) ? 0
690
+ : (typeof ev.priorRounds === 'number' && Number.isFinite(ev.priorRounds) && ev.priorRounds >= 0 ? ev.priorRounds : null),
691
+ roundsUsd: 0, roundsComplete: true, r1Written: false, jobEnd: null,
692
+ /** @type {any[]} the window's own events, so the SAME `readTry` the live loop uses
693
+ * reads a graded-but-unrecorded try — never a second reader spelling it differently */
694
+ seen: [],
695
+ });
696
+
697
+ for (const ev of list) {
698
+ if (ev.type === 'selection-result') {
699
+ if (typeof ev.costUsd === 'number' && Number.isFinite(ev.costUsd)) selectionCostUsd += ev.costUsd;
700
+ else selectionComplete = false;
701
+ continue;
702
+ }
703
+ if (ev.type === 'try-start') {
704
+ // a new try-start while one is OPEN means the open one was abandoned (a kill, and
705
+ // this is the restart). Its spend is not added here: the restart's own try-start
706
+ // declares the fold it inherited, so counting the window again would double-bill.
707
+ //
708
+ // Its CHECKPOINT is kept, though. Measured on the real killed run: leg 3 restarted,
709
+ // spent its close precheck and died in the redraft, so its own window carries no
710
+ // plan-accepted at all — reading only the open window there discards a checkpoint
711
+ // leg 2 paid $8.18 to reach. The window is abandoned; the work it proved is not.
712
+ if (open) {
713
+ if (open.bridge) tried.push(open.bridge);
714
+ abandoned.push({ n: open.n, seen: open.seen });
715
+ }
716
+ open = openTry(ev);
717
+ continue;
718
+ }
719
+ // A DIRECT spine's `job-start` opens the one implicit window, through the SAME
720
+ // constructor a `try-start` uses — the run IS the try. `n: 1` and `mode: 'direct'`
721
+ // are what it is: one attempt, no bridge, no envelope leg. The fold rides on the
722
+ // same three declared fields, so a chain of resumes is accounted identically.
723
+ if (directRun && !open && ev.type === 'job-start') {
724
+ open = openTry({ ...ev, n: 1, mode: 'direct', bridge: null });
725
+ open.seen.push(ev);
726
+ continue;
727
+ }
728
+ if (!open) {
729
+ // a priced worker round outside every try window: nothing in this runner emits
730
+ // one, so it is a writer this reader does not model. It is COUNTED and reported
731
+ // rather than dropped — the blind-instrument class is what F45 is about.
732
+ if (ev.type === 'worker-round') {
733
+ strayRounds += 1;
734
+ if (typeof ev.costUsd === 'number' && Number.isFinite(ev.costUsd)) strayCostUsd += ev.costUsd;
735
+ else selectionComplete = false;
736
+ }
737
+ continue;
738
+ }
739
+ open.seen.push(ev);
740
+ if (ev.type === 'worker-round') {
741
+ // the ONE event the live ledger accounts (src/run.js) — same rule, same spelling
742
+ if (typeof ev.costUsd === 'number' && Number.isFinite(ev.costUsd)) open.roundsUsd += ev.costUsd;
743
+ else open.roundsComplete = false;
744
+ } else if (ev.type === 'job-end') {
745
+ open.jobEnd = ev;
746
+ } else if (ev.type === 'bridge-write') {
747
+ open.r1Written = true;
748
+ } else if (ev.type === 'try-end') {
749
+ completed.push({ ...ev, seq: undefined, ts: undefined, inherited: true });
750
+ if (open.bridge) tried.push(open.bridge);
751
+ open = null;
752
+ }
753
+ }
754
+
755
+ const lastTs = list.length ? Date.parse(String(list.at(-1).ts)) : NaN;
756
+ const died = typeof deathAt === 'number' && Number.isFinite(deathAt) ? deathAt : lastTs;
757
+ const deathAtKnown = Number.isFinite(died);
758
+
759
+ /** @type {any} */
760
+ let restart = null;
761
+ let r1Missing = false;
762
+ /** v1.46 §3 — a landed terminal that is a governance HALT rather than a verdict.
763
+ * The close never got the say it was asked for: the run ran out of an operator-owned
764
+ * allowance with its work on disk and its plan on the spine, so the window is a
765
+ * CHECKPOINT and re-entering it is the ruling, not a second payment for an answer
766
+ * already in hand. Empty by default, which is what keeps the reuse loop's own
767
+ * graded-row semantics exactly where they were. */
768
+ const haltedResumable = open?.jobEnd ? resumableHalt.has(String(open.jobEnd.outcome)) : false;
769
+ if (open) {
770
+ if (open.jobEnd && !haltedResumable) {
771
+ // GRADED: runJob returned and the close had its say. Not a restart — the row is
772
+ // reconstructed with the live loop's own reader, and the registry write that the
773
+ // death may have cost is named.
774
+ const read = readTry(open.seen);
775
+ // the try STOPPED at its own terminal, not at the kill — the process may have gone
776
+ // on for minutes afterwards writing the box it never finished. The precise answer
777
+ // is on the record this branch is reading, so the kill time is the FALLBACK for a
778
+ // terminal whose stamp cannot be parsed, never the first reading.
779
+ const endedAt = Date.parse(String(open.jobEnd.ts));
780
+ const stoppedAt = Number.isFinite(endedAt) ? endedAt : died;
781
+ completed.push({
782
+ n: open.n, mode: open.mode, bridge: open.bridge,
783
+ runOutcome: open.jobEnd.outcome, verdictClass: verdictClassOf(String(open.jobEnd.outcome)),
784
+ failingStage: read.failingStage, closeReached: read.closeReached,
785
+ spentUsd: read.spentUsd, spendComplete: read.spendComplete,
786
+ capUsd: head?.perTryBudgetUsd ?? null,
787
+ wallMs: Number.isFinite(stoppedAt) && Number.isFinite(open.startedAtMs) ? open.declaredWallMs + (stoppedAt - open.startedAtMs) : null,
788
+ wallCapMs: head?.perTryWallMs ?? null,
789
+ rounds: read.rounds, inherited: true, rowLostToKill: true,
790
+ });
791
+ if (open.bridge) tried.push(open.bridge);
792
+ r1Missing = !open.r1Written;
793
+ } else {
794
+ const windowRounds = readTry(open.seen).rounds;
795
+ restart = {
796
+ n: open.n, mode: open.mode, bridge: open.bridge,
797
+ // WHERE inside the try the kill landed — the plan it accepted and the steps it
798
+ // finished, or null when it died before a plan existed (hamr: "start last step
799
+ // instead from the beginning"). Money and signature are unchanged by it.
800
+ //
801
+ // Read from THIS window first, and only then from the newest abandoned window of
802
+ // the SAME try: an attempt that died before re-accepting the plan (the real leg-3
803
+ // shape) still has its predecessor's checkpoint standing behind it, and the work
804
+ // that checkpoint proves is on disk either way. Never across tries — a plan
805
+ // belongs to the try that earned it, and running one try's workflow as another's
806
+ // is the substitution D3 forbids at the selection seam.
807
+ seed: readStepCheckpoint(open.seen)
808
+ ?? abandoned.filter((w) => w.n === open.n).map((w) => readStepCheckpoint(w.seen)).filter(Boolean).at(-1)
809
+ ?? null,
810
+ // the CLOSE GRADES this leg recorded, so the resumed leg's halt readout spans
811
+ // the chain rather than restarting at its own first close (readGradeSeed).
812
+ // Read from THIS window only, never the abandoned ones: a plan checkpoint
813
+ // survives an abandoned attempt because the work it proves is on disk, but a
814
+ // grade is a reading of a tree at a moment, and the abandoned attempt's own
815
+ // restart already re-graded that tree. Counting both would enter the same
816
+ // reading twice and flatten a chain that moved.
817
+ grades: readGradeSeed(open.seen),
818
+ priorSpentUsd: open.declaredSpentUsd + open.roundsUsd,
819
+ // F83 — was that fold EXACT? Three sources, because `runJob` decides the same
820
+ // question from more than this window can see (src/run.js: `!unpriced &&
821
+ // !stalled && !cutMidCall && !priorFloor`):
822
+ // - what the attempt DECLARED it inherited (`priorFloor`, one resume back),
823
+ // - the rounds this reader can count (`unpriced`), and
824
+ // - the attempt's OWN terminal, when it landed one.
825
+ // The third is what closes the seam. A `stalled` or `cutMidCall` floor leaves
826
+ // every counted round PRICED, so the first two read exact and the fold came
827
+ // back exact — a leg that honestly said `spendComplete: false` was laundered
828
+ // into an exact total one function call later (F6). `readTry`, the GRADED
829
+ // branch of this same reader, has always consulted the terminal; only this one
830
+ // skipped it, and the asymmetry was the tell.
831
+ //
832
+ // Read only where a terminal EXISTS — the resumable-halt shape. The ordinary
833
+ // killed-mid-flight restart has no `job-end` in its window at all and is
834
+ // unchanged by construction: nothing to consult is not an unknown, it is the
835
+ // same two sources as before. `!== false` matches `declaredComplete` above,
836
+ // the sibling fold field on the same object: `runJob` emits the flag on EVERY
837
+ // terminal precisely so no consumer branches on presence, so absence means a
838
+ // spine older than the flag, never a floor being hidden.
839
+ priorSpendComplete: open.declaredComplete && open.roundsComplete
840
+ && (open.jobEnd ? open.jobEnd.spendComplete !== false : true),
841
+ priorWallMs: deathAtKnown && Number.isFinite(open.startedAtMs)
842
+ ? open.declaredWallMs + Math.max(0, died - open.startedAtMs)
843
+ : open.declaredWallMs,
844
+ // the ROUNDS the dead attempt bought, so the restarted try's row is read against
845
+ // the same span its money and its wall already are. Counted with `readTry` — the
846
+ // one turn-counting rule this file has — never a second arithmetic. Null when
847
+ // either half is unknown: a fold that cannot be established honestly is not
848
+ // folded, and the row states the unknown rather than a number it invented (F6).
849
+ priorRounds: open.declaredRounds !== null && typeof windowRounds === 'number'
850
+ ? open.declaredRounds + windowRounds
851
+ : null,
852
+ };
853
+ if (open.bridge) tried.push(open.bridge);
854
+ }
855
+ }
856
+
857
+ // the money, in two figures that are deliberately NOT the same one:
858
+ // - `carry*` is what a resumed run seeds its own ledger with. The restart's fold is
859
+ // excluded, because it is handed to `runJob` as `priorSpentUsd` and comes back
860
+ // inside the restarted try's own terminal — seeding it here too would count it twice.
861
+ // - `spentUsd` is everything the dead run spent, which is what a human reads.
862
+ let carrySpentUsd = selectionCostUsd + strayCostUsd;
863
+ let carrySpendComplete = selectionComplete;
864
+ for (const t of completed) {
865
+ if (typeof t.spentUsd === 'number' && Number.isFinite(t.spentUsd)) carrySpentUsd += t.spentUsd;
866
+ else carrySpendComplete = false;
867
+ if (t.spendComplete !== true) carrySpendComplete = false;
868
+ }
869
+ const spentUsd = carrySpentUsd + (restart ? restart.priorSpentUsd : 0);
870
+ const spendComplete = carrySpendComplete && (!restart || restart.priorSpendComplete);
871
+
872
+ /** the terminal a DIRECT spine landed on, which is its `job-end` — there is no
873
+ * `reuse-end` on a run nobody wrapped in an envelope. A resumable halt is
874
+ * deliberately NOT "ended": the caller's refusal gate reads that field to mean
875
+ * "there is nothing left to continue", and the whole amendment is that on a halt
876
+ * there is. `endOutcome` names it either way, so nothing is hidden. */
877
+ const directEnd = directRun ? open?.jobEnd ?? null : null;
878
+ /** a direct spine's own head record — `job-start` carries the identity fields the
879
+ * envelope would otherwise supply (which job, which spec version, what ceiling) */
880
+ const directHead = directRun ? list.find((e) => e.type === 'job-start') ?? null : null;
881
+ return {
882
+ started: head !== null || directHead !== null,
883
+ approvalHash: head?.approvalHash ?? null,
884
+ specHash: head?.specHash ?? directHead?.specHash ?? null,
885
+ patient: head?.patient ?? null,
886
+ job: head?.job ?? directHead?.job ?? null,
887
+ bridgeTries: typeof head?.bridgeTries === 'number' ? head.bridgeTries : null,
888
+ perTryBudgetUsd: head?.perTryBudgetUsd ?? directHead?.budgetUsd ?? null,
889
+ perTryWallMs: head?.perTryWallMs ?? null,
890
+ ended: end !== null || (directEnd !== null && !haltedResumable),
891
+ endOutcome: end?.outcome ?? directEnd?.outcome ?? null,
892
+ greened: completed.some((t) => t.verdictClass === 'green'),
893
+ completed,
894
+ tried,
895
+ selectionCostUsd, selectionComplete, strayRounds,
896
+ r1Missing, restart, deathAtKnown,
897
+ spentUsd, spendComplete, carrySpentUsd, carrySpendComplete,
898
+ };
899
+ }
900
+
901
+ /**
902
+ * The PATIENT gate for a resume, which is deliberately NOT the fresh-launch one.
903
+ *
904
+ * A fresh reuse run refuses a dirty tree: a run that inherits the previous run's edits
905
+ * measures the wrong thing. A RESUME is the opposite case — the dead tries' work is real
906
+ * progress this run already paid for, the tries share one workdir by design, and the tree
907
+ * is reset between RUNS by the operator, never by the harness. So a dirty tree is exactly
908
+ * what a resume expects to find, and it is continued as the run left it: not reset, not
909
+ * refused.
910
+ *
911
+ * What IS refused is HEAD moving off the seed. That is not the run's own work — nothing
912
+ * in the flow commits — so it means a human committed, rebased or checked something out
913
+ * under the dead run. The reconstruction's premise (this tree is where try N left it) no
914
+ * longer holds, and that decision comes back to the operator.
915
+ *
916
+ * @param {{head: string, seed: string, dirty: string}} state as `git rev-parse HEAD` and
917
+ * `git status --porcelain` report it
918
+ * @returns {{ok: boolean, detail: string|null}}
919
+ */
920
+ export function resumeTreeGate({ head, seed, dirty }) {
921
+ if (head !== seed) {
922
+ return {
923
+ ok: false,
924
+ detail: `HEAD is ${String(head).slice(0, 12)}, not the frozen seed ${String(seed).slice(0, 12)} — something COMMITTED or rebased under the dead run. Nothing in a run does that, so this is operator intervention and the resume stops rather than continuing on a tree it cannot vouch for.`,
925
+ };
926
+ }
927
+ // `dirty` is not even read for a verdict: it is the expected state. Named in the return
928
+ // so a caller can report what it is continuing on top of.
929
+ return { ok: true, detail: dirty ? `continuing a WORKING tree (${dirty.trimEnd().split('\n').length} changed paths) — the dead tries' progress is kept, never reset` : null };
930
+ }
931
+
932
+ /**
933
+ * Run a job under the reuse envelope: select a stored workflow, try it, and — if the
934
+ * envelope pre-authorized it — try the next one, then draft cold.
935
+ *
936
+ * The shape, end to end:
937
+ *
938
+ * 1. **Envelope** validated against the SIGNED spec (tighten-only). A red stops here,
939
+ * before the registry is read and before anything is spent.
940
+ * 2. **Registry** loaded from the operator-supplied directory (never a default location).
941
+ * A missing directory is a named red; a malformed ENTRY is skipped, reported, and
942
+ * carried into the listing so the picker is never handed a silently shorter set.
943
+ * 3. **Up to `bridgeTries` tries.** Each try re-runs selection against a FRESHLY RELOADED
944
+ * registry with the already-tried names excluded — so the next pick sees the row the
945
+ * last try just wrote, and cannot re-pick a workflow this run already spent money on.
946
+ * The chosen entry rides into `runJob`, whose own load gate is the D2 shape check; a
947
+ * gate refusal (`recipe-stale`) costs $0 and simply moves to the next candidate.
948
+ * 4. **After each try the box is written (R1)** — green appends a version + a row, a
949
+ * graded red appends a row, a casualty appends a row under its own name. Saved
950
+ * atomically, one write per try.
951
+ * 5. **A green ENDS the loop.** The job is done; there is nothing left to try.
952
+ * 6. **Tries exhausted → the cold leg**, under the SAME per-try numbers ("then start
953
+ * anew"). Its green mints a NEW bridge named for the job slug — or appends to the
954
+ * entry of that name if one already exists, because clobbering a green is exactly the
955
+ * thing R1 exists to prevent.
956
+ *
957
+ * Fallthrough is automatic ONLY because the envelope pre-authorized it (D7) — that is
958
+ * what makes it not a per-failure nag, and it is also why nothing else here is automatic:
959
+ * a refused pin, an unreadable selection answer and a missing registry all STOP and hand
960
+ * the decision back.
961
+ *
962
+ * **RESUME (module C).** With `resume` — a `readResume` reading of the dead run's own
963
+ * spine — the same loop picks up where a killed run stopped: the completed tries are
964
+ * seeded in (never re-run, their bridges never re-offered), a try that was mid-flight is
965
+ * RESTARTED from its beginning against the SAME workflow and with NO second selection
966
+ * call (the pick was made and paid for), and it runs under the REMAINDER of its own
967
+ * signed per-try numbers. The remainder is expressed as a FOLD of the dead attempt's
968
+ * spend and wall — never as tightened caps — because the caps are in the spec hash and
969
+ * rewriting them would need a signature nobody typed. If the remainder cannot fund the
970
+ * restart the run caps honestly, having launched nothing.
971
+ *
972
+ * @param {object} opts
973
+ * @param {object} opts.job the signed job spec
974
+ * @param {unknown} opts.approvals approval records — passed straight through to `runJob`;
975
+ * this module never mints one (a tightened envelope makes a NEW spec version, and the
976
+ * operator signs it)
977
+ * @param {string} opts.registryDir the operator-supplied bridge registry
978
+ * @param {unknown} opts.envelope `{ perTryBudgetUsd, perTryWallMs, bridgeTries }`
979
+ * @param {string} opts.patient the instance this run works on — the distinct-patient
980
+ * count IS the status ladder (D6/R2), so it is required and never guessed from a path
981
+ * @param {string} opts.workdir the run directory
982
+ * @param {any} opts.provider the worker provider binding
983
+ * @param {any} [opts.selectionProvider] the drafter-tier provider for the selection call
984
+ * (defaults to `provider`)
985
+ * @param {(type: string, data?: object) => object} opts.emit spine emitter
986
+ * @param {string} [opts.ask] the ask in the operator's words (defaults to the job's goal)
987
+ * @param {string|null} [opts.pinned]
988
+ * @param {string[]|null} [opts.shortlist]
989
+ * @param {boolean} [opts.forceCold]
990
+ * @param {string} [opts.runid] the run id the bridge rows point back at (defaults to a
991
+ * fresh one); the operator runner passes its own so a row names its spine file
992
+ * @param {any} [opts.providerFor] per-step model-tier factory, forwarded
993
+ * @param {any} [opts.nativeProvider] native provider factory, forwarded
994
+ * @param {number} [opts.capRuns] forwarded
995
+ * @param {number} [opts.strikeLimit] forwarded (a plan step's strike ceiling)
996
+ * @param {number} [opts.closeTimeoutMs] forwarded
997
+ * @param {boolean} [opts.layerRoot] forwarded
998
+ * @param {any} [opts.resume] RESUME (module C) — a `readResume` reading of the KILLED
999
+ * run's own spine. Its `approvalHash` must match this run's, or the run refuses:
1000
+ * a resume continues ONE signed run, not any run.
1001
+ * @param {typeof shippedRunJob} [opts.runJob] the job runner. An injected seam of the
1002
+ * same class as `provider`: a try is a real paid run, and the loop's own logic —
1003
+ * selection order, exclusion, minting, exhaustion — is not testable against one.
1004
+ * @param {() => number} [opts.now] the clock, injected (per-try wall measurement)
1005
+ * @returns {Promise<any>} `{ outcome, tries, selection, spentUsd, spendComplete,
1006
+ * bridgeWrites, decision, options, reds, triesAuthorized, triesUsed, envelope, specHash,
1007
+ * approvalHash }` — `specHash` is the per-try spec's (what `runJob`'s own gate reads);
1008
+ * `approvalHash` is the OPERATOR's, covering all three envelope numbers
1009
+ */
1010
+ export async function runReuse(opts) {
1011
+ const {
1012
+ job, approvals, registryDir, patient, workdir, provider, selectionProvider,
1013
+ emit, ask, pinned = null, shortlist = null, forceCold = false,
1014
+ providerFor, nativeProvider, capRuns, strikeLimit, closeTimeoutMs, layerRoot,
1015
+ runJob = shippedRunJob, now = () => Date.now(),
1016
+ } = opts;
1017
+ const runid = opts.runid ?? Date.now().toString(36);
1018
+ const resume = isObj(opts.resume) ? /** @type {any} */ (opts.resume) : null;
1019
+ /** a try that inherits nothing: the ordinary, non-resumed attempt */
1020
+ const NO_PRIOR = { spentUsd: 0, wallMs: 0, spendComplete: true, rounds: /** @type {number|null} */ (0), grades: /** @type {any[]} */ ([]) };
1021
+
1022
+ /** @type {any[]} */
1023
+ const tries = [];
1024
+ /** @type {any[]} */
1025
+ const selection = [];
1026
+ /** @type {any[]} */
1027
+ const bridgeWrites = [];
1028
+
1029
+ /** the whole run's money, F6-honest: the sum of PRICED figures only, with a one-way
1030
+ * floor flag. A try that could not say what it spent makes the total a floor, and a
1031
+ * floor that reads as exact is F6 in an honest coat. */
1032
+ let spentUsd = 0;
1033
+ let spendComplete = true;
1034
+ /** @param {number|null} c @param {boolean} complete */
1035
+ const account = (c, complete) => {
1036
+ if (typeof c === 'number' && Number.isFinite(c)) spentUsd += c; else spendComplete = false;
1037
+ if (!complete) spendComplete = false;
1038
+ };
1039
+
1040
+ /** every terminal goes through here, so a caller reads ONE shape whatever stopped it
1041
+ * @param {string} outcome @param {object} [extra]
1042
+ * @param {boolean} [escalate] emit the reuse-level escalation. False for the ONE stop
1043
+ * whose category the ledger CLASSIFIES (`interpreter-red`): runJob already filed it
1044
+ * from the fault's own site with its typed `lib`, and a second copy would count one
1045
+ * wiring fault twice against an upstream package. The terminal stays decision-ready
1046
+ * in the RETURN value either way. */
1047
+ const done = (outcome, extra = {}, escalate = true) => {
1048
+ const out = {
1049
+ outcome, tries, selection, spentUsd, spendComplete, bridgeWrites,
1050
+ // read off the SIGNED artifact once one exists — the count reported IS the count
1051
+ // enforced IS the count hashed. The raw echo covers the envelope-red case only,
1052
+ // where nothing resolved and nothing ran.
1053
+ triesAuthorized: resolved ? resolved.bridgeTries : (isObj(opts.envelope) ? /** @type {any} */ (opts.envelope).bridgeTries : null),
1054
+ triesUsed: tries.filter((t) => t.mode === 'bridge').length,
1055
+ envelope: env?.envelope ?? null, specHash: trySpecHash, approvalHash, reds: [], decision: null, options: [], category: null,
1056
+ ...extra,
1057
+ };
1058
+ emit('reuse-end', { outcome: out.outcome, triesUsed: out.triesUsed, triesAuthorized: out.triesAuthorized, spentUsd: out.spentUsd, spendComplete: out.spendComplete, bridgeWrites: bridgeWrites.map((w) => `${w.action} ${w.name}`) });
1059
+ if (out.decision && escalate) {
1060
+ // the escalation's CATEGORY names the REUSE-level stop, not the last try's outcome:
1061
+ // each try already escalated under its own name from inside `runJob`, and repeating
1062
+ // that name here would file the envelope's story under the run's. Every category
1063
+ // this emits is in the ledger's executable excluded-set — an unmapped one would be
1064
+ // COUNTED as an unclassified lib bug, which is the point of that set.
1065
+ emit('escalation', { category: out.category ?? out.outcome, decisionReady: true, decision: out.decision, options: out.options, detail: out.detail ?? undefined });
1066
+ }
1067
+ return out;
1068
+ };
1069
+
1070
+ // ── 1. the envelope, before anything is read or spent
1071
+ let trySpecHash = null;
1072
+ /** the operator-facing signature: all three envelope numbers, one hash */
1073
+ let approvalHash = null;
1074
+ /** @type {{schema: string, spec: any, bridgeTries: number}|null} the SIGNED artifact —
1075
+ * every number this run enforces is read back off it, never off the raw envelope */
1076
+ let resolved = null;
1077
+ const env = validateEnvelope(opts.envelope, { job });
1078
+ if (!env.ok) {
1079
+ emit('envelope-red', { reds: env.reds });
1080
+ return done('envelope-red', {
1081
+ reds: env.reds,
1082
+ decision: 'The reuse envelope does not compose with the signed job spec, so no run was started.',
1083
+ options: ['correct the envelope (it may only TIGHTEN the signed caps)', 'raise the spec\'s own caps — a spec edit, whose new hash needs re-approval'],
1084
+ detail: env.reds.map((r) => `${r.code}:${r.path}${r.detail ? ` — ${r.detail}` : ''}`).join('\n'),
1085
+ });
1086
+ }
1087
+ const envelope = env.envelope;
1088
+ // the signed artifact, resolved ONCE: the per-try spec `runJob` executes plus the try
1089
+ // count, hashed together so the signature covers the run's whole worst case
1090
+ resolved = resolveReuse(job, envelope);
1091
+ const trySpec = resolved.spec;
1092
+ trySpecHash = jobSpecHash(trySpec);
1093
+ approvalHash = reuseSpecHash(resolved);
1094
+ emit('reuse-start', {
1095
+ job: job.job, patient, registryDir, specHash: trySpecHash, approvalHash,
1096
+ perTryBudgetUsd: envelope.perTryBudgetUsd, perTryWallMs: envelope.perTryWallMs, bridgeTries: resolved.bridgeTries,
1097
+ pinned, shortlist, forceCold,
1098
+ });
1099
+
1100
+ // ── 2. the registry. Its path is operator-supplied and never conjured — a run that
1101
+ // would mint into a directory nobody created is a typo, not a decision.
1102
+ if (!registryExists(registryDir)) {
1103
+ return done('registry-red', {
1104
+ reds: [{ code: 'registry-missing', path: String(registryDir), detail: 'the registry path is operator-supplied and is never conjured' }],
1105
+ decision: `The bridge registry ${JSON.stringify(registryDir)} does not exist, so no workflow could be offered and no green could be recorded.`,
1106
+ options: ['create the registry directory', 'point --registry at the right one'],
1107
+ });
1108
+ }
1109
+
1110
+ const askText = isNonEmptyString(ask) ? ask : job.goal;
1111
+ /** @type {Set<string>} names this run has already spent a try on */
1112
+ const tried = new Set();
1113
+
1114
+ /** the shared per-try execution: run the job, read the spine, write the box (R1).
1115
+ * @param {any|null} bridge @param {number} n
1116
+ * @param {{spentUsd: number, wallMs: number, spendComplete: boolean, rounds: number|null, grades: any[]}} [prior]
1117
+ * RESUME — what a KILLED attempt of this same try already consumed, and what it
1118
+ * already measured. Money and wall are FOLDED into the attempt (`runJob`'s own ledger
1119
+ * and clock start partly spent) rather than shrinking the caps, so the signed per-try
1120
+ * numbers stay the numbers hashed and the numbers reported. `rounds` folds the same
1121
+ * way but on the ROW alone (nothing enforces a round bound here), and `null` — an
1122
+ * attempt whose turns could not be counted — is reported, never folded. `grades` are
1123
+ * the close readings that leg recorded, handed on as the run trend's BASELINES: they
1124
+ * feed the HALT READOUT alone and spend none of this attempt's bounds.
1125
+ * @param {any} [seed] RESUME (v2) — WHERE that attempt was killed (`readResume`'s
1126
+ * `restart.seed`): the plan it accepted and the steps it finished. The fold says how
1127
+ * much of the try's allowance is gone; this says how much of its WORK is done, so the
1128
+ * restart re-pays for neither the scout, the draft, nor a finished step. Null is the
1129
+ * scout/draft death, where nothing paid is re-payable and the try simply restarts.
1130
+ * @returns {Promise<any>} the try row */
1131
+ const runTry = async (bridge, n, prior = NO_PRIOR, seed = null) => {
1132
+ const mode = bridge ? 'bridge' : 'cold';
1133
+ const name = bridge ? bridge.name : null;
1134
+ const resumed = prior.spentUsd > 0 || prior.wallMs > 0;
1135
+ emit('try-start', {
1136
+ n, mode, bridge: name, capUsd: envelope.perTryBudgetUsd, wallCapMs: envelope.perTryWallMs,
1137
+ // DECLARED on the spine, and only when there is one — a non-resumed run's spine
1138
+ // stays byte-identical. The declaration is what makes a resume OF a resume fold
1139
+ // once: the next reader adds this attempt's own rounds to the number stated here
1140
+ // instead of re-deriving the whole history and double-billing an abandoned attempt.
1141
+ // `priorRounds` rides in that same declaration, INCLUDING when it is null: absence
1142
+ // means "a spine older than this field", and a reader that cannot tell that from a
1143
+ // predecessor's honest unknown would launder the unknown into a zero (F6).
1144
+ ...(resumed ? { priorSpentUsd: prior.spentUsd, priorWallMs: prior.wallMs, priorSpendComplete: prior.spendComplete, priorRounds: prior.rounds, resumedFrom: resume?.fromRunid ?? runid } : {}),
1145
+ // WHERE it picks up, on the row's own record: a try that restarts at step 3 of 4 is
1146
+ // a materially different attempt from one that restarts at its beginning, and a
1147
+ // reader must not have to infer which happened from the absence of a scout.
1148
+ ...(seed ? { resumedAt: { phase: seed.phase, stepsDone: seed.completedSteps.length, stepsPlanned: seed.plan?.steps?.length ?? null } } : {}),
1149
+ });
1150
+ /** @type {any[]} the try's OWN slice of the spine — the caller's emitter still sees
1151
+ * every event; this is a tap, never a replacement (the run's books stay one book) */
1152
+ const seen = [];
1153
+ const tap = (/** @type {string} */ type, /** @type {object} */ data) => { seen.push({ type, ...(data ?? {}) }); return emit(type, data); };
1154
+ const started = now();
1155
+ let outcome;
1156
+ try {
1157
+ outcome = await runJob(trySpec, {
1158
+ approvals, workdir, provider, nativeProvider, providerFor, emit: tap,
1159
+ shellCapUsd: envelope.perTryBudgetUsd,
1160
+ ...(capRuns !== undefined ? { capRuns } : {}),
1161
+ ...(strikeLimit !== undefined ? { strikeLimit } : {}),
1162
+ ...(closeTimeoutMs !== undefined ? { closeTimeoutMs } : {}),
1163
+ ...(layerRoot !== undefined ? { layerRoot } : {}),
1164
+ // the fold, and whether the fold is EXACT. `priorSpendComplete` used to stop at
1165
+ // the `try-start` record: it was computed, written down, and then not handed to
1166
+ // the one component whose `job-end` the row's own `spendComplete` is read off —
1167
+ // so a restart of a partly-unpriced attempt came back looking exact. The unknown
1168
+ // travels WITH the money it qualifies (F6).
1169
+ ...(resumed ? { priorSpentUsd: prior.spentUsd, priorWallMs: prior.wallMs, priorSpendComplete: prior.spendComplete } : {}),
1170
+ // the dead leg's close grades, as the run trend's BASELINES. Same class of
1171
+ // omission the fold above was: computed by `readResume`, declared on the record,
1172
+ // and then not handed to the component whose halt readout is read off it — a
1173
+ // restarted leg would report `flat` on a run that was converging. Only when there
1174
+ // ARE grades: an empty array is the cold path, and the cold path stays identical.
1175
+ ...(prior.grades.length ? { resumeGrades: prior.grades } : {}),
1176
+ ...(seed ? { resumeSeed: seed } : {}),
1177
+ bridge,
1178
+ });
1179
+ } catch (e) {
1180
+ // runJob is contracted not to throw, so a throw here is the runner itself dying.
1181
+ // It is a casualty with an UNKNOWN spend — the one thing it must not do is report
1182
+ // a zero that reads as exact.
1183
+ outcome = 'runner-crashed';
1184
+ seen.push({ type: 'runner-crashed', detail: String(/** @type {Error} */ (e)?.message ?? e) });
1185
+ }
1186
+ const read = readTry(seen);
1187
+ // the try's WHOLE wall, both attempts: the fold is time this try already consumed,
1188
+ // and a row quoting only the restart's minutes against the signed cap would read as
1189
+ // a try that ran comfortably inside a wall it had in fact nearly exhausted
1190
+ const wallMs = prior.wallMs + (now() - started);
1191
+ // and the try's WHOLE round count, on the same fold and the same reason — a row
1192
+ // reading "$8.59 / 20 rounds" for 113 rounds bought states two numbers measured over
1193
+ // different spans. Unknown on either side stays unknown: an unfoldable count is
1194
+ // reported as null, never as the half of it this leg happens to be able to see (F6).
1195
+ // Cold, `prior.rounds` is 0 and this is exactly `read.rounds`.
1196
+ const rounds = typeof read.rounds === 'number' && typeof prior.rounds === 'number' ? prior.rounds + read.rounds : null;
1197
+ const verdictClass = verdictClassOf(outcome);
1198
+ const row = /** @type {Record<string, any>} */ ({
1199
+ n, mode, bridge: name, runOutcome: outcome, verdictClass,
1200
+ failingStage: read.failingStage, closeReached: read.closeReached,
1201
+ // `spentUsd` is the try's WHOLE spend: `runJob`'s ledger started at the fold, so
1202
+ // its terminal already states both attempts. The dead attempt's money is never
1203
+ // laundered into $0 by a restart (F6/F12).
1204
+ spentUsd: read.spentUsd, spendComplete: read.spendComplete, capUsd: envelope.perTryBudgetUsd,
1205
+ // `capBound` is the row's own reading of the money bind, and it is the OUTCOME that
1206
+ // says so — never a comparison of spend against cap, which cannot tell a run that
1207
+ // finished at its cap from one the cap cut off.
1208
+ capBound: outcome === 'cap-halt',
1209
+ wallMs, wallCapMs: envelope.perTryWallMs, wallBound: outcome === 'wall-halt',
1210
+ rounds, plan: read.plan, bridgeWrite: null,
1211
+ ...(resumed ? { restarted: true, priorSpentUsd: prior.spentUsd, priorWallMs: prior.wallMs, priorRounds: prior.rounds } : {}),
1212
+ });
1213
+ // F6 on the resumed row: when the restart could not say what it spent, the fold is
1214
+ // still a KNOWN floor and is kept as one — dropping it would report less money than
1215
+ // the run is already known to have spent.
1216
+ if (read.spentUsd === null && prior.spentUsd > 0) account(prior.spentUsd, false);
1217
+ else account(read.spentUsd, read.spendComplete);
1218
+
1219
+ // ── R1: the box. A green mints the next version FROM THE PLAN AS EXECUTED; anything
1220
+ // else writes a history row and leaves the recipe untouched.
1221
+ const record = {
1222
+ runid: `${runid}-t${n}`, patient, at: new Date(now()).toISOString(),
1223
+ costUsd: read.spentUsd, spendComplete: read.spendComplete, wallMs, rounds,
1224
+ ...(trySpecHash ? { specHash: trySpecHash } : {}),
1225
+ };
1226
+ if (verdictClass === 'green' && read.plan === null) {
1227
+ // a green with no plan on the spine cannot mint a version — the artifact that
1228
+ // inherits is the one that RAN, and there is nothing here to inherit. Reported,
1229
+ // never faked with a placeholder.
1230
+ bridgeWrites.push({ name, action: 'none', file: null, reds: [{ code: 'no-plan-executed', path: 'plan', detail: 'the run greened but emitted no accepted plan — nothing to mint (R1: the artifact that inherits is the one that ran)' }] });
1231
+ } else if (verdictClass === 'green') {
1232
+ const meta = {
1233
+ name: name ?? job.job,
1234
+ goal: job.goal,
1235
+ specHash: trySpecHash,
1236
+ closeStageNames: (stageClose(job.close) ?? []).map((/** @type {any} */ s) => s.name),
1237
+ toolsUsed: [...new Set((read.plan.steps ?? []).flatMap((/** @type {any} */ s) => s.tools ?? []))],
1238
+ };
1239
+ row.bridgeWrite = writeGreen(bridge, meta, { ...record, plan: read.plan });
1240
+ } else if (bridge) {
1241
+ // R1: the recipe is NOT edited by a red. `outcome` rides in verbatim — a casualty
1242
+ // keeps its own name, and only the literal 'red' demotes (D6).
1243
+ const r = appendRed(bridge, { ...record, outcome: verdictClass === 'red' ? 'red' : outcome, failingStage: read.failingStage });
1244
+ row.bridgeWrite = commit(r, name, verdictClass === 'red' ? 'appendRed' : 'appendCasualty');
1245
+ }
1246
+ // a cold RED writes nothing at all: the entry bar is a green (D6), so a failed plan
1247
+ // never enters the registry — that is also the clutter control
1248
+ emit('try-end', { ...row, plan: undefined });
1249
+ tries.push(row);
1250
+ return row;
1251
+ };
1252
+
1253
+ /** save a bridge result and record the write, whatever produced it
1254
+ * @param {{ok: boolean, reds: any[], bridge: any}} r @param {string|null} name @param {string} action
1255
+ * @param {object} [extra] fields that make the write's own story visible on the record */
1256
+ const commit = (r, name, action, extra = {}) => {
1257
+ if (!r.ok) {
1258
+ const w = { name, action: 'none', file: null, reds: r.reds, ...extra };
1259
+ bridgeWrites.push(w);
1260
+ emit('bridge-write', w);
1261
+ return w;
1262
+ }
1263
+ const s = saveBridge(registryDir, r.bridge);
1264
+ const w = { name: r.bridge.name, action: s.ok ? action : 'none', file: s.file, reds: s.reds, ...extra };
1265
+ bridgeWrites.push(w);
1266
+ emit('bridge-write', w);
1267
+ return w;
1268
+ };
1269
+
1270
+ /** a refused write, recorded exactly like a committed one so a reader never has to
1271
+ * branch on presence @param {string} name @param {any} red @param {object} [extra] */
1272
+ const refuseWrite = (name, red, extra = {}) => {
1273
+ const w = { name, action: 'none', file: null, reds: [red], ...extra };
1274
+ bridgeWrites.push(w);
1275
+ emit('bridge-write', w);
1276
+ return w;
1277
+ };
1278
+
1279
+ /**
1280
+ * KNOWN LIMIT, named rather than discovered later: `appendGreen` carries the entry's
1281
+ * LABEL (`goal`, `closeStageNames`, `toolsUsed`) forward untouched, so a new version
1282
+ * whose plan uses a verb the founding green did not is not reflected in `toolsUsed`.
1283
+ * The load gate reads that field, so the gate's verb check goes slightly stale across
1284
+ * versions. It is not a safety hole — the gate only ever gets more permissive there,
1285
+ * and the TWEAKED plan still passes the full `validatePlan` against the job's signed
1286
+ * menu at draft time, which is the check that actually decides. Fixing it properly
1287
+ * means `appendGreen` taking a label update, which is module 1's contract and hamr's
1288
+ * call, not something to change from the caller.
1289
+ * @param {any|null} bridge @param {any} meta @param {any} record
1290
+ */
1291
+ function writeGreen(bridge, meta, record) {
1292
+ // the BRIDGE leg needs no shape check: `runJob`'s own load gate already proved this
1293
+ // entry is the same kind of recipe as this job, at the door, before a token was spent
1294
+ // (a mismatch never gets here — it returns `recipe-stale`).
1295
+ if (bridge) return commit(appendGreen(bridge, record), bridge.name, 'appendGreen');
1296
+
1297
+ // the COLD leg's green. `mintBridge` is for a name nobody holds; a name that already
1298
+ // holds greens APPENDS, because a cold run of a job that already has a bridge is
1299
+ // another version of that workflow, and overwriting it would destroy the green that
1300
+ // minted it.
1301
+ //
1302
+ // But the name is the JOB's slug, and a job's CLOSE can be re-signed under that same
1303
+ // slug. Appending across two close shapes does two wrong things at once: it counts a
1304
+ // green another close rendered toward this entry's distinct-patient status (a false
1305
+ // `proven`), and it hands the load gate — which matches on exactly these stage names
1306
+ // — a plan that satisfied a different verification. So the shape is checked here with
1307
+ // the gate's own predicate, and a mismatch neither appends NOR discards the green: it
1308
+ // forks to a deterministic derived name, with the fork ON the record.
1309
+ const stageNames = Array.isArray(meta.closeStageNames) ? meta.closeStageNames : [];
1310
+ const entries = loadRegistry(registryDir).bridges;
1311
+ const existing = entries.find((/** @type {any} */ b) => b.name === meta.name);
1312
+ if (existing && sameCloseShape(existing, stageNames)) return commit(appendGreen(existing, record), meta.name, 'appendGreen');
1313
+
1314
+ const forked = existing !== undefined;
1315
+ const target = forked ? shapeForkName(meta.name, stageNames) : meta.name;
1316
+ /** the fork's own story, carried on every write record it produces */
1317
+ const mark = forked ? { shapeForked: true, forkedFrom: meta.name, closeStageNames: [...stageNames] } : {};
1318
+ const held = forked ? entries.find((/** @type {any} */ b) => b.name === target) : undefined;
1319
+ if (held) {
1320
+ // the derived name is derived FROM the shape, so a readable entry there matches by
1321
+ // construction; a mismatch would mean two different closes hashed the same, and
1322
+ // that is refused rather than appended blind
1323
+ return sameCloseShape(held, stageNames)
1324
+ ? commit(appendGreen(held, record), target, 'appendGreen-shape-forked', mark)
1325
+ : refuseWrite(target, { code: 'shape-fork-collision', path: `${target}.json`, detail: `the derived name for close [${stageNames.join(' → ')}] is already held by an entry of a different shape — refusing to append this green to a recipe another close greened` }, mark);
1326
+ }
1327
+ // A file of that name that `loadRegistry` could not READ is not an absent entry — it
1328
+ // is an entry nobody can see, and minting over it destroys whatever greens it held.
1329
+ // `loadRegistry` skips-and-reports for that reason; the mint must not undo the skip.
1330
+ if (existsSync(join(registryDir, `${target}.json`))) {
1331
+ return refuseWrite(target, { code: 'mint-collision', path: `${target}.json`, detail: 'a file of this name exists but could not be read as a bridge — refusing to mint over it, because whatever greens it holds are not recoverable once overwritten. Repair or move it, then re-run.' }, mark);
1332
+ }
1333
+ return commit(mintBridge({ ...meta, name: target }, record), target, forked ? 'mint-shape-forked' : 'mint', mark);
1334
+ }
1335
+
1336
+ /**
1337
+ * The stops no further try can change. A spec nobody signed, a spec that will not
1338
+ * validate, and a degraded primitive are properties of the RUN, not of the recipe — the
1339
+ * next try would reproduce them for $0 apiece and then the cold leg would too, filling
1340
+ * the readout with rows that all say the same thing and burying the one fact the
1341
+ * operator needs. The envelope pre-authorized SPENDING on further attempts; it did not
1342
+ * authorize repeating an answer already known.
1343
+ * @param {any} row @returns {any|null} the terminal, or null to carry on
1344
+ */
1345
+ const hardStop = (row) => {
1346
+ const decisions = {
1347
+ 'unapproved-spec': {
1348
+ decision: `No approval record matches the per-try spec (hash ${trySpecHash}). Tightening the caps with an envelope makes a NEW spec version, and a new version is signed, not inherited — nothing was run.`,
1349
+ options: [
1350
+ // the approval hash, never the per-try spec's: the number an operator TYPES
1351
+ // covers all three envelope numbers, and handing over the inner one would send
1352
+ // them to sign a version that leaves the try count unsigned
1353
+ `sign this envelope: --approve ${approvalHash} (the approval hash covers all three numbers — per-try budget, per-try wall AND ${resolved ? resolved.bridgeTries : 'the'} tries)`,
1354
+ 'use an envelope equal to the spec\'s own caps, which leaves the per-try spec hash-identical',
1355
+ ],
1356
+ },
1357
+ 'job-red': {
1358
+ decision: 'The per-try spec did not validate, so no try can run — every further attempt would reproduce this exact refusal.',
1359
+ options: ['fix the job spec', 'check the envelope: it writes budgetUsd/maxWallMs onto the spec, and both have bounds'],
1360
+ },
1361
+ 'smoke-red': {
1362
+ decision: 'The litectx primitive failed its known-answer check. No run verdict is trustworthy on a degraded primitive, and a second try would be judged by the same degraded one.',
1363
+ options: ['fix the primitive/store', 'abandon the run'],
1364
+ },
1365
+ 'close-unsupported': {
1366
+ decision: 'The plan flow cannot execute this job\'s close — it runs commands whose exit codes are truth. Nothing was judged, and no workflow can be graded by a close that will not run, so the next try and the cold leg would refuse identically for $0 apiece.',
1367
+ options: ['restate the close as a staged (or single predicate) close — a spec edit, whose new hash needs re-approval', 'wait for the verdict-classes rung'],
1368
+ },
1369
+ 'interpreter-red': {
1370
+ // NOT re-filed as an escalation. `interpreter-red` is one of the categories the
1371
+ // ledger CLASSIFIES — it aims a runtime-red at a library — and runJob already
1372
+ // emitted it at the fault's own site with its typed `lib`. A second copy from
1373
+ // here would count ONE wiring fault twice against an upstream package, which is
1374
+ // the step-stalled lesson exactly. The stop is still decision-ready in the
1375
+ // return value, and the spine still carries the fault's own escalation.
1376
+ refile: false,
1377
+ decision: 'The run stopped on a wiring/interpreter fault — the runner or a primitive it was handed is not correctly bound. That is a property of the RUN, not of any stored workflow: every further try would reproduce it, and so would the cold leg.',
1378
+ options: ['fix the wiring the escalation names (provider factory, store binding)', 'abandon the run'],
1379
+ },
1380
+ };
1381
+ const d = decisions[row.runOutcome];
1382
+ if (!d) return null;
1383
+ // the category IS the outcome here: each of these names its own story exactly. All
1384
+ // but one are in the ledger's excluded set (operator input, a degraded primitive, a
1385
+ // close the flow cannot run — never a library failing); the one that is not is the
1386
+ // one that does not re-file.
1387
+ const { refile = true, ...decision } = d;
1388
+ return done(row.runOutcome, { category: row.runOutcome, ...decision }, refile);
1389
+ };
1390
+
1391
+ /** what to do once a try has returned: a green ends the run, and the stops no further
1392
+ * try could change end it too. Shared by the loop and by a resumed RESTART, which is
1393
+ * the same event happening at a different point in the same sequence.
1394
+ * @param {any} row @returns {any|null} the terminal, or null to carry on */
1395
+ const afterTry = (row) => (row.verdictClass === 'green' ? done('green', { decision: null }) : hardStop(row));
1396
+
1397
+ // ── 3. RESUME (module C): pick up where the killed run stopped
1398
+ //
1399
+ // Three things are seeded and one is restarted. The completed tries come back as rows
1400
+ // (so the readout is the whole run, not the last leg of it) and their bridges come back
1401
+ // as SPENT (so no workflow this run already paid for is offered again); the money comes
1402
+ // back as the carried figure — deliberately WITHOUT any mid-try fold, which travels
1403
+ // into the restarted try's own ledger instead and would otherwise be counted twice.
1404
+ /** the cold leg's fold, when the kill landed inside the COLD attempt @type {any} */
1405
+ let coldRestart = null;
1406
+ let startN = 1;
1407
+ if (resume) {
1408
+ // the signature half of hamr's ruling: a resume continues ONE signed run. The
1409
+ // envelope is re-validated and re-hashed above from the CURRENT arguments, so a
1410
+ // mismatch here means the seed came from a different envelope than the one this
1411
+ // process is about to enforce — refused with both hashes named, exactly like a
1412
+ // fresh launch under a stale signature.
1413
+ // a seed with NO hash (a pre-hash-scheme spine) is a refusal, not a pass — the
1414
+ // strict comparison catches null/undefined too (tail-review F-2); signed specs are
1415
+ // never silently re-validated, and a missing signature is the least valid of all
1416
+ if (resume.approvalHash !== approvalHash) {
1417
+ return done('resume-red', {
1418
+ category: 'resume-red',
1419
+ decision: 'The run being resumed was signed under a DIFFERENT envelope than this one, so nothing was run — a resume continues one signed run, never any run.',
1420
+ options: ['resume with the envelope the dead run was signed under', 'start a fresh run under this envelope (the dead run\'s tries are then not inherited)'],
1421
+ detail: `dead run's approval hash ${resume.approvalHash}\nthis envelope's approval hash ${approvalHash}`,
1422
+ });
1423
+ }
1424
+ for (const t of Array.isArray(resume.completed) ? resume.completed : []) {
1425
+ tries.push({ ...t, inherited: true });
1426
+ if (typeof t.n === 'number' && t.n >= startN) startN = t.n + 1;
1427
+ }
1428
+ for (const name of Array.isArray(resume.tried) ? resume.tried : []) if (isNonEmptyString(name)) tried.add(name);
1429
+ // seeded through the SAME accountant the live legs use, so the floor rules are one
1430
+ // rule: a carried figure that is not a finite number, or that arrived flagged
1431
+ // incomplete, makes the resumed run's total a floor too.
1432
+ account(typeof resume.carrySpentUsd === 'number' ? resume.carrySpentUsd : null, resume.carrySpendComplete !== false);
1433
+
1434
+ const rs = isObj(resume.restart) ? /** @type {any} */ (resume.restart) : null;
1435
+ const prior = rs
1436
+ ? {
1437
+ spentUsd: typeof rs.priorSpentUsd === 'number' && Number.isFinite(rs.priorSpentUsd) && rs.priorSpentUsd > 0 ? rs.priorSpentUsd : 0,
1438
+ wallMs: typeof rs.priorWallMs === 'number' && Number.isFinite(rs.priorWallMs) && rs.priorWallMs > 0 ? rs.priorWallMs : 0,
1439
+ spendComplete: rs.priorSpendComplete !== false,
1440
+ // the ROUNDS that attempt bought — folded like the wall so the row spans the same
1441
+ // legs its money already does. `null` is the honest unknown and is NOT folded.
1442
+ rounds: typeof rs.priorRounds === 'number' && Number.isFinite(rs.priorRounds) && rs.priorRounds >= 0 ? rs.priorRounds : null,
1443
+ // the dead leg's close GRADES, carried to the restart's halt readout so it
1444
+ // judges the chain and not just its own leg (readGradeSeed). Baselines only:
1445
+ // `runJob` seeds the RUN trend with them and never the strike governor — "did
1446
+ // this run make progress" and "is this loop out of ideas" are two questions,
1447
+ // and one instrument answering both is how they come to disagree.
1448
+ grades: Array.isArray(rs.grades) ? rs.grades : [],
1449
+ }
1450
+ : NO_PRIOR;
1451
+ const remainingCapUsd = envelope.perTryBudgetUsd - prior.spentUsd;
1452
+ const remainingWallMs = envelope.perTryWallMs - prior.wallMs;
1453
+ emit('resume-start', {
1454
+ fromRunid: resume.fromRunid ?? null, approvalHash,
1455
+ triesCompleted: tries.map((t) => ({ n: t.n, mode: t.mode, bridge: t.bridge, runOutcome: t.runOutcome })),
1456
+ tried: [...tried],
1457
+ carriedUsd: resume.carrySpentUsd ?? null, carriedComplete: resume.carrySpendComplete !== false,
1458
+ r1Missing: resume.r1Missing === true,
1459
+ // `gradesInherited` is a COUNT, and only when there is one to state: the seam
1460
+ // carries stage names and numbers, never a close byte — the spine is append-only,
1461
+ // so a gap that crosses here crosses for good.
1462
+ restart: rs ? { n: rs.n, mode: rs.mode, bridge: rs.bridge ?? null, priorSpentUsd: prior.spentUsd, priorWallMs: prior.wallMs, priorSpendComplete: prior.spendComplete, remainingCapUsd, remainingWallMs, resumedAt: rs.seed ? { phase: rs.seed.phase, stepsDone: rs.seed.completedSteps.length, stepsPlanned: rs.seed.plan?.steps?.length ?? null } : null, ...(prior.grades.length ? { gradesInherited: prior.grades.length } : {}) } : null,
1463
+ nextTry: rs ? null : startN,
1464
+ });
1465
+
1466
+ if (rs) {
1467
+ // "if mid loop, restart that loop" — under the REMAINDER, and only if the
1468
+ // remainder can actually fund an attempt PLUS its close. It cannot be topped up
1469
+ // from here: raising a per-try number is a spec edit whose new hash the operator
1470
+ // signs (D7), so an unfundable restart STOPS, having launched nothing. The stop IS
1471
+ // the checkpoint — everything already earned is on the spine and in the box.
1472
+ const unfundable = remainingCapUsd <= 0
1473
+ ? {
1474
+ outcome: 'cap-halt',
1475
+ decision: `Try ${rs.n} was killed with ${prior.spendComplete ? '' : 'at least '}$${prior.spentUsd.toFixed(4)} of its $${envelope.perTryBudgetUsd} already spent, so no money remains to restart it. Prior spend folds into the ceiling — a kill can never buy a fresh allotment — so the run stops here with everything earned so far kept.`,
1476
+ detail: `remaining for try ${rs.n}: $${remainingCapUsd.toFixed(4)} of $${envelope.perTryBudgetUsd}`,
1477
+ }
1478
+ : remainingWallMs < MIN_WALL_MS
1479
+ ? {
1480
+ outcome: 'wall-halt',
1481
+ decision: `Try ${rs.n} was killed with ${(prior.wallMs / 60000).toFixed(1)}min of its ${(envelope.perTryWallMs / 60000).toFixed(1)}min already spent, leaving less than one close timeout. A try that cannot fund its own close produces an unreadable row, and an unreadable row is a casualty rather than evidence (F45), so nothing was launched.`,
1482
+ detail: `remaining for try ${rs.n}: ${remainingWallMs}ms, below the ${MIN_WALL_MS}ms floor (one close timeout)`,
1483
+ }
1484
+ : null;
1485
+ if (unfundable) {
1486
+ return done(unfundable.outcome, {
1487
+ category: unfundable.outcome,
1488
+ decision: unfundable.decision,
1489
+ detail: unfundable.detail,
1490
+ options: [
1491
+ 'raise the envelope (a bigger per-try budget/wall) and resume again — that is a NEW envelope, so it needs a new signature (--approve the printed hash)',
1492
+ 'revise the goal or the close — a spec edit, whose new hash needs re-approval',
1493
+ 'abandon the run',
1494
+ ],
1495
+ });
1496
+ }
1497
+ if (rs.mode === 'cold') {
1498
+ // every bridge try was already spent before the cold leg started: the loop below
1499
+ // has nothing left to authorize, and the restart IS the cold attempt
1500
+ coldRestart = { n: typeof rs.n === 'number' ? rs.n : tries.length + 1, prior, seed: rs.seed ?? null };
1501
+ startN = resolved.bridgeTries + 1;
1502
+ } else {
1503
+ const entry = loadRegistry(registryDir).bridges.find((/** @type {any} */ b) => b.name === rs.bridge);
1504
+ if (!entry) {
1505
+ // the pick was the operator's and the model's, made and paid for. Substituting
1506
+ // another workflow — or falling to cold — would be the runner overriding a
1507
+ // decision it did not make (D3's rule, at the resume seam).
1508
+ return done('resume-red', {
1509
+ category: 'resume-red',
1510
+ decision: `The workflow the killed run was part-way through (${JSON.stringify(rs.bridge)}) is no longer in the registry, so try ${rs.n} cannot be restarted and nothing was run. Which workflow this run continues from is not the runner's to change.`,
1511
+ options: [`restore ${JSON.stringify(rs.bridge)} to the registry and resume again`, 'start a fresh run (the completed tries are then not inherited)'],
1512
+ detail: `registry ${registryDir}`,
1513
+ });
1514
+ }
1515
+ tried.add(entry.name);
1516
+ const stop = afterTry(await runTry(entry, rs.n, prior, rs.seed ?? null));
1517
+ if (stop) return stop;
1518
+ startN = rs.n + 1;
1519
+ }
1520
+ }
1521
+ // tail-review F-1: a dead run that had already RUN its cold leg left the bridge loop
1522
+ // by DECISION (the model said none-matches), not by count — re-deriving startN from
1523
+ // try numbers alone re-enters the loop and buys a paid bridge try the signed preview
1524
+ // said would not happen, running it AFTER the cold leg (an order no fresh run can
1525
+ // produce). One predicate, mirroring the runner's own coldAlreadyRun arithmetic.
1526
+ if (!coldRestart && tries.some((t) => t.mode === 'cold' && t.inherited)) startN = resolved.bridgeTries + 1;
1527
+ }
1528
+
1529
+ // ── 4. the tries
1530
+ // the bound is read off the SIGNED artifact, not the raw envelope: the count that
1531
+ // authorizes spend here is the count the operator's signature covers
1532
+ for (let n = startN; n <= resolved.bridgeTries; n += 1) {
1533
+ // reloaded per try: the row the LAST try just wrote is part of what the next pick
1534
+ // reads, and a registry read once at the top would hand every later selection a
1535
+ // history that stopped at the start of the run
1536
+ const registry = loadRegistry(registryDir);
1537
+ // The pin travels only until it has had its try. Once its name is in `tried` it is
1538
+ // EXCLUDED from the listing, so restating it as the pin makes every answer the model
1539
+ // can legally give a refusal (D3 refuses anything that is not the pin) — and the run
1540
+ // would end "nothing was run" with a paid try behind it and tries still authorized.
1541
+ // A spent pin is a decision already honoured, not a standing instruction.
1542
+ const pinNow = pinned && !tried.has(pinned) ? pinned : null;
1543
+ const sel = await selectBridge({
1544
+ registry, job, ask: askText, provider: selectionProvider ?? provider,
1545
+ pinned: pinNow, shortlist, forceCold, exclude: [...tried],
1546
+ });
1547
+ selection.push({ n, ...sel, listing: undefined });
1548
+ account(sel.costUsd, sel.spendComplete);
1549
+ emit('selection-result', { n, choice: sel.choice, reason: sel.reason, called: sel.called, refused: sel.refused, costUsd: sel.costUsd, candidates: sel.candidates });
1550
+
1551
+ if (sel.refused) {
1552
+ return done('selection-refused', {
1553
+ decision: `The workflow you pinned (${JSON.stringify(pinned)}) was refused: ${sel.reason}. Nothing was run — which workflow this job starts from is your call, not the runner's.`,
1554
+ options: [`run it anyway by re-pinning ${JSON.stringify(pinned)} after reading the reason`, 'pin a different workflow', 'run cold (--force-cold)'],
1555
+ });
1556
+ }
1557
+ if (sel.red) {
1558
+ return done('selection-red', {
1559
+ reds: [sel.red],
1560
+ decision: `The selection call did not produce a usable answer (${sel.red.code}), so no workflow was chosen and no run was started. Falling back to a cold draft would be spending a run's budget on a decision nobody made.`,
1561
+ options: ['rerun the selection', 'pin a workflow explicitly', 'run cold (--force-cold)'],
1562
+ detail: sel.red.detail,
1563
+ });
1564
+ }
1565
+ if (sel.choice === null) break; // "none matches" / force-cold / nothing to offer → cold
1566
+
1567
+ const entry = loadRegistry(registryDir).bridges.find((/** @type {any} */ b) => b.name === sel.choice);
1568
+ if (!entry) break; // the listing named it and the directory no longer holds it — cold
1569
+ tried.add(sel.choice);
1570
+ const stop = afterTry(await runTry(entry, n));
1571
+ if (stop) return stop;
1572
+ }
1573
+
1574
+ // ── 5. "then start anew" — the cold leg, under the SAME per-try numbers (or, when the
1575
+ // kill landed inside the cold attempt itself, under what is LEFT of them)
1576
+ //
1577
+ // A resumed run can arrive here with the cold leg ALREADY RUN: the killed process
1578
+ // finished it and died before its own terminal (a narrow window, but a real one — and
1579
+ // the one where a second draft would cost a whole extra attempt the envelope never
1580
+ // authorized). The inherited row IS the cold leg; nothing is re-run, and the run keeps
1581
+ // the verdict that attempt actually earned.
1582
+ const inheritedCold = coldRestart ? null : tries.find((t) => t.mode === 'cold' && t.inherited) ?? null;
1583
+ const cold = coldRestart
1584
+ ? await runTry(null, coldRestart.n, coldRestart.prior, coldRestart.seed)
1585
+ : inheritedCold ?? await runTry(null, tries.length + 1);
1586
+ const coldStop = afterTry(cold);
1587
+ if (coldStop) return coldStop;
1588
+ return done(cold.runOutcome, {
1589
+ category: 'reuse-exhausted',
1590
+ decision: `Every authorized attempt is spent: ${tries.filter((t) => t.mode === 'bridge').length} workflow ${tries.filter((t) => t.mode === 'bridge').length === 1 ? 'try' : 'tries'} of ${resolved.bridgeTries}, then a cold draft, and the job's own verification is still not satisfied. The envelope pre-authorized exactly this much and no more.`,
1591
+ options: [
1592
+ 'raise the envelope (more tries, or a bigger per-try budget/wall) and rerun — resume-to-cap, the stop IS the checkpoint',
1593
+ 'revise the goal or the close — a spec edit, whose new hash needs re-approval',
1594
+ 'abandon the run',
1595
+ ],
1596
+ detail: tries.map((t) => `try ${t.n} (${t.mode}${t.bridge ? ` "${t.bridge}"` : ''}): ${t.runOutcome}`
1597
+ + `${t.failingStage ? ` at stage "${t.failingStage}"` : ''}`
1598
+ + ` — ${t.spentUsd === null ? 'spend UNKNOWN' : `${t.spendComplete ? '' : '≥'}$${t.spentUsd.toFixed(4)}`} of $${t.capUsd},`
1599
+ + ` ${typeof t.wallMs === 'number' && Number.isFinite(t.wallMs) ? `${(t.wallMs / 60000).toFixed(1)}min of ${(t.wallCapMs / 60000).toFixed(1)}min` : 'wall UNKNOWN'},`
1600
+ + ` close ${t.closeReached ? 'reached' : 'NEVER REACHED'}`).join('\n'),
1601
+ });
1602
+ }