bareloop 0.26.0 → 0.28.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 +271 -0
- package/bareloop.context.md +308 -38
- package/package.json +4 -3
- package/src/authorflow.js +255 -33
- package/src/authorjob.js +12 -9
- package/src/authorreadout.js +323 -0
- package/src/authorrun.js +1337 -0
- package/src/authorscout.js +16 -11
- package/src/cli.js +130 -8
- package/src/index.js +14 -2
- package/src/interviewrun.js +842 -0
- package/src/job.js +42 -26
- package/src/planrun.js +82 -3
- package/src/promptregisters.js +8 -0
- package/src/providers.js +36 -0
- package/src/replayio.js +153 -0
- package/src/reuse.js +1 -1
- package/src/run.js +1 -1
- package/src/source.js +138 -1
- package/src/text.js +45 -0
- package/src/u-patient.js +73 -0
- package/src/u-readout.js +451 -0
- package/src/userrun.js +2273 -0
- package/src/validate.js +25 -1
- package/types/authorflow.d.ts +49 -7
- package/types/authorreadout.d.ts +172 -0
- package/types/authorrun.d.ts +37 -0
- package/types/index.d.ts +3 -2
- package/types/interviewrun.d.ts +46 -0
- package/types/job.d.ts +21 -0
- package/types/planrun.d.ts +43 -3
- package/types/providers.d.ts +28 -0
- package/types/replayio.d.ts +93 -0
- package/types/reuse.d.ts +2 -2
- package/types/run.d.ts +2 -2
- package/types/source.d.ts +45 -0
- package/types/text.d.ts +35 -0
- package/types/u-patient.d.ts +39 -0
- package/types/u-readout.d.ts +247 -0
- package/types/userrun.d.ts +134 -0
- package/types/validate.d.ts +22 -0
package/src/text.js
CHANGED
|
@@ -8,8 +8,12 @@
|
|
|
8
8
|
// record, for the same reason again — a second writer that forgets the scrub is
|
|
9
9
|
// a leak on the very trail the scrub exists to protect.
|
|
10
10
|
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
11
12
|
import { redactSecrets } from './validate.js';
|
|
12
13
|
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
const { HaltError } = require('bare-agent');
|
|
16
|
+
|
|
13
17
|
// A fence counts as the artifact's WRAPPER only when it opens within this many
|
|
14
18
|
// lines of the response — the chatty-preamble shape ("Here's the fix:\n```…").
|
|
15
19
|
// A fence buried deeper inside an unfenced reply is the artifact's OWN content
|
|
@@ -149,6 +153,47 @@ export function capStop({ ceilingUsd, knownUsd, spendComplete }) {
|
|
|
149
153
|
return null;
|
|
150
154
|
}
|
|
151
155
|
|
|
156
|
+
/**
|
|
157
|
+
* THE ONE SPELLING of "was this throw a call casualty, or something else" —
|
|
158
|
+
* F179/F180's seam. `authorscout.js`'s `settled` narrows a rejected
|
|
159
|
+
* `loop.run()` to this one class: swallowing every throw would launder a
|
|
160
|
+
* budget `HaltError`, a programming bug, or any other named terminal into
|
|
161
|
+
* "the call failed" — the exact class of dishonesty this repo refuses (an
|
|
162
|
+
* unknown reported as a known).
|
|
163
|
+
*
|
|
164
|
+
* bare-agent 0.42.0's `OpenAIProvider.generate` used to throw a raw
|
|
165
|
+
* `SyntaxError` from an unguarded `JSON.parse(tc.function.arguments)` after
|
|
166
|
+
* a billed HTTP round (F179/BA-27) — this predicate used to admit that class
|
|
167
|
+
* too. bare-agent 0.43.0 fixed it upstream (`parseToolCalls`, shared by
|
|
168
|
+
* OpenAIProvider and Ollama): a malformed tool-call arguments string now
|
|
169
|
+
* returns a priced round with `toolCalls: []` plus its own `malformedToolCall`
|
|
170
|
+
* marker instead of throwing. Verified against 0.43.0's installed source that
|
|
171
|
+
* no provider bareloop constructs (`AnthropicProvider`, `OpenAIProvider`,
|
|
172
|
+
* `GeminiProvider` — `src/providers.js`) can still throw a JSON `SyntaxError`
|
|
173
|
+
* out of `generate()` after a billed round: Anthropic and Gemini read tool-call
|
|
174
|
+
* arguments as already-parsed objects off their own response shape (nothing to
|
|
175
|
+
* `JSON.parse`), and every provider's raw-HTTP-body parse is already
|
|
176
|
+
* try/catch-wrapped inside `_request`, converted to a plain `Error` before it
|
|
177
|
+
* can reach here. bareloop never streams a provider response. So the
|
|
178
|
+
* SyntaxError branch this predicate used to carry is dead code and was
|
|
179
|
+
* removed in the same change that bumped to bare-agent 0.43.0 — re-add it (or
|
|
180
|
+
* a narrower successor) only if a real provider is found to reintroduce the
|
|
181
|
+
* class.
|
|
182
|
+
*
|
|
183
|
+
* A `HaltError` is deliberately excluded — it is bare-agent's own governance
|
|
184
|
+
* exit (a budget/turn cap), never a transport casualty, and this predicate
|
|
185
|
+
* must never launder one into a mere "the call failed".
|
|
186
|
+
* @param {any} err
|
|
187
|
+
* @returns {string|null} a short reason when `err` is an admitted casualty class,
|
|
188
|
+
* `null` for everything else (including `HaltError`) — the caller re-throws on `null`.
|
|
189
|
+
*/
|
|
190
|
+
export function callCasualty(err) {
|
|
191
|
+
if (err instanceof HaltError) return null;
|
|
192
|
+
const e = /** @type {any} */ (err);
|
|
193
|
+
if (e?.code === 'ETIMEDOUT' || e?.name === 'TimeoutError') return String(e?.message ?? e);
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
152
197
|
// ── THE RAW MODEL OUTPUT, AS A PERSISTED RECORD ─────────────────────────────
|
|
153
198
|
//
|
|
154
199
|
// Run mslhn707 died on a survey that broke JSON at position 1339 and left NO
|
package/src/u-patient.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// THE COLD RESET, in ONE place.
|
|
2
|
+
//
|
|
3
|
+
// `run-u.mjs` has always owned this: a cold run starts at the frozen seed with the
|
|
4
|
+
// `.litectx` store removed, because a run that inherits the previous run's edits — or
|
|
5
|
+
// the previous run's isolate-verb memory (stash/remember persist in the store) — is
|
|
6
|
+
// measuring the wrong thing. The reuse rung's OFF arm and every cold contrast depend
|
|
7
|
+
// on it.
|
|
8
|
+
//
|
|
9
|
+
// It moved here the day a BATTERY driver needed to rehearse it (the read-shim Phase 2
|
|
10
|
+
// battery's `--dry-run` has to prove the patient really does go cold between rows).
|
|
11
|
+
// The alternative was a second spelling of `git reset --hard` in the battery, and two
|
|
12
|
+
// spellings of "cold" is exactly how one driver's rows come to start from a different
|
|
13
|
+
// tree than the other's — the blind-instrument class, at the patient.
|
|
14
|
+
//
|
|
15
|
+
// Nothing here decides anything and nothing here is arbiter territory: it is the
|
|
16
|
+
// mechanical preparation of a tree, and both callers print what it did.
|
|
17
|
+
import { execFileSync } from 'node:child_process';
|
|
18
|
+
import {
|
|
19
|
+
rmSync, existsSync, renameSync,
|
|
20
|
+
} from 'node:fs';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Reset a patient to its frozen seed and drop its `.litectx` store.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} wd absolute path to the patient's tree (a git repo with >=1 commit)
|
|
27
|
+
* @param {string} seed the frozen seed commit
|
|
28
|
+
* @returns {{ head: string, storeRemoved: boolean }} `head` is the short sha the tree
|
|
29
|
+
* now stands at; `storeRemoved` says whether a store was actually there to remove
|
|
30
|
+
* (so a caller can print the truth rather than a claim).
|
|
31
|
+
*/
|
|
32
|
+
export function coldReset(wd, seed) {
|
|
33
|
+
const git = (/** @type {string[]} */ a) => execFileSync('git', ['-C', wd, ...a], { encoding: 'utf8' }).trim();
|
|
34
|
+
git(['reset', '--hard', seed]);
|
|
35
|
+
git(['clean', '-fd']);
|
|
36
|
+
const store = join(wd, '.litectx');
|
|
37
|
+
const storeRemoved = existsSync(store);
|
|
38
|
+
rmSync(store, { recursive: true, force: true });
|
|
39
|
+
return { head: git(['rev-parse', '--short', 'HEAD']), storeRemoved };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* F186 — the patient's own `.gitignore` denies `*.jsonl`, so `coldReset`'s
|
|
44
|
+
* `git clean -fd` (no `-x`) never removes a `gate-audit.jsonl` already
|
|
45
|
+
* sitting at the tree root — it can only be there from an EARLIER run
|
|
46
|
+
* against this same patient (an authoring scout, or a prior worker run)
|
|
47
|
+
* that never got archived out. Left in place, THIS run's own Gate (which
|
|
48
|
+
* writes to that same path) would append to it, and the end-of-run rename
|
|
49
|
+
* that claims the whole file as "this run's own audit" would silently
|
|
50
|
+
* carry every earlier run's rows too — a harness slicing a shared append-
|
|
51
|
+
* only log has to account for every writer inside its window.
|
|
52
|
+
*
|
|
53
|
+
* Call this AFTER `coldReset` and BEFORE this run's own Gate ever opens: at
|
|
54
|
+
* that point a `gate-audit.jsonl` sitting in the tree is provably not this
|
|
55
|
+
* run's, because coldReset just proved the tree is unable to have produced
|
|
56
|
+
* one since the seed reset. NOT for a resume — a halted run's own gate
|
|
57
|
+
* audit already in the tree it resumes into is that SAME run's prior leg,
|
|
58
|
+
* not a stranger's; callers on the resume path must not call this.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} wd absolute path to the patient's tree, just cold-reset
|
|
61
|
+
* @param {string} spineDir absolute path to this run's spine directory
|
|
62
|
+
* @param {string} runid this run's own id (the same one every other spine
|
|
63
|
+
* file for this run is named after)
|
|
64
|
+
* @returns {string|null} the path the stale file was moved to, or `null`
|
|
65
|
+
* when there was nothing to move
|
|
66
|
+
*/
|
|
67
|
+
export function moveStaleGateAudit(wd, spineDir, runid) {
|
|
68
|
+
const staleAudit = join(wd, 'gate-audit.jsonl');
|
|
69
|
+
if (!existsSync(staleAudit)) return null;
|
|
70
|
+
const preFile = join(spineDir, `pre-${runid}-gate-audit.jsonl`);
|
|
71
|
+
renameSync(staleAudit, preFile);
|
|
72
|
+
return preFile;
|
|
73
|
+
}
|
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
|
+
}
|