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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse one JSONL file into records, tolerant of a malformed line (counted
|
|
3
|
+
* in `skipped`, never thrown on). Mirrors `scripts/behaviour-readout.mjs`'s
|
|
4
|
+
* own parse.
|
|
5
|
+
* @param {string} file
|
|
6
|
+
* @returns {{records: any[], skipped: number}}
|
|
7
|
+
*/
|
|
8
|
+
export function parseJsonl(file: string): {
|
|
9
|
+
records: any[];
|
|
10
|
+
skipped: number;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* `true` when `name` (a bare filename, not a path) is a gate-audit or lag
|
|
14
|
+
* sidecar by NAME convention — the one filename rule that holds across the
|
|
15
|
+
* whole archive.
|
|
16
|
+
* @param {string} name
|
|
17
|
+
*/
|
|
18
|
+
export function isSidecarByName(name: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* `true` when `records` carries at least one `job-start` (present on every
|
|
21
|
+
* real spine checked, first record or not — a reuse/battery run nests one or
|
|
22
|
+
* more ordinary `runJob` calls inside it) or `run-start` (ralph's own inner
|
|
23
|
+
* loop marker, present on most but not all real spines).
|
|
24
|
+
* @param {any[]} records
|
|
25
|
+
*/
|
|
26
|
+
export function looksLikeSpine(records: any[]): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* `<name>.jsonl` → `<name>-gate-audit.jsonl` in the same directory, and the
|
|
29
|
+
* bare run id for display — the run id lives only in the filename (no spine
|
|
30
|
+
* record carries it; see `src/replay.js`'s header). A leading `u-` (the
|
|
31
|
+
* run-u convention) is stripped; every other prefix (`battery-A1-`,
|
|
32
|
+
* `reuse-`, …) is kept verbatim, since it is the only thing distinguishing
|
|
33
|
+
* that run from its siblings in the same directory.
|
|
34
|
+
* @param {string} spinePath
|
|
35
|
+
* @returns {{runId: string, auditPath: string|null}}
|
|
36
|
+
*/
|
|
37
|
+
export function resolveSiblings(spinePath: string): {
|
|
38
|
+
runId: string;
|
|
39
|
+
auditPath: string | null;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Read one run's spine (+ its gate-audit sidecar, when present) off disk and
|
|
43
|
+
* hand both to {@link replayRun}. Read-only, $0, mints no verdict.
|
|
44
|
+
* @param {string} spinePath absolute or cwd-relative path to a `.jsonl` spine
|
|
45
|
+
* @param {{preParsedSpine?: {records: any[], skipped: number}, skipAudit?: boolean}} [opts]
|
|
46
|
+
* `preParsedSpine`: avoids a second parse of the same file when the caller
|
|
47
|
+
* ({@link listSpines}) already read it once to run {@link looksLikeSpine}.
|
|
48
|
+
* `skipAudit` (PR #23 review item 5, 2026-08-26, carried over): a
|
|
49
|
+
* directory listing never opens the gate-audit sidecar at all —
|
|
50
|
+
* `summarizeForAllLine` reads none of the fields that sidecar feeds inside
|
|
51
|
+
* `replayRun`, so reading every run's sidecar in a directory listing was
|
|
52
|
+
* pure unused I/O, multiplied by every spine found.
|
|
53
|
+
* @returns {ReturnType<typeof replayRun>}
|
|
54
|
+
*/
|
|
55
|
+
export function replayOne(spinePath: string, opts?: {
|
|
56
|
+
preParsedSpine?: {
|
|
57
|
+
records: any[];
|
|
58
|
+
skipped: number;
|
|
59
|
+
};
|
|
60
|
+
skipAudit?: boolean;
|
|
61
|
+
}): ReturnType<typeof replayRun>;
|
|
62
|
+
/**
|
|
63
|
+
* List every spine found directly in `dir` (not recursive — same as the
|
|
64
|
+
* script this replaces): one entry per `.jsonl` file that is not a sidecar
|
|
65
|
+
* by name, each either `{kind:'spine', row}` ({@link summarizeForAllLine}'s
|
|
66
|
+
* row) or `{kind:'not-a-spine', name}` when the file's content doesn't look
|
|
67
|
+
* like a spine. Feeds `formatAllLines` (`src/replay.js`) for the printed
|
|
68
|
+
* table — this function does no formatting of its own, only IO + detection.
|
|
69
|
+
* @param {string} dir
|
|
70
|
+
* @returns {Array<{kind: 'spine', row: ReturnType<typeof summarizeForAllLine>}|{kind: 'not-a-spine', name: string}>}
|
|
71
|
+
*/
|
|
72
|
+
export function listSpines(dir: string): Array<{
|
|
73
|
+
kind: "spine";
|
|
74
|
+
row: ReturnType<typeof summarizeForAllLine>;
|
|
75
|
+
} | {
|
|
76
|
+
kind: "not-a-spine";
|
|
77
|
+
name: string;
|
|
78
|
+
}>;
|
|
79
|
+
/**
|
|
80
|
+
* Read a bundle's `history.jsonl` (one `bareloop run` row per line, written
|
|
81
|
+
* by `appendHistory` in `src/bundle.js`) — the single reader for this file,
|
|
82
|
+
* replacing `src/cli.js`'s own hand-rolled `readFileSync`/split. Tolerant of
|
|
83
|
+
* a malformed line the same way {@link parseJsonl} is (counted in
|
|
84
|
+
* `skipped`, never thrown on).
|
|
85
|
+
* @param {string} file absolute path to `history.jsonl`
|
|
86
|
+
* @returns {{rows: any[], skipped: number}}
|
|
87
|
+
*/
|
|
88
|
+
export function readHistoryLog(file: string): {
|
|
89
|
+
rows: any[];
|
|
90
|
+
skipped: number;
|
|
91
|
+
};
|
|
92
|
+
import { replayRun } from './replay.js';
|
|
93
|
+
import { summarizeForAllLine } from './replay.js';
|
package/types/reuse.d.ts
CHANGED
|
@@ -234,7 +234,7 @@ export function selectBridge({ registry, job, ask, provider, pinned, shortlist,
|
|
|
234
234
|
* abandoned attempt twice in a chain of resumes.
|
|
235
235
|
*
|
|
236
236
|
* @param {any[]} events the dead spine's events, parsed, in file order
|
|
237
|
-
* @param {{deathAt?: number|null, direct?: boolean, resumableOutcomes?: string[]}} [opts]
|
|
237
|
+
* @param {{deathAt?: number|null, direct?: boolean, resumableOutcomes?: readonly string[]}} [opts]
|
|
238
238
|
* `deathAt`: when the process is judged to have died (the watchdog's kill record,
|
|
239
239
|
* which is later and better evidence than the last event). Defaults to the last
|
|
240
240
|
* event's own timestamp — the last sign of life there is.
|
|
@@ -246,7 +246,7 @@ export function selectBridge({ registry, job, ask, provider, pinned, shortlist,
|
|
|
246
246
|
export function readResume(events: any[], { deathAt, direct, resumableOutcomes }?: {
|
|
247
247
|
deathAt?: number | null;
|
|
248
248
|
direct?: boolean;
|
|
249
|
-
resumableOutcomes?: string[];
|
|
249
|
+
resumableOutcomes?: readonly string[];
|
|
250
250
|
}): any;
|
|
251
251
|
/**
|
|
252
252
|
* The AGE gate for a paused checkpoint — the sibling of `resumeTreeGate`, and it
|
package/types/run.d.ts
CHANGED
|
@@ -143,7 +143,7 @@
|
|
|
143
143
|
* a manufactured-fixation probe). `true` is the ON/experimental arm; the
|
|
144
144
|
* default is the OFF arm. (Shell-owned seam, same doctrine as the provider
|
|
145
145
|
* binding.)
|
|
146
|
-
* @param {{decision: string, text?: string}|null} [opts.humanRuling] N4 — the SIGNER's
|
|
146
|
+
* @param {{decision: string, text?: string|null}|null} [opts.humanRuling] N4 — the SIGNER's
|
|
147
147
|
* answer at a hitl pause (accept | rerun <text> | pause), carried by the leg that
|
|
148
148
|
* RESUMES a paused run and forwarded verbatim to the plan flow. Absent on every
|
|
149
149
|
* ordinary run; never authored and never defaulted (a defaulted answer to "is this
|
|
@@ -219,7 +219,7 @@ export function runJob(rawSpec: object | string, { approvals, workdir, provider,
|
|
|
219
219
|
layerRoot?: boolean | undefined;
|
|
220
220
|
humanRuling?: {
|
|
221
221
|
decision: string;
|
|
222
|
-
text?: string;
|
|
222
|
+
text?: string | null;
|
|
223
223
|
} | null | undefined;
|
|
224
224
|
heldRuling?: {
|
|
225
225
|
decision: string;
|
package/types/source.d.ts
CHANGED
|
@@ -88,6 +88,51 @@ export function prepareSource({ source, into, destination, fetchTimeoutMs }: {
|
|
|
88
88
|
manifestPath: string;
|
|
89
89
|
manifest: object;
|
|
90
90
|
} | SourceRefusal>;
|
|
91
|
+
/**
|
|
92
|
+
* THE INSTALL-GAP DETECTOR (PRD item 33 close-out, `docs/product/PRD.md:450-452`,
|
|
93
|
+
* hamr's ruling 2026-09-14, option A): `prepareSource` copies ONLY git-tracked
|
|
94
|
+
* files (hamr's ruling, `docs/product/ITEM33-BUILD.md:225-239` — unchanged by
|
|
95
|
+
* this function), so a JS/TS repo's prepared copy never carries `node_modules`,
|
|
96
|
+
* and every close stage needing a tool (`tsc`, a test runner) instrument-stops
|
|
97
|
+
* with nothing telling the person why. bareloop NEVER runs an install itself —
|
|
98
|
+
* it only ever touches gated primitives, and a shell-out to `npm`/etc from
|
|
99
|
+
* inside this module would be exactly the verb hamr's law reserves for the
|
|
100
|
+
* worker's own granted tools, never the arbiter-side machinery that decides
|
|
101
|
+
* whether a run even starts. This is a pure, $0 DETECTOR: it names the gap and
|
|
102
|
+
* the exact command a person runs THEMSELVES in the copy, then returns.
|
|
103
|
+
*
|
|
104
|
+
* JS/TS ONLY for now — a repo in another detected language returns `null`
|
|
105
|
+
* here unconditionally (no `package.json` to find), which is the correct
|
|
106
|
+
* "nothing to report" reading, not a gap in this function; per-language
|
|
107
|
+
* dependency-gap data for other languages is M3b's job
|
|
108
|
+
* (`docs/product/ITEM33-BUILD.md`, "M3b"), not this one's.
|
|
109
|
+
*
|
|
110
|
+
* SUBFOLDER-AS-REPO (ruling 2 addendum, commit 2a020d8): a subfolder job
|
|
111
|
+
* freezes the WHOLE repo, so `package.json` may sit at the repo root, at the
|
|
112
|
+
* subfolder itself, or anywhere between — the SAME nearest-manifest-walking-up
|
|
113
|
+
* rule `detectLanguage` (`src/detectlang.js`) applies, done independently here
|
|
114
|
+
* (not by importing `detectLanguage`) to avoid a `source.js` ⇄ `detectlang.js`
|
|
115
|
+
* import cycle, since `detectlang.js` already imports `nearestGitAncestor`
|
|
116
|
+
* from this module.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} treeDir the prepared tree's root (`prepareSource`'s
|
|
119
|
+
* returned `tree`, e.g. `<into>/tree` — a REPO source sits directly at this
|
|
120
|
+
* root; every other kind sits under `input/` inside it and has no repo
|
|
121
|
+
* dependency story yet, so callers only need this for a `kind: 'repo'`
|
|
122
|
+
* manifest)
|
|
123
|
+
* @param {string} [sourceSubdir] the manifest's own `sourceSubdir` ('' or
|
|
124
|
+
* undefined when Source IS the repo root)
|
|
125
|
+
* @returns {{manager: string, command: string, reason: string}|null} `null`
|
|
126
|
+
* when there is nothing to report: no JS/TS manifest found walking up from
|
|
127
|
+
* `sourceSubdir` to `treeDir`, the nearest one found has no dependencies,
|
|
128
|
+
* `node_modules` is already there, or the manifest cannot even be read as
|
|
129
|
+
* JSON (an invalid `package.json` is "cannot tell", never a false alarm).
|
|
130
|
+
*/
|
|
131
|
+
export function missingDependencies(treeDir: string, sourceSubdir?: string): {
|
|
132
|
+
manager: string;
|
|
133
|
+
command: string;
|
|
134
|
+
reason: string;
|
|
135
|
+
} | null;
|
|
91
136
|
/**
|
|
92
137
|
* The delivered name (M2b fix 4, kept under D3): a destination FILE of
|
|
93
138
|
* `/home/me/profile.md` lands as `/home/me/profile-2026-09-12.md`, and a
|
package/types/text.d.ts
CHANGED
|
@@ -100,6 +100,41 @@ export function capStop({ ceilingUsd, knownUsd, spendComplete }: {
|
|
|
100
100
|
knownUsd: number;
|
|
101
101
|
spendComplete: boolean;
|
|
102
102
|
}): "cap-halt" | "pricing-red" | null;
|
|
103
|
+
/**
|
|
104
|
+
* THE ONE SPELLING of "was this throw a call casualty, or something else" —
|
|
105
|
+
* F179/F180's seam. `authorscout.js`'s `settled` narrows a rejected
|
|
106
|
+
* `loop.run()` to this one class: swallowing every throw would launder a
|
|
107
|
+
* budget `HaltError`, a programming bug, or any other named terminal into
|
|
108
|
+
* "the call failed" — the exact class of dishonesty this repo refuses (an
|
|
109
|
+
* unknown reported as a known).
|
|
110
|
+
*
|
|
111
|
+
* bare-agent 0.42.0's `OpenAIProvider.generate` used to throw a raw
|
|
112
|
+
* `SyntaxError` from an unguarded `JSON.parse(tc.function.arguments)` after
|
|
113
|
+
* a billed HTTP round (F179/BA-27) — this predicate used to admit that class
|
|
114
|
+
* too. bare-agent 0.43.0 fixed it upstream (`parseToolCalls`, shared by
|
|
115
|
+
* OpenAIProvider and Ollama): a malformed tool-call arguments string now
|
|
116
|
+
* returns a priced round with `toolCalls: []` plus its own `malformedToolCall`
|
|
117
|
+
* marker instead of throwing. Verified against 0.43.0's installed source that
|
|
118
|
+
* no provider bareloop constructs (`AnthropicProvider`, `OpenAIProvider`,
|
|
119
|
+
* `GeminiProvider` — `src/providers.js`) can still throw a JSON `SyntaxError`
|
|
120
|
+
* out of `generate()` after a billed round: Anthropic and Gemini read tool-call
|
|
121
|
+
* arguments as already-parsed objects off their own response shape (nothing to
|
|
122
|
+
* `JSON.parse`), and every provider's raw-HTTP-body parse is already
|
|
123
|
+
* try/catch-wrapped inside `_request`, converted to a plain `Error` before it
|
|
124
|
+
* can reach here. bareloop never streams a provider response. So the
|
|
125
|
+
* SyntaxError branch this predicate used to carry is dead code and was
|
|
126
|
+
* removed in the same change that bumped to bare-agent 0.43.0 — re-add it (or
|
|
127
|
+
* a narrower successor) only if a real provider is found to reintroduce the
|
|
128
|
+
* class.
|
|
129
|
+
*
|
|
130
|
+
* A `HaltError` is deliberately excluded — it is bare-agent's own governance
|
|
131
|
+
* exit (a budget/turn cap), never a transport casualty, and this predicate
|
|
132
|
+
* must never launder one into a mere "the call failed".
|
|
133
|
+
* @param {any} err
|
|
134
|
+
* @returns {string|null} a short reason when `err` is an admitted casualty class,
|
|
135
|
+
* `null` for everything else (including `HaltError`) — the caller re-throws on `null`.
|
|
136
|
+
*/
|
|
137
|
+
export function callCasualty(err: any): string | null;
|
|
103
138
|
/**
|
|
104
139
|
* ONE raw model emission, ready to persist.
|
|
105
140
|
*
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reset a patient to its frozen seed and drop its `.litectx` store.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} wd absolute path to the patient's tree (a git repo with >=1 commit)
|
|
5
|
+
* @param {string} seed the frozen seed commit
|
|
6
|
+
* @returns {{ head: string, storeRemoved: boolean }} `head` is the short sha the tree
|
|
7
|
+
* now stands at; `storeRemoved` says whether a store was actually there to remove
|
|
8
|
+
* (so a caller can print the truth rather than a claim).
|
|
9
|
+
*/
|
|
10
|
+
export function coldReset(wd: string, seed: string): {
|
|
11
|
+
head: string;
|
|
12
|
+
storeRemoved: boolean;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* F186 — the patient's own `.gitignore` denies `*.jsonl`, so `coldReset`'s
|
|
16
|
+
* `git clean -fd` (no `-x`) never removes a `gate-audit.jsonl` already
|
|
17
|
+
* sitting at the tree root — it can only be there from an EARLIER run
|
|
18
|
+
* against this same patient (an authoring scout, or a prior worker run)
|
|
19
|
+
* that never got archived out. Left in place, THIS run's own Gate (which
|
|
20
|
+
* writes to that same path) would append to it, and the end-of-run rename
|
|
21
|
+
* that claims the whole file as "this run's own audit" would silently
|
|
22
|
+
* carry every earlier run's rows too — a harness slicing a shared append-
|
|
23
|
+
* only log has to account for every writer inside its window.
|
|
24
|
+
*
|
|
25
|
+
* Call this AFTER `coldReset` and BEFORE this run's own Gate ever opens: at
|
|
26
|
+
* that point a `gate-audit.jsonl` sitting in the tree is provably not this
|
|
27
|
+
* run's, because coldReset just proved the tree is unable to have produced
|
|
28
|
+
* one since the seed reset. NOT for a resume — a halted run's own gate
|
|
29
|
+
* audit already in the tree it resumes into is that SAME run's prior leg,
|
|
30
|
+
* not a stranger's; callers on the resume path must not call this.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} wd absolute path to the patient's tree, just cold-reset
|
|
33
|
+
* @param {string} spineDir absolute path to this run's spine directory
|
|
34
|
+
* @param {string} runid this run's own id (the same one every other spine
|
|
35
|
+
* file for this run is named after)
|
|
36
|
+
* @returns {string|null} the path the stale file was moved to, or `null`
|
|
37
|
+
* when there was nothing to move
|
|
38
|
+
*/
|
|
39
|
+
export function moveStaleGateAudit(wd: string, spineDir: string, runid: string): string | null;
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A token count, k/M-abbreviated — 2 decimals at and above a million (the tail's own
|
|
3
|
+
* `.2M`-shaped resolution matters more once six figures are folded into a suffix), 1
|
|
4
|
+
* decimal in the thousands, and the bare integer under that (a two- or three-digit
|
|
5
|
+
* round is more readable plain than as `"0.2k"`).
|
|
6
|
+
* @param {number} n
|
|
7
|
+
* @returns {string}
|
|
8
|
+
*/
|
|
9
|
+
export function fmtTokens(n: number): string;
|
|
10
|
+
/**
|
|
11
|
+
* THE TOKENS TAIL LINE — hamr's 2026-09-01 order: `job-end` carries dollars only, and
|
|
12
|
+
* the run tail never prints a token total anywhere, though tokens exist per round on
|
|
13
|
+
* `worker-round.usage` and `judge-round.usage`
|
|
14
|
+
* ({inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens}).
|
|
15
|
+
*
|
|
16
|
+
* Summed off THIS LEG's own spine, over every round TYPE that spends money —
|
|
17
|
+
* worker-round AND judge-round (house rule: a spend-slicing instrument must enumerate
|
|
18
|
+
* every round type that spends, or it silently undercounts). `worker-result` is an
|
|
19
|
+
* ATTEMPT-LEVEL ECHO of round sums, not a round, and is deliberately never matched
|
|
20
|
+
* here — folding it in would double the real figure.
|
|
21
|
+
*
|
|
22
|
+
* A round whose `usage` is missing or not an object (today: every `judge-round` — the
|
|
23
|
+
* payload at `src/planrun.js`'s `onJudgeCost` does not carry one) is counted as
|
|
24
|
+
* UNKNOWN, never laundered into a silent 0 — the same rule `costUsd` already lives by
|
|
25
|
+
* (F6/F44): an unpriced round is reported as unpriced, not folded into the total as
|
|
26
|
+
* if it cost nothing.
|
|
27
|
+
*
|
|
28
|
+
* @param {{events: any[]}} o `events` this leg's own parsed spine, never a folded
|
|
29
|
+
* chain — a resumed leg's tokens are this leg's own bill, same as `rounds` above it.
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
export function tokensLine({ events }: {
|
|
33
|
+
events: any[];
|
|
34
|
+
}): string;
|
|
35
|
+
/**
|
|
36
|
+
* The banner's wall line: what this run has consumed of its signed cap.
|
|
37
|
+
* @param {{legMs: number, priorWallMs?: number, wallLabel: string}} o
|
|
38
|
+
* `legMs`: wall this process bought. `priorWallMs`: wall its predecessors burnt,
|
|
39
|
+
* as `readResume` folded it (0/absent on a cold run). `wallLabel`: the signed cap
|
|
40
|
+
* as the runner spells it, including the honest UNBOUNDED prose.
|
|
41
|
+
* @returns {string}
|
|
42
|
+
*/
|
|
43
|
+
export function wallLine({ legMs, priorWallMs, wallLabel }: {
|
|
44
|
+
legMs: number;
|
|
45
|
+
priorWallMs?: number;
|
|
46
|
+
wallLabel: string;
|
|
47
|
+
}): string;
|
|
48
|
+
/**
|
|
49
|
+
* THE EVIDENCE PACKAGE (N4, ruling 2) — what a person is shown before they answer.
|
|
50
|
+
*
|
|
51
|
+
* *"Before/after changes plus every mechanical stage's result; never a bare
|
|
52
|
+
* 'approve?'"*. The LIBRARY assembles the facts and puts them on the `hitl-pause`
|
|
53
|
+
* spine record — the ask the close itself declared, every stage's own result, and
|
|
54
|
+
* WHICH files the run changed (`src/planrun.js`'s `emitHitlPause`). This renders
|
|
55
|
+
* them, and adds the one thing a library cannot state honestly from where it sits:
|
|
56
|
+
* the LINE-level diff, which needs a screen and a repository in front of it.
|
|
57
|
+
*
|
|
58
|
+
* Nothing here derives a verdict, and nothing here decides. It is a readout of a
|
|
59
|
+
* record that already exists — so an absence is written as prose (F6: an empty
|
|
60
|
+
* stage list is "the checkpoint carries none", never a blank screen that reads as
|
|
61
|
+
* "nothing happened") and a trim is announced (F28).
|
|
62
|
+
*
|
|
63
|
+
* @param {{pause: any, diff: {lines: string[], truncated?: number, untracked?: string[],
|
|
64
|
+
* unavailable?: string|null}|null}} o `pause` the spine record; `diff` what the
|
|
65
|
+
* runner read off the patient, or null when it did not look.
|
|
66
|
+
* @returns {string[]} lines, ready to print
|
|
67
|
+
*/
|
|
68
|
+
export function evidencePackage({ pause, diff }: {
|
|
69
|
+
pause: any;
|
|
70
|
+
diff: {
|
|
71
|
+
lines: string[];
|
|
72
|
+
truncated?: number;
|
|
73
|
+
untracked?: string[];
|
|
74
|
+
unavailable?: string | null;
|
|
75
|
+
} | null;
|
|
76
|
+
}): string[];
|
|
77
|
+
/**
|
|
78
|
+
* THE REVIEW DOOR's package (softgreen module 8) — the same evidence, at the door
|
|
79
|
+
* at the END of a run rather than at a stage inside the close.
|
|
80
|
+
*
|
|
81
|
+
* It is a SEPARATE renderer from `evidencePackage` and deliberately so: that one
|
|
82
|
+
* opens with *"this run reached the one stage a machine cannot render, and it is
|
|
83
|
+
* waiting on you"*, which at a review door would be false twice over — the machine
|
|
84
|
+
* rendered every stage, and the run is over. What a person needs told here is the
|
|
85
|
+
* opposite fact: a verdict was minted, it STANDS, and what happens to the work is
|
|
86
|
+
* theirs. The stage/changed/diff body is shared, because it is the same evidence.
|
|
87
|
+
* @param {{door: any, diff?: any}} o
|
|
88
|
+
* @returns {string[]}
|
|
89
|
+
*/
|
|
90
|
+
export function reviewDoorPackage({ door, diff }: {
|
|
91
|
+
door: any;
|
|
92
|
+
diff?: any;
|
|
93
|
+
}): string[];
|
|
94
|
+
/**
|
|
95
|
+
* The door's OWN `close-timing-red` refusal (F137, hamr's ruling 2026-09-07,
|
|
96
|
+
* `docs/product/CLOSE-INTEGRITY-BUILD.md` "Ruled 2026-09-07"): when the
|
|
97
|
+
* door's timing pass (`resolveCloseTimeoutMs`) times out, the accept must
|
|
98
|
+
* REFUSE outright rather than fall through to the library's 120s default —
|
|
99
|
+
* mirroring the wording/options list `scripts/run-u.mjs`'s in-run
|
|
100
|
+
* `close-timing-red` escalation already prints (~line 1173), never a second
|
|
101
|
+
* invented phrasing. Pure so the wording is unit-testable without spawning
|
|
102
|
+
* the script or waiting out the real 600s timing-preflight ceiling.
|
|
103
|
+
* @param {{names: string}} args comma-joined names of the stage(s) that timed out
|
|
104
|
+
* @returns {string[]}
|
|
105
|
+
*/
|
|
106
|
+
export function doorTimingRedLines({ names }: {
|
|
107
|
+
names: string;
|
|
108
|
+
}): string[];
|
|
109
|
+
/**
|
|
110
|
+
* The three doors AT THE REVIEW DOOR. Same order and the same rule as
|
|
111
|
+
* `doorLines` (rerun leads, ~40% rubber-stamp), different consequences: this run
|
|
112
|
+
* is OVER, so a rerun is a fresh engagement rather than a continuation, and a
|
|
113
|
+
* pause keeps the door rather than a step checkpoint.
|
|
114
|
+
* @param {{rerun: string, accept: string, pause: string, ttlDays: number, held: boolean}} o
|
|
115
|
+
* @returns {string[]}
|
|
116
|
+
*/
|
|
117
|
+
export function runDoorLines({ rerun, accept, pause, ttlDays, held }: {
|
|
118
|
+
rerun: string;
|
|
119
|
+
accept: string;
|
|
120
|
+
pause: string;
|
|
121
|
+
ttlDays: number;
|
|
122
|
+
held: boolean;
|
|
123
|
+
}): string[];
|
|
124
|
+
/**
|
|
125
|
+
* THE THREE DOORS, rendered — and the 2026-08-12 §4 rule is the ORDER.
|
|
126
|
+
*
|
|
127
|
+
* ~40% of human reviewers approve without reading. That datum is a design input:
|
|
128
|
+
* the default must lean toward rerun and never toward accept, because a default
|
|
129
|
+
* that costs one extra cycle when wrong is cheap and a default that mints a green
|
|
130
|
+
* nobody read is the failure mode the whole class exists to prevent.
|
|
131
|
+
*
|
|
132
|
+
* "Default" here is which door the prompt LEADS with — never an action taken for
|
|
133
|
+
* the operator. No door happens without its flag, and this says so.
|
|
134
|
+
*
|
|
135
|
+
* The third door is PAUSE (2026-08-18), not cancel. hamr: *"what's the point of
|
|
136
|
+
* cancel anyways? pause can resume — that would be more honest"*. A person who does
|
|
137
|
+
* not want to carry on today is not making a permanent judgment, and the door should
|
|
138
|
+
* not ask them to: the checkpoint keeps, and an unanswered one expires on its own.
|
|
139
|
+
*
|
|
140
|
+
* @param {{rerun: string, accept: string, pause: string}} invocations the exact
|
|
141
|
+
* command each door costs, so nobody has to reconstruct one
|
|
142
|
+
* @returns {string[]}
|
|
143
|
+
*/
|
|
144
|
+
export function doorLines({ rerun, accept, pause }: {
|
|
145
|
+
rerun: string;
|
|
146
|
+
accept: string;
|
|
147
|
+
pause: string;
|
|
148
|
+
}): string[];
|
|
149
|
+
/**
|
|
150
|
+
* WHEN THE DEAD LEG STOPPED — the input `readResume` folds into `priorWallMs`, and
|
|
151
|
+
* therefore what the resumed leg's wall remainder is computed from.
|
|
152
|
+
*
|
|
153
|
+
* The runner has two candidate witnesses and they answer different questions:
|
|
154
|
+
*
|
|
155
|
+
* - the WATCHDOG's kill record (`<spine>.watchdog.json`), which is written when the
|
|
156
|
+
* outside guard reaps a run. For a run that was killed it is later and better
|
|
157
|
+
* evidence than the last spine event: the process was alive, silently, right up to
|
|
158
|
+
* the signal, and billing only to its last emitted event would under-report the
|
|
159
|
+
* wall it really burnt.
|
|
160
|
+
* - the SPINE's own landed terminal (`job-end`). A run that wrote one did not die —
|
|
161
|
+
* it ENDED ITSELF and dated its own stop.
|
|
162
|
+
*
|
|
163
|
+
* So the preference order is not "the later record wins", it is "the run's own record
|
|
164
|
+
* wins when there is one". N4 §1.6 is where that distinction stopped being academic:
|
|
165
|
+
* a `hitl-pause` is a clean terminal a person may answer days later, and any record
|
|
166
|
+
* dated after it bills their deciding time to the run's wall. The POC measured the
|
|
167
|
+
* shape — a report 45 days after the pause made `RESUME_WALL_MS` exactly 0, dooming
|
|
168
|
+
* the resumed leg before it opened, on a run that had barely started. W-2 holds by
|
|
169
|
+
* construction only if this reads the pause.
|
|
170
|
+
*
|
|
171
|
+
* Returning `null` is not a failure: it hands `readResume` back its own documented
|
|
172
|
+
* default (the last event's timestamp), which for a clean terminal IS the terminal.
|
|
173
|
+
*
|
|
174
|
+
* @param {{watchdogAt?: unknown, events?: unknown}} o `watchdogAt` as the report
|
|
175
|
+
* spells it (an ISO string, or anything at all — the file is read off disk);
|
|
176
|
+
* `events` the dead spine, parsed.
|
|
177
|
+
* @returns {number|null} ms, or null to leave the default in place
|
|
178
|
+
*/
|
|
179
|
+
export function deathAtOf({ watchdogAt, events }?: {
|
|
180
|
+
watchdogAt?: unknown;
|
|
181
|
+
events?: unknown;
|
|
182
|
+
}): number | null;
|
|
183
|
+
/**
|
|
184
|
+
* WHERE A RESUME PICKS UP — the preview's `at` line, which is what says how much of
|
|
185
|
+
* the signed dollars this leg can still spend.
|
|
186
|
+
*
|
|
187
|
+
* `readStepCheckpoint` (src/reuse.js) knows two phases and a null: `steps` (re-enter
|
|
188
|
+
* the plan at the first unfinished step), `close` (every step landed; the close and
|
|
189
|
+
* its fix loop are what is left), and no checkpoint at all (the halt landed before a
|
|
190
|
+
* plan was accepted). The runner rendered those three directly, and a PAUSE fits none
|
|
191
|
+
* of them cleanly:
|
|
192
|
+
*
|
|
193
|
+
* - a pause happens AFTER the plan's steps, at the close's human stage. On the
|
|
194
|
+
* fixture a person actually meets, the checkpoint reads `phase:'steps'` with every
|
|
195
|
+
* step green (the pause site emits no `outer-close` of its own), so the step
|
|
196
|
+
* arithmetic ran off the end and printed `at step 2 of 1 "(unknown)"` — a step
|
|
197
|
+
* count larger than the plan and an id nobody drafted. hamr found it on the first
|
|
198
|
+
* resume preview he read;
|
|
199
|
+
* - and the same walk-off is reachable one terminal over, with no person involved:
|
|
200
|
+
* a kill between the last `step-end` and the `outer-close` leaves exactly that
|
|
201
|
+
* shape, and read `step 3 of 2`.
|
|
202
|
+
*
|
|
203
|
+
* So the count is never allowed to exceed the plan, and the PHASE is said in words.
|
|
204
|
+
* Rendering only; it decides nothing and bounds nothing.
|
|
205
|
+
*
|
|
206
|
+
* @param {{seed: {phase?: string, plan?: any, completedSteps?: any[]}|null|undefined,
|
|
207
|
+
* paused: boolean}} o `seed` is `readResume`'s `restart.seed`; `paused` is the
|
|
208
|
+
* recorded terminal being `hitl-pause` — read off the spine, never off a flag.
|
|
209
|
+
* @returns {string[]} lines, ready to print
|
|
210
|
+
*/
|
|
211
|
+
export function resumeAtLines({ seed, paused }: {
|
|
212
|
+
seed: {
|
|
213
|
+
phase?: string;
|
|
214
|
+
plan?: any;
|
|
215
|
+
completedSteps?: any[];
|
|
216
|
+
} | null | undefined;
|
|
217
|
+
paused: boolean;
|
|
218
|
+
}): string[];
|
|
219
|
+
/**
|
|
220
|
+
* F97 — the DOOMED RESUME read, the one the operator should make before signing.
|
|
221
|
+
*
|
|
222
|
+
* `u-msn0uccv` spent $0.82 re-entering the exact plan whose action was the diagnosed
|
|
223
|
+
* defect, carrying `priorReplans: 2` and `priorReplanGrantUsed: true`. A resume does
|
|
224
|
+
* not re-draft — `scout-skipped {reason:"resumed"}` and the plan comes back
|
|
225
|
+
* byte-for-byte — so with the replan ledger empty there was no mechanism left that
|
|
226
|
+
* could replace it. Two attempts, 26 rounds, 0 writes, `step-red` with money and
|
|
227
|
+
* minutes still on the clock. Nothing in the library was at fault; every part of that
|
|
228
|
+
* leg reported honestly. The DECISION to fire was the defect, and the read that would
|
|
229
|
+
* have refused it costs $0: both facts are already parsed by the preview gate.
|
|
230
|
+
*
|
|
231
|
+
* WARNING ONLY. This never blocks and never changes behaviour — a resume of a
|
|
232
|
+
* mechanical gap (a named wall, a count) converts on re-entry all the time, and the
|
|
233
|
+
* operator is the one who can tell which kind of gap it is. Blocking here would be
|
|
234
|
+
* this file inventing a gate; the arbiter's gates are signed, not derived from a
|
|
235
|
+
* readout module.
|
|
236
|
+
*
|
|
237
|
+
* @param {{seed?: {phase?: string}|null, replans?: number, replanGrantUsed?: boolean}|null} [restart]
|
|
238
|
+
* `readResume`'s `restart` fold: WHERE it picks up and WHAT LEDGER it inherited.
|
|
239
|
+
* @returns {boolean} true only for the shape F97 measured.
|
|
240
|
+
*/
|
|
241
|
+
export function doomedResume(restart?: {
|
|
242
|
+
seed?: {
|
|
243
|
+
phase?: string;
|
|
244
|
+
} | null;
|
|
245
|
+
replans?: number;
|
|
246
|
+
replanGrantUsed?: boolean;
|
|
247
|
+
} | null): boolean;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A fresh run — today's `--job` / `--spec` path. `opts.workdir`/`opts.seed`/
|
|
3
|
+
* `opts.spineName` name the patient (the JOBS-table row, or the `--spec`
|
|
4
|
+
* manifest's own reading of it, when called through `main`/`scripts/run-u.mjs`).
|
|
5
|
+
* @param {any} spec the parsed, not-yet-validated job spec
|
|
6
|
+
* @param {RunOpts} [opts]
|
|
7
|
+
* @returns {Promise<number>} an exit code — never calls process.exit
|
|
8
|
+
*/
|
|
9
|
+
export function startRun(spec: any, opts?: RunOpts): Promise<number>;
|
|
10
|
+
/**
|
|
11
|
+
* A halted run — today's `--resume` path.
|
|
12
|
+
* @param {string} runId the runid (or an explicit spine path, for tests)
|
|
13
|
+
* @param {RunOpts & {decide?: string|null, text?: string|null}} [opts]
|
|
14
|
+
* @returns {Promise<number>} an exit code — never calls process.exit
|
|
15
|
+
*/
|
|
16
|
+
export function resumeRun(runId: string, opts?: RunOpts & {
|
|
17
|
+
decide?: string | null;
|
|
18
|
+
text?: string | null;
|
|
19
|
+
}): Promise<number>;
|
|
20
|
+
/**
|
|
21
|
+
* A finished run's review door — today's `--door` path.
|
|
22
|
+
* @param {string} runId the runid (or an explicit spine path, for tests)
|
|
23
|
+
* @param {{decide: string|null, text?: string|null}} decision
|
|
24
|
+
* @param {RunOpts} [opts]
|
|
25
|
+
* @returns {Promise<number>} an exit code — never calls process.exit
|
|
26
|
+
*/
|
|
27
|
+
export function answerDoor(runId: string, decision: {
|
|
28
|
+
decide: string | null;
|
|
29
|
+
text?: string | null;
|
|
30
|
+
}, opts?: RunOpts): Promise<number>;
|
|
31
|
+
/**
|
|
32
|
+
* Parses this runner's own argv shape — the JOBS table's `--job <key>` /
|
|
33
|
+
* `--spec <path>`, plus `--resume`/`--door`/`--decide`/`--text`/
|
|
34
|
+
* `--review-door`/`--approve`/`--registry`/`--workflow`/`--model`/
|
|
35
|
+
* `--read-shim`/`--scout` — and dispatches to whichever of the three doors
|
|
36
|
+
* above the flags name. `scripts/run-u.mjs` is the thin adapter over this,
|
|
37
|
+
* the same shape `bin/bareloop.mjs` already is over `src/cli.js`'s own
|
|
38
|
+
* `main(argv, deps)`.
|
|
39
|
+
* @param {string[]} argv
|
|
40
|
+
* @param {Deps} [deps]
|
|
41
|
+
* @returns {Promise<number>}
|
|
42
|
+
*/
|
|
43
|
+
export function main(argv: string[], deps?: Deps): Promise<number>;
|
|
44
|
+
export type Deps = {
|
|
45
|
+
env?: Record<string, string | undefined> | undefined;
|
|
46
|
+
out?: ((s: string) => void) | undefined;
|
|
47
|
+
err?: ((s: string) => void) | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* the TEST SEAM: supplying this skips the real-key
|
|
50
|
+
* check entirely, the same shape `src/cli.js`'s `main(argv, deps)` uses.
|
|
51
|
+
*/
|
|
52
|
+
provider?: any;
|
|
53
|
+
providerFor?: ((tier: string) => any) | undefined;
|
|
54
|
+
judgeProvider?: any;
|
|
55
|
+
};
|
|
56
|
+
export type RunOpts = {
|
|
57
|
+
/**
|
|
58
|
+
* the parsed, not-yet-validated job spec — read by
|
|
59
|
+
* `resumeRun`/`answerDoor`, whose positional argument is the runid rather
|
|
60
|
+
* than the spec (unlike `startRun`, which takes `spec` positionally and
|
|
61
|
+
* never reads this).
|
|
62
|
+
*/
|
|
63
|
+
spec?: any;
|
|
64
|
+
/**
|
|
65
|
+
* the patient's tree
|
|
66
|
+
*/
|
|
67
|
+
workdir?: string | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* the patient's frozen seed commit
|
|
70
|
+
*/
|
|
71
|
+
seed?: string | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* the spine directory name, beside `workdir`
|
|
74
|
+
*/
|
|
75
|
+
spineName?: string | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* the raw `--model` tier name (`sonnet`/`haiku`), or null for the default
|
|
78
|
+
*/
|
|
79
|
+
model?: string | null | undefined;
|
|
80
|
+
/**
|
|
81
|
+
* the raw `--read-shim` arm name, or null for the default
|
|
82
|
+
*/
|
|
83
|
+
readShim?: string | null | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* the raw `--scout` value (`on`/`off`), or null for the default
|
|
86
|
+
*/
|
|
87
|
+
scout?: string | null | undefined;
|
|
88
|
+
/**
|
|
89
|
+
* a bridge registry directory
|
|
90
|
+
*/
|
|
91
|
+
registry?: string | null | undefined;
|
|
92
|
+
/**
|
|
93
|
+
* the registry's workflow name (defaults to `spec.job`)
|
|
94
|
+
*/
|
|
95
|
+
workflow?: string | null | undefined;
|
|
96
|
+
/**
|
|
97
|
+
* the spec hash being signed; omitted previews only
|
|
98
|
+
*/
|
|
99
|
+
approve?: string | null | undefined;
|
|
100
|
+
/**
|
|
101
|
+
* opt in to a green-class run opening a review door
|
|
102
|
+
*/
|
|
103
|
+
reviewDoor?: boolean | undefined;
|
|
104
|
+
/**
|
|
105
|
+
* how the re-invocation commands this prints should name the job (`--job x` / `--spec <path>`)
|
|
106
|
+
*/
|
|
107
|
+
selector?: string | undefined;
|
|
108
|
+
/**
|
|
109
|
+
* how a "go edit the spec" hint should read
|
|
110
|
+
*/
|
|
111
|
+
specDesc?: string | undefined;
|
|
112
|
+
deps?: Deps | undefined;
|
|
113
|
+
};
|
|
114
|
+
export type RunCtx = {
|
|
115
|
+
spec: any;
|
|
116
|
+
specPath: string;
|
|
117
|
+
selector: string;
|
|
118
|
+
specDesc: string;
|
|
119
|
+
workdir: string;
|
|
120
|
+
seed: string;
|
|
121
|
+
spineName: string;
|
|
122
|
+
model: string | null;
|
|
123
|
+
readShim: string | null;
|
|
124
|
+
scout: string | null;
|
|
125
|
+
registry: string | null;
|
|
126
|
+
workflow: string | null;
|
|
127
|
+
approve: string | null;
|
|
128
|
+
reviewDoor: boolean;
|
|
129
|
+
resume: string | null;
|
|
130
|
+
door: string | null;
|
|
131
|
+
decide: string | null;
|
|
132
|
+
text: string | null;
|
|
133
|
+
deps: Deps;
|
|
134
|
+
};
|