bareloop 0.27.0 → 0.29.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.
@@ -0,0 +1,451 @@
1
+ // The U runner's end-of-run readout, in the one place a test can reach it.
2
+ //
3
+ // `run-u.mjs` is a script: importing it runs it. So the one line of it whose
4
+ // arithmetic was WRONG lives here instead, and the runner calls it.
5
+ //
6
+ // F83, the small unfixed item: the banner printed money FOLDED (`job-end`'s total
7
+ // across every leg of a resume) and wall LEG-ONLY (`Date.now() - started` of this
8
+ // process), both against the SAME signed cap, and said which for neither. On
9
+ // u-msf70nei that read `wall 12.8min of 45min` for a leg that resumed onto 24.9
10
+ // already-burnt minutes. Nothing was mis-ENFORCED — `createClock` folds
11
+ // `priorElapsedMs` and the outside watchdog armed on the 20.1min remainder — the
12
+ // readout simply computed the wall a second way, which is the two-transforms class
13
+ // (F9) the clock's own `enforcedMs` comment exists to refuse.
14
+ //
15
+ // Both figures stay on the line. The folded total is the one the cap governs, so it
16
+ // leads; the leg is what this process actually bought, and dropping it would trade
17
+ // one blindness for the other.
18
+
19
+ /** minutes, at the precision the readout has ever claimed (~90%, hamr's ruling) */
20
+ const min = (/** @type {number} */ ms) => (ms / 60_000).toFixed(1);
21
+
22
+ /**
23
+ * A token count, k/M-abbreviated — 2 decimals at and above a million (the tail's own
24
+ * `.2M`-shaped resolution matters more once six figures are folded into a suffix), 1
25
+ * decimal in the thousands, and the bare integer under that (a two- or three-digit
26
+ * round is more readable plain than as `"0.2k"`).
27
+ * @param {number} n
28
+ * @returns {string}
29
+ */
30
+ export function fmtTokens(n) {
31
+ if (!Number.isFinite(n)) return '?';
32
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`;
33
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
34
+ return String(Math.round(n));
35
+ }
36
+
37
+ /**
38
+ * THE TOKENS TAIL LINE — hamr's 2026-09-01 order: `job-end` carries dollars only, and
39
+ * the run tail never prints a token total anywhere, though tokens exist per round on
40
+ * `worker-round.usage` and `judge-round.usage`
41
+ * ({inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens}).
42
+ *
43
+ * Summed off THIS LEG's own spine, over every round TYPE that spends money —
44
+ * worker-round AND judge-round (house rule: a spend-slicing instrument must enumerate
45
+ * every round type that spends, or it silently undercounts). `worker-result` is an
46
+ * ATTEMPT-LEVEL ECHO of round sums, not a round, and is deliberately never matched
47
+ * here — folding it in would double the real figure.
48
+ *
49
+ * A round whose `usage` is missing or not an object (today: every `judge-round` — the
50
+ * payload at `src/planrun.js`'s `onJudgeCost` does not carry one) is counted as
51
+ * UNKNOWN, never laundered into a silent 0 — the same rule `costUsd` already lives by
52
+ * (F6/F44): an unpriced round is reported as unpriced, not folded into the total as
53
+ * if it cost nothing.
54
+ *
55
+ * @param {{events: any[]}} o `events` this leg's own parsed spine, never a folded
56
+ * chain — a resumed leg's tokens are this leg's own bill, same as `rounds` above it.
57
+ * @returns {string}
58
+ */
59
+ export function tokensLine({ events }) {
60
+ const list = Array.isArray(events) ? events : [];
61
+ const num = (/** @type {unknown} */ v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
62
+ let inputTokens = 0;
63
+ let outputTokens = 0;
64
+ let cacheReadTokens = 0;
65
+ let cacheCreationTokens = 0;
66
+ let unpriced = 0;
67
+ for (const e of list) {
68
+ if (!e || typeof e !== 'object') continue;
69
+ if (e.type !== 'worker-round' && e.type !== 'judge-round') continue;
70
+ const u = e.usage;
71
+ if (!u || typeof u !== 'object') { unpriced += 1; continue; }
72
+ inputTokens += num(u.inputTokens);
73
+ outputTokens += num(u.outputTokens);
74
+ cacheReadTokens += num(u.cacheReadTokens);
75
+ cacheCreationTokens += num(u.cacheCreationTokens);
76
+ }
77
+ const total = inputTokens + outputTokens + cacheReadTokens + cacheCreationTokens;
78
+ const note = unpriced > 0 ? ` (${unpriced} round${unpriced === 1 ? '' : 's'} unpriced/no usage)` : '';
79
+ return `${fmtTokens(total)} · in ${fmtTokens(inputTokens)} · out ${fmtTokens(outputTokens)} · `
80
+ + `cache-read ${fmtTokens(cacheReadTokens)} · cache-write ${fmtTokens(cacheCreationTokens)}${note}`;
81
+ }
82
+
83
+ /**
84
+ * The banner's wall line: what this run has consumed of its signed cap.
85
+ * @param {{legMs: number, priorWallMs?: number, wallLabel: string}} o
86
+ * `legMs`: wall this process bought. `priorWallMs`: wall its predecessors burnt,
87
+ * as `readResume` folded it (0/absent on a cold run). `wallLabel`: the signed cap
88
+ * as the runner spells it, including the honest UNBOUNDED prose.
89
+ * @returns {string}
90
+ */
91
+ export function wallLine({ legMs, priorWallMs = 0, wallLabel }) {
92
+ // the belt every sibling fold site carries (src/clock.js's `prior`, readResume's
93
+ // four): a non-finite or negative fold contributes 0. A NaN here would render
94
+ // `NaNmin` and a negative would report LESS wall than the leg demonstrably took.
95
+ const prior = typeof priorWallMs === 'number' && Number.isFinite(priorWallMs) && priorWallMs > 0 ? priorWallMs : 0;
96
+ // a cold run has no fold and gets exactly the line it always got
97
+ if (prior === 0) return `${min(legMs)}min of ${wallLabel}`;
98
+ return `${min(legMs + prior)}min of ${wallLabel} (this leg ${min(legMs)}min)`;
99
+ }
100
+
101
+ /**
102
+ * THE EVIDENCE PACKAGE (N4, ruling 2) — what a person is shown before they answer.
103
+ *
104
+ * *"Before/after changes plus every mechanical stage's result; never a bare
105
+ * 'approve?'"*. The LIBRARY assembles the facts and puts them on the `hitl-pause`
106
+ * spine record — the ask the close itself declared, every stage's own result, and
107
+ * WHICH files the run changed (`src/planrun.js`'s `emitHitlPause`). This renders
108
+ * them, and adds the one thing a library cannot state honestly from where it sits:
109
+ * the LINE-level diff, which needs a screen and a repository in front of it.
110
+ *
111
+ * Nothing here derives a verdict, and nothing here decides. It is a readout of a
112
+ * record that already exists — so an absence is written as prose (F6: an empty
113
+ * stage list is "the checkpoint carries none", never a blank screen that reads as
114
+ * "nothing happened") and a trim is announced (F28).
115
+ *
116
+ * @param {{pause: any, diff: {lines: string[], truncated?: number, untracked?: string[],
117
+ * unavailable?: string|null}|null}} o `pause` the spine record; `diff` what the
118
+ * runner read off the patient, or null when it did not look.
119
+ * @returns {string[]} lines, ready to print
120
+ */
121
+ export function evidencePackage({ pause, diff }) {
122
+ const p = pause ?? {};
123
+ /** @type {string[]} */
124
+ const out = [];
125
+ out.push('HUMAN REVIEW — this run reached the one stage a machine cannot render, and it is waiting on you.');
126
+ out.push(` ask ${typeof p.ask === 'string' && p.ask.trim() ? p.ask : '(the close declared no question — review the result as it stands)'}`);
127
+
128
+ const stages = Array.isArray(p.stages) ? p.stages : [];
129
+ if (!stages.length) {
130
+ out.push(' stages no stage results on the checkpoint — nothing here can say what the machine judged, so read the diff below and the run\'s own spine');
131
+ } else {
132
+ out.push(' stages what the machine judged before it stopped, in the order it judged it:');
133
+ const w = Math.max(...stages.map((s) => String(s?.name ?? '?').length));
134
+ for (const s of stages) {
135
+ const name = String(s?.name ?? '?').padEnd(w);
136
+ const verdict = String(s?.verdict ?? 'unknown').padEnd(12);
137
+ // the MEASUREMENT, where a stage made one: `baseline → value` is the
138
+ // before/after a number can state, and it is the half of "before/after" a
139
+ // diff cannot show
140
+ const measured = typeof s?.value === 'number'
141
+ ? (typeof s?.baseline === 'number' ? ` ${s.baseline} → ${s.value}` : ` ${s.value}`)
142
+ : '';
143
+ // and whatever the stage ANNOUNCED about its own reading (a trim, a scope)
144
+ const notes = typeof s?.notes === 'string' && s.notes.trim() ? ` (${s.notes.trim()})` : '';
145
+ const waiting = s?.verdict === 'human-pause' ? ' ← waiting on you' : '';
146
+ out.push(` ${name} ${verdict}${measured}${notes}${waiting}`.trimEnd());
147
+ }
148
+ }
149
+
150
+ const changed = p.changed ?? {};
151
+ const paths = Array.isArray(changed.paths) ? changed.paths : [];
152
+ if (typeof changed.unreadable === 'string' && changed.unreadable.trim()) {
153
+ // F6 — an unreadable set is UNKNOWN, and it must never render as a clean tree
154
+ out.push(` changed UNKNOWN — the run could not read its own changed set: ${changed.unreadable.trim()}`);
155
+ } else if (!paths.length) {
156
+ out.push(' changed no changed files recorded on the checkpoint');
157
+ } else {
158
+ const more = typeof changed.more === 'number' && changed.more > 0 ? `, and ${changed.more} more not listed` : '';
159
+ out.push(` changed ${paths.length} file(s) this run touched${more}:`);
160
+ for (const f of paths) out.push(` ${f}`);
161
+ }
162
+
163
+ if (diff) {
164
+ if (typeof diff.unavailable === 'string' && diff.unavailable) {
165
+ out.push(` diff line-level diff UNAVAILABLE — ${diff.unavailable}`);
166
+ } else {
167
+ const lines = Array.isArray(diff.lines) ? diff.lines : [];
168
+ const untracked = Array.isArray(diff.untracked) ? diff.untracked : [];
169
+ out.push(' diff the patient AS IT STANDS NOW, against the seed (it can move while a run is paused — that is why accept re-runs the mechanical stages):');
170
+ for (const l of lines) out.push(` ${l}`);
171
+ if (typeof diff.truncated === 'number' && diff.truncated > 0) {
172
+ out.push(` … ${diff.truncated} more diff line(s) not shown — read the patient for the rest`);
173
+ }
174
+ if (untracked.length) {
175
+ out.push(` NEW file(s), which have no diff at all: ${untracked.join(', ')}`);
176
+ }
177
+ if (!lines.length && !untracked.length) out.push(' (the tree matches the seed — nothing to show)');
178
+ }
179
+ }
180
+ return out;
181
+ }
182
+
183
+ /**
184
+ * THE REVIEW DOOR's package (softgreen module 8) — the same evidence, at the door
185
+ * at the END of a run rather than at a stage inside the close.
186
+ *
187
+ * It is a SEPARATE renderer from `evidencePackage` and deliberately so: that one
188
+ * opens with *"this run reached the one stage a machine cannot render, and it is
189
+ * waiting on you"*, which at a review door would be false twice over — the machine
190
+ * rendered every stage, and the run is over. What a person needs told here is the
191
+ * opposite fact: a verdict was minted, it STANDS, and what happens to the work is
192
+ * theirs. The stage/changed/diff body is shared, because it is the same evidence.
193
+ * @param {{door: any, diff?: any}} o
194
+ * @returns {string[]}
195
+ */
196
+ export function reviewDoorPackage({ door, diff }) {
197
+ const d = door ?? {};
198
+ const held = d.quarantined === true;
199
+ /** @type {string[]} */
200
+ const out = [];
201
+ out.push(`REVIEW DOOR — this run's close minted ${String(d.outcome ?? 'a verdict')} and it STANDS. Nothing you do here changes it.`);
202
+ if (held) {
203
+ out.push(' held this green was rendered by the judged floor, so it has earned NO reuse and NO learning credit until you accept it');
204
+ }
205
+ // the body: what the machine judged, what moved, and the lines themselves —
206
+ // rendered by the SAME code the pause screen uses, off the same record shape.
207
+ // The ASK line is dropped when there is no ask: a door asks nothing of its own
208
+ // (it offers three answers to a verdict already minted), and the pause renderer's
209
+ // stand-in sentence would be inventing a question nobody declared. Keyed on the
210
+ // gutter rather than on that sentence's wording, which is not this file's to pin.
211
+ const hasAsk = typeof d.ask === 'string' && d.ask.trim() !== '';
212
+ out.push(...evidencePackage({ pause: { ...d, ask: d.ask ?? null }, diff }).slice(1)
213
+ .filter((l) => hasAsk || !l.startsWith(' ask ')));
214
+ return out;
215
+ }
216
+
217
+ /**
218
+ * THE DOOR LIST ITSELF — the header, the order, and the gutter, spelled ONCE.
219
+ *
220
+ * Both screens that offer the three doors (the step-level pause and the review
221
+ * door at the end of a run) show the same list in the same order for the same
222
+ * reason; only the CONSEQUENCES differ, so only the prose is passed in. Spelling
223
+ * the structure twice is how the two screens come to disagree about which door
224
+ * leads — and the order is the whole design input (see `doorLines`).
225
+ * @param {{rerun: string, accept: string, pause: string}} prose what each door
226
+ * costs on THIS screen, without its trailing colon
227
+ * @param {{rerun: string, accept: string, pause: string}} cmd the exact command
228
+ * each door costs, so nobody has to reconstruct one
229
+ * @returns {string[]}
230
+ */
231
+ function threeDoors(prose, cmd) {
232
+ return [
233
+ 'THREE DOORS — no fourth, and no decision is taken for you: nothing happens without the flag.',
234
+ ` rerun ${prose.rerun}:\n ${cmd.rerun}`,
235
+ ` accept ${prose.accept}:\n ${cmd.accept}`,
236
+ ` pause ${prose.pause}:\n ${cmd.pause}`,
237
+ ];
238
+ }
239
+
240
+ /**
241
+ * The door's OWN `close-timing-red` refusal (F137, hamr's ruling 2026-09-07,
242
+ * `docs/product/CLOSE-INTEGRITY-BUILD.md` "Ruled 2026-09-07"): when the
243
+ * door's timing pass (`resolveCloseTimeoutMs`) times out, the accept must
244
+ * REFUSE outright rather than fall through to the library's 120s default —
245
+ * mirroring the wording/options list `scripts/run-u.mjs`'s in-run
246
+ * `close-timing-red` escalation already prints (~line 1173), never a second
247
+ * invented phrasing. Pure so the wording is unit-testable without spawning
248
+ * the script or waiting out the real 600s timing-preflight ceiling.
249
+ * @param {{names: string}} args comma-joined names of the stage(s) that timed out
250
+ * @returns {string[]}
251
+ */
252
+ export function doorTimingRedLines({ names }) {
253
+ return [
254
+ `CLOSE-TIMING-RED (door) — stage(s) never finished the timing preflight: ${names}. `
255
+ + 'A close stage did not finish within the timing preflight\'s own ceiling — the close cannot run on this '
256
+ + 'machine in a boundable time, so nothing was run, nothing was recorded, and nothing was spent.',
257
+ ' investigate why the stage hangs (infra/network/resource issue)',
258
+ ' sign an explicit closeTimeoutMs override once the real duration is known',
259
+ ' abandon the task',
260
+ ];
261
+ }
262
+
263
+ /**
264
+ * The three doors AT THE REVIEW DOOR. Same order and the same rule as
265
+ * `doorLines` (rerun leads, ~40% rubber-stamp), different consequences: this run
266
+ * is OVER, so a rerun is a fresh engagement rather than a continuation, and a
267
+ * pause keeps the door rather than a step checkpoint.
268
+ * @param {{rerun: string, accept: string, pause: string, ttlDays: number, held: boolean}} o
269
+ * @returns {string[]}
270
+ */
271
+ export function runDoorLines({ rerun, accept, pause, ttlDays, held }) {
272
+ return threeDoors({
273
+ rerun: '"I don\'t like it, go again" — your words become the requirement a FRESH engagement plans against (its own clock, what is left of the signed budget)',
274
+ accept: `${held ? 'releases this run\'s learning credit' : 'confirms it'} — and the close\'s MECHANICAL stages re-run first, because the tree can move after a run ends`,
275
+ pause: `not now. Nothing runs and nothing is spent; the door keeps for ${ttlDays} days and then expires on its own, which is all that "cancel" ever meant`,
276
+ }, { rerun, accept, pause });
277
+ }
278
+
279
+ /**
280
+ * THE THREE DOORS, rendered — and the 2026-08-12 §4 rule is the ORDER.
281
+ *
282
+ * ~40% of human reviewers approve without reading. That datum is a design input:
283
+ * the default must lean toward rerun and never toward accept, because a default
284
+ * that costs one extra cycle when wrong is cheap and a default that mints a green
285
+ * nobody read is the failure mode the whole class exists to prevent.
286
+ *
287
+ * "Default" here is which door the prompt LEADS with — never an action taken for
288
+ * the operator. No door happens without its flag, and this says so.
289
+ *
290
+ * The third door is PAUSE (2026-08-18), not cancel. hamr: *"what's the point of
291
+ * cancel anyways? pause can resume — that would be more honest"*. A person who does
292
+ * not want to carry on today is not making a permanent judgment, and the door should
293
+ * not ask them to: the checkpoint keeps, and an unanswered one expires on its own.
294
+ *
295
+ * @param {{rerun: string, accept: string, pause: string}} invocations the exact
296
+ * command each door costs, so nobody has to reconstruct one
297
+ * @returns {string[]}
298
+ */
299
+ export function doorLines({ rerun, accept, pause }) {
300
+ return threeDoors({
301
+ rerun: 'your words BECOME the gap the worker fixes from, and the run continues under what is left of the budget',
302
+ accept: 'green — minted on FRESH evidence (the mechanical stages re-run first, since the tree may have moved)',
303
+ pause: 'not now. Nothing is run and nothing is spent; the checkpoint keeps and this run resumes from the start of its last step whenever you come back',
304
+ }, { rerun, accept, pause });
305
+ }
306
+
307
+ /**
308
+ * WHEN THE DEAD LEG STOPPED — the input `readResume` folds into `priorWallMs`, and
309
+ * therefore what the resumed leg's wall remainder is computed from.
310
+ *
311
+ * The runner has two candidate witnesses and they answer different questions:
312
+ *
313
+ * - the WATCHDOG's kill record (`<spine>.watchdog.json`), which is written when the
314
+ * outside guard reaps a run. For a run that was killed it is later and better
315
+ * evidence than the last spine event: the process was alive, silently, right up to
316
+ * the signal, and billing only to its last emitted event would under-report the
317
+ * wall it really burnt.
318
+ * - the SPINE's own landed terminal (`job-end`). A run that wrote one did not die —
319
+ * it ENDED ITSELF and dated its own stop.
320
+ *
321
+ * So the preference order is not "the later record wins", it is "the run's own record
322
+ * wins when there is one". N4 §1.6 is where that distinction stopped being academic:
323
+ * a `hitl-pause` is a clean terminal a person may answer days later, and any record
324
+ * dated after it bills their deciding time to the run's wall. The POC measured the
325
+ * shape — a report 45 days after the pause made `RESUME_WALL_MS` exactly 0, dooming
326
+ * the resumed leg before it opened, on a run that had barely started. W-2 holds by
327
+ * construction only if this reads the pause.
328
+ *
329
+ * Returning `null` is not a failure: it hands `readResume` back its own documented
330
+ * default (the last event's timestamp), which for a clean terminal IS the terminal.
331
+ *
332
+ * @param {{watchdogAt?: unknown, events?: unknown}} o `watchdogAt` as the report
333
+ * spells it (an ISO string, or anything at all — the file is read off disk);
334
+ * `events` the dead spine, parsed.
335
+ * @returns {number|null} ms, or null to leave the default in place
336
+ */
337
+ export function deathAtOf({ watchdogAt, events } = {}) {
338
+ const list = Array.isArray(events) ? events : [];
339
+ // a landed terminal is the run dating its own stop — nothing outside it is better
340
+ // evidence about a run that was never killed
341
+ if (list.some((e) => e !== null && typeof e === 'object' && /** @type {any} */ (e).type === 'job-end')) return null;
342
+ const at = typeof watchdogAt === 'string' ? Date.parse(watchdogAt) : NaN;
343
+ // an unreadable stamp is UNKNOWN, never a NaN handed to a fold (F6): the caller's
344
+ // own default is the honest fallback
345
+ return Number.isFinite(at) ? at : null;
346
+ }
347
+
348
+ /**
349
+ * WHERE A RESUME PICKS UP — the preview's `at` line, which is what says how much of
350
+ * the signed dollars this leg can still spend.
351
+ *
352
+ * `readStepCheckpoint` (src/reuse.js) knows two phases and a null: `steps` (re-enter
353
+ * the plan at the first unfinished step), `close` (every step landed; the close and
354
+ * its fix loop are what is left), and no checkpoint at all (the halt landed before a
355
+ * plan was accepted). The runner rendered those three directly, and a PAUSE fits none
356
+ * of them cleanly:
357
+ *
358
+ * - a pause happens AFTER the plan's steps, at the close's human stage. On the
359
+ * fixture a person actually meets, the checkpoint reads `phase:'steps'` with every
360
+ * step green (the pause site emits no `outer-close` of its own), so the step
361
+ * arithmetic ran off the end and printed `at step 2 of 1 "(unknown)"` — a step
362
+ * count larger than the plan and an id nobody drafted. hamr found it on the first
363
+ * resume preview he read;
364
+ * - and the same walk-off is reachable one terminal over, with no person involved:
365
+ * a kill between the last `step-end` and the `outer-close` leaves exactly that
366
+ * shape, and read `step 3 of 2`.
367
+ *
368
+ * So the count is never allowed to exceed the plan, and the PHASE is said in words.
369
+ * Rendering only; it decides nothing and bounds nothing.
370
+ *
371
+ * @param {{seed: {phase?: string, plan?: any, completedSteps?: any[]}|null|undefined,
372
+ * paused: boolean}} o `seed` is `readResume`'s `restart.seed`; `paused` is the
373
+ * recorded terminal being `hitl-pause` — read off the spine, never off a flag.
374
+ * @returns {string[]} lines, ready to print
375
+ */
376
+ export function resumeAtLines({ seed, paused }) {
377
+ const at = (/** @type {string} */ s) => ` at ${s}`;
378
+ const reloaded = ' the plan is reloaded from that run\'s own spine: no re-scout, no re-draft';
379
+ if (!seed) {
380
+ // the pause has its OWN cold shape (src/planrun.js's close precheck: every
381
+ // mechanical stage already passed on the untouched tree, so the run asked its
382
+ // person before drafting anything). "the beginning" is where a resume of it
383
+ // starts and is not the phase it stopped in — both facts, in that order.
384
+ if (paused) {
385
+ return [at('the human review — no plan was ever accepted: this run reached its human stage at the close PRECHECK, '
386
+ + 'before it drafted anything'),
387
+ ' an accept re-runs the mechanical stages for no tokens; a rerun starts the plan from the beginning'];
388
+ }
389
+ return [at('the beginning — it halted before a plan was accepted, so nothing paid is re-payable')];
390
+ }
391
+ const total = Array.isArray(seed.plan?.steps) ? seed.plan.steps.length : 0;
392
+ const done = Array.isArray(seed.completedSteps) ? seed.completedSteps.length : 0;
393
+ if (paused) {
394
+ return [at(`the human review — all ${total} step(s) finished and are SKIPPED; the close reached its human stage, `
395
+ + 'and it re-runs for no tokens (what your answer costs after that is the door you pick)'), reloaded];
396
+ }
397
+ if (seed.phase === 'close') {
398
+ return [at(`the close and its fix loop — all ${total} step(s) are done and are SKIPPED; the close re-runs for no tokens`), reloaded];
399
+ }
400
+ // the plan is EXHAUSTED but no close was recorded: the halt landed between the last
401
+ // step and the close's first reading. There is no next step to name, and inventing
402
+ // one is what produced `step 3 of 2 "(unknown)"`.
403
+ if (done >= total) {
404
+ return [at(`the close — all ${total} step(s) finished and are SKIPPED, and the run stopped before the close rendered; `
405
+ + 'it runs next, for no tokens'), reloaded];
406
+ }
407
+ return [at(`step ${done + 1} of ${total} ${JSON.stringify(seed.plan.steps[done]?.id ?? '(unknown)')} — ${done} already finished and SKIPPED, not re-paid`), reloaded];
408
+ }
409
+
410
+ /**
411
+ * F97 — the DOOMED RESUME read, the one the operator should make before signing.
412
+ *
413
+ * `u-msn0uccv` spent $0.82 re-entering the exact plan whose action was the diagnosed
414
+ * defect, carrying `priorReplans: 2` and `priorReplanGrantUsed: true`. A resume does
415
+ * not re-draft — `scout-skipped {reason:"resumed"}` and the plan comes back
416
+ * byte-for-byte — so with the replan ledger empty there was no mechanism left that
417
+ * could replace it. Two attempts, 26 rounds, 0 writes, `step-red` with money and
418
+ * minutes still on the clock. Nothing in the library was at fault; every part of that
419
+ * leg reported honestly. The DECISION to fire was the defect, and the read that would
420
+ * have refused it costs $0: both facts are already parsed by the preview gate.
421
+ *
422
+ * WARNING ONLY. This never blocks and never changes behaviour — a resume of a
423
+ * mechanical gap (a named wall, a count) converts on re-entry all the time, and the
424
+ * operator is the one who can tell which kind of gap it is. Blocking here would be
425
+ * this file inventing a gate; the arbiter's gates are signed, not derived from a
426
+ * readout module.
427
+ *
428
+ * @param {{seed?: {phase?: string}|null, replans?: number, replanGrantUsed?: boolean}|null} [restart]
429
+ * `readResume`'s `restart` fold: WHERE it picks up and WHAT LEDGER it inherited.
430
+ * @returns {boolean} true only for the shape F97 measured.
431
+ */
432
+ export function doomedResume(restart) {
433
+ // WHERE. `phase: 'steps'` is the re-entry that reloads the accepted plan and runs
434
+ // it again. `'close'` is not this shape and must not warn: every step FINISHED, so
435
+ // nothing failed plan is being re-entered, and the outer close fix loop writes
436
+ // without spending a replan. `null` (no `plan-accepted` in the window) is the cold
437
+ // path — that resume re-drafts, which is the very escape this warning is about.
438
+ if (restart?.seed?.phase !== 'steps') return false;
439
+ // WHAT IT HAS LEFT. The ceiling is a latch of ONE (PRD v1.12 — unlimited replanning
440
+ // launders thrash as adaptation) plus the arbiter's single variance-granted extra
441
+ // (F85-C). Both spent is zero capacity; a count above zero with the grant STILL
442
+ // UNEARNED is conditional capacity, and warning there would fire the banner on the
443
+ // commonest resume there is, which trains the eye to skip the line that matters.
444
+ //
445
+ // The count is belted the way every other fold site belts one, and in the fail-safe
446
+ // direction FOR A WARNING: an unreadable ledger is not evidence of exhaustion, so it
447
+ // stays silent rather than claiming a fact it cannot read (F6 — unknown is never
448
+ // rounded into a claim).
449
+ const spent = typeof restart.replans === 'number' && Number.isFinite(restart.replans) && restart.replans > 0;
450
+ return spent && restart.replanGrantUsed === true;
451
+ }