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.
- package/CHANGELOG.md +114 -0
- package/bareloop.context.md +245 -33
- package/package.json +1 -1
- package/src/authorreadout.js +323 -0
- package/src/authorrun.js +1345 -0
- package/src/behaviour.js +5 -2
- package/src/cli.js +190 -8
- package/src/index.js +13 -1
- package/src/interviewrun.js +842 -0
- package/src/panel/index.html +1910 -0
- package/src/panel/server.js +1527 -0
- package/src/planrun.js +1 -1
- package/src/replay.js +632 -13
- package/src/replayio.js +159 -0
- package/src/reuse.js +1 -1
- package/src/run.js +1 -1
- package/src/runlist.js +281 -0
- package/src/u-patient.js +73 -0
- package/src/u-readout.js +451 -0
- package/src/userrun.js +2299 -0
- package/types/authorreadout.d.ts +172 -0
- package/types/authorrun.d.ts +37 -0
- package/types/behaviour.d.ts +5 -3
- package/types/cli.d.ts +2 -1
- package/types/index.d.ts +2 -1
- package/types/interviewrun.d.ts +46 -0
- package/types/panel/server.d.ts +265 -0
- package/types/planrun.d.ts +2 -2
- package/types/replay.d.ts +104 -6
- package/types/replayio.d.ts +93 -0
- package/types/reuse.d.ts +2 -2
- package/types/run.d.ts +2 -2
- package/types/runlist.d.ts +112 -0
- package/types/u-patient.d.ts +39 -0
- package/types/u-readout.d.ts +247 -0
- package/types/userrun.d.ts +141 -0
package/src/u-readout.js
ADDED
|
@@ -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
|
+
}
|