bareloop 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/validate.js CHANGED
@@ -73,6 +73,65 @@ export function isNonEmptyString(v) {
73
73
  return typeof v === 'string' && v.length > 0;
74
74
  }
75
75
 
76
+ /**
77
+ * PRD item 27/M2 close-bytes signature, orchestrator-flagged gap
78
+ * (2026-09-06): a close stage's `cmd` can name an addressable script TWO
79
+ * ways — `node <path> …` (M1's original scope) or a BARE directly-executable
80
+ * absolute path (a `.sh` wrapper, e.g. `jobs/aurora-testgen-cold.json`'s
81
+ * close, which `readCloseScripts` used to skip entirely, leaving both
82
+ * `close-absolute-path` and the sha256 signature blind to it — a "blind
83
+ * instrument" the audit named directly). This list is the shared, STATIC
84
+ * (no fs) shape test both `src/job.js` (which only needs to know WHETHER a
85
+ * stage names an addressable script, to decide whether `sha256` is
86
+ * demanded) and `src/close-integrity.js` (which resolves the token against
87
+ * a real cwd and reads it off disk) key off — one spelling, so the demand
88
+ * and the detector can never drift apart about which stages are in scope.
89
+ * A leading-interpreter cmd (`sh`/`bash`/`python`/`python3`/`npx` besides
90
+ * `node`) is included for the same reason `node` is: a close author may use
91
+ * any of them, and a scope that only recognized `node` would just move the
92
+ * blind spot rather than close it.
93
+ * @type {readonly string[]}
94
+ */
95
+ export const CLOSE_SCRIPT_INTERPRETERS = Object.freeze(['node', 'sh', 'bash', 'python', 'python3', 'npx']);
96
+
97
+ /**
98
+ * The candidate script path TOKEN a close stage's `cmd` names, by SHAPE
99
+ * alone — no fs, no cwd resolution, so this is safe for `validateJob` (pure)
100
+ * to call directly. Two recognized forms, first match wins:
101
+ * - an INTERPRETER cmd: the token is the first argv[1..] element that is
102
+ * PATH-SHAPED (not a `-`-prefixed flag, and either contains `/` or ends
103
+ * in one of `.mjs .cjs .js .ts .sh .py`) — never just argv[1] outright.
104
+ * `node --enable-source-maps ./close/x.mjs stage` names `./close/x.mjs`,
105
+ * not `--enable-source-maps`; `python -m pytest` and `npx tsc --noEmit`
106
+ * name NOTHING (`-m`/`--noEmit` are flags, `pytest`/`tsc` are bare
107
+ * command names with no path shape) — fixed 2026-09-06 (orchestrator
108
+ * audit) after the earlier bare-argv[1] version demanded a sha256 for a
109
+ * token that named no file, making such a spec unsignable forever (the
110
+ * sign helper reads nothing, the validator reds permanently);
111
+ * - a bare cmd whose argv[0] ITSELF is an absolute path (no interpreter
112
+ * prefix — a `.sh`/other directly-executable close script) → the token
113
+ * is argv[0].
114
+ * Anything else — a relative bare executable (`true`, `pytest`), an
115
+ * interpreter with no path-shaped argument (`python -m pytest`, `npx tsc
116
+ * --noEmit`), a non-string/empty cmd — names no addressable file under this
117
+ * scheme and returns null; those stages carry no sha256 demand and are
118
+ * invisible to the byte/path detectors, exactly as they always were (most
119
+ * test-fixture closes use exactly this shape on purpose, to exercise
120
+ * runtime behaviour with no real script file at all).
121
+ * @param {unknown} cmd
122
+ * @returns {string|null}
123
+ */
124
+ export function closeScriptCandidateToken(cmd) {
125
+ if (typeof cmd !== 'string') return null;
126
+ const parts = cmd.trim().split(/\s+/);
127
+ if (!parts[0]) return null;
128
+ if (CLOSE_SCRIPT_INTERPRETERS.includes(parts[0])) {
129
+ const pathShaped = (/** @type {string} */ tok) => !tok.startsWith('-') && (tok.includes('/') || /\.(?:mjs|cjs|js|ts|sh|py)$/.test(tok));
130
+ return parts.slice(1).find(pathShaped) ?? null;
131
+ }
132
+ return parts[0].startsWith('/') ? parts[0] : null;
133
+ }
134
+
76
135
  // Secrets never enter the tree/spine/configs (hard line) — BOTH config
77
136
  // documents get the same sweep: the agent-authored workflow config is the
78
137
  // riskier entry point (machine-written), the operator's job spec the other.
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Hex sha256 of a close script's TEXT, exactly the way `src/bundle.js`'s own
3
+ * `sha256(source)` hashes a close script's source before bundling (a JS
4
+ * source file is always valid UTF-8, so hashing the decoded string and
5
+ * hashing the raw file bytes agree) — ONE formula, so a signature minted here
6
+ * and a mismatch read at export time can never disagree about what "the
7
+ * bytes" means.
8
+ * @param {string} text
9
+ * @returns {string} sha256 hex
10
+ */
11
+ export function hashCloseScriptBytes(text: string): string;
12
+ /**
13
+ * F8/F129 — a close judges the cwd the runner gives it, never a path baked
14
+ * into the script itself. This scans every quoted string literal in a close
15
+ * script's source for an absolute POSIX path (`/…`) that EXISTS on this
16
+ * machine right now and is not under an allow-listed system prefix.
17
+ *
18
+ * Deliberately monotone and simple, not clever, with named limits: a
19
+ * nonexistent absolute-looking string is treated as a NAME, not a path —
20
+ * `existsSync` is the only oracle this has for "load-bearing", so it is not
21
+ * flagged; a path built by string concatenation (never one whole literal)
22
+ * is invisible to this scan; and a literal that only exists inside a `//` or
23
+ * `/* *\/` comment is naturally excluded because a comment carries no quote
24
+ * characters of its own around the path text.
25
+ * @param {string} source close script text
26
+ * @returns {string[]} the offending literal path values, in source order
27
+ */
28
+ export function absolutePathLiteralsOf(source: string): string[];
29
+ /**
30
+ * Resolve every `close[].cmd` (or `closeDecl`'s equivalent, via
31
+ * `closeStagesOf`) that names an addressable script (`closeScriptCandidateToken`'s
32
+ * scope: an interpreter cmd or a bare absolute executable) to its absolute
33
+ * path and bytes off disk. The spec's OWN `cmd` path is the address and is never
34
+ * itself judged — only the named script's CONTENT is read here. A relative
35
+ * path resolves against `cwd` (the caller passes the run's own workdir or
36
+ * the spec file's directory, matching how each caller already resolves
37
+ * paths elsewhere). Never throws: a missing/unreadable script reports
38
+ * `bytes: null` so a caller can red it distinctly from a tampered one.
39
+ * `sha256` (M2, additive): the stage's OWN signed field, verbatim, or `null`
40
+ * when the stage carries none — the field this rung's checker compares
41
+ * against `hashCloseScriptBytes(bytes)`. M1's callers read `.stage`/`.path`/
42
+ * `.bytes` only, so this addition is behaviour-preserving for them.
43
+ * @param {any} spec resolved job spec (or anything `closeStagesOf` accepts)
44
+ * @param {string} cwd
45
+ * @returns {{stage: string, path: string, bytes: string|null, sha256: string|null}[]}
46
+ */
47
+ export function readCloseScripts(spec: any, cwd: string): {
48
+ stage: string;
49
+ path: string;
50
+ bytes: string | null;
51
+ sha256: string | null;
52
+ }[];
53
+ /**
54
+ * The $0 run-start integrity check (PRD item 27(c)): red `close-absolute-path`
55
+ * for EVERY job whose close script bakes in an absolute path literal that
56
+ * exists on disk — the same rule `src/bundle.js`'s `exportBundle` already
57
+ * applies at export time, now applied before the close-first precheck and
58
+ * before any provider call, for every run, not only exported bundles. A
59
+ * script this reads couldn't read (missing/unreadable) is not reported here
60
+ * — that is `broken-close`'s job once the close actually tries to run it.
61
+ * @param {any} spec resolved job spec
62
+ * @param {string} cwd
63
+ * @returns {{ ok: true } | { ok: false, reds: {stage: string, path: string, literal: string}[] }}
64
+ */
65
+ export function checkCloseAbsolutePaths(spec: any, cwd: string): {
66
+ ok: true;
67
+ } | {
68
+ ok: false;
69
+ reds: {
70
+ stage: string;
71
+ path: string;
72
+ literal: string;
73
+ }[];
74
+ };
75
+ /**
76
+ * M2's run-start integrity check (PRD item 27, `close-tampered`): every
77
+ * signed `close[].sha256` must match the script's bytes off disk, RIGHT NOW —
78
+ * checked before the close-first precheck and before any provider call,
79
+ * exactly where `checkCloseAbsolutePaths` already runs (the two are always
80
+ * called together at that seam). A script this cannot read reads `actual:
81
+ * null` — distinct from a byte mismatch, but still a red: an unreadable
82
+ * signed script is exactly as untrustworthy as a tampered one, and silently
83
+ * treating "gone" as "fine" would be the M1 F129 hazard in a new coat.
84
+ * @param {any} spec resolved job spec
85
+ * @param {string} cwd
86
+ * @returns {{ ok: true } | { ok: false, reds: ByteRed[] }}
87
+ */
88
+ export function checkCloseByteSignature(spec: any, cwd: string): {
89
+ ok: true;
90
+ } | {
91
+ ok: false;
92
+ reds: ByteRed[];
93
+ };
94
+ /**
95
+ * PRD item 27/M3, Part B — "the library `runJob` takes `closeDir` and refuses
96
+ * at $0 if a spec's close needs it and none is given." A close script's
97
+ * NEED is read the same way its bytes already are (`readCloseScripts`): a
98
+ * script whose source mentions `BARELOOP_CLOSE_DIR` reads that variable at
99
+ * run time, so a run with no `closeDir` handed to it would run that script
100
+ * against `undefined` — silently, since `env.BARELOOP_CLOSE_DIR` would just
101
+ * be absent from the child's environment rather than throwing. Refusing here,
102
+ * at the same $0 run-start seam as the absolute-path/byte-signature checks,
103
+ * turns that silent gap into a named red before any tokens spend, rather than
104
+ * leaving it to whatever the script's own author remembered to check for.
105
+ *
106
+ * A script that never mentions the variable carries no demand — most
107
+ * predicate-only fixture closes (`true`, `pytest`) have no books of their own
108
+ * to keep, and requiring a `closeDir` for them would be a needless refusal.
109
+ * @param {any} spec resolved job spec
110
+ * @param {string} cwd
111
+ * @param {string|null|undefined} closeDir the value the runner is about to hand
112
+ * the close as `BARELOOP_CLOSE_DIR` (or none)
113
+ * @returns {{ ok: true } | { ok: false, reds: {stage: string, path: string}[] }}
114
+ */
115
+ export function checkCloseDirRequired(spec: any, cwd: string, closeDir: string | null | undefined): {
116
+ ok: true;
117
+ } | {
118
+ ok: false;
119
+ reds: {
120
+ stage: string;
121
+ path: string;
122
+ }[];
123
+ };
124
+ /**
125
+ * The re-verify that runs before EVERY close run, precheck included (PRD
126
+ * item 27, M2 §2) — scoped to the STAGES about to actually run rather than
127
+ * the whole spec, because that is the true blast radius of "was this close
128
+ * script's content tampered with since it was signed" for this particular
129
+ * invocation. Same comparison, same red shape, as `checkCloseByteSignature`;
130
+ * kept as a separate export because its caller (`runCloseStages` in
131
+ * `src/planrun.js`) already holds a stage array, not a spec.
132
+ * @param {any[]} stages a close stage chain (the shape `runStages` takes)
133
+ * @param {string} cwd
134
+ * @returns {{ ok: true } | { ok: false, reds: ByteRed[] }}
135
+ */
136
+ export function checkStageByteSignature(stages: any[], cwd: string): {
137
+ ok: true;
138
+ } | {
139
+ ok: false;
140
+ reds: ByteRed[];
141
+ };
142
+ /**
143
+ * M2's minting helper (PRD item 27 §3) — fills `close[].sha256` from disk for
144
+ * every addressable-script stage in `spec.close` (`closeScriptCandidateToken`'s
145
+ * scope: an interpreter cmd or a bare absolute executable, e.g. a `.sh`
146
+ * wrapper). Pure: never writes anything itself (the CLI wrapper,
147
+ * `scripts/sign-close.mjs`, gates on `--write`) and never throws on a
148
+ * missing/unreadable script — it names the miss in `missing` and leaves
149
+ * that stage's `sha256` untouched, so a caller can refuse to sign (and
150
+ * refuse to report a hash it never computed) a spec it could not fully
151
+ * hash. A stage outside this rung's scope (a relative bare executable, a
152
+ * declared-kind stage) passes through unchanged and is never listed as
153
+ * missing — it was never addressable to begin with (same scope
154
+ * `readCloseScripts`/`checkCloseAbsolutePaths` already draw).
155
+ *
156
+ * Only ever operates on `spec.close` (the command-close array) — a
157
+ * `closeDecl` spec has no script path to sign and `signCloseScripts` reports
158
+ * `ok:false` for one rather than silently doing nothing to it.
159
+ * @param {any} spec resolved job spec (parsed `jobs/<x>.json`)
160
+ * @param {string} cwd base for resolving a RELATIVE script path (jobs in this
161
+ * repo only ever use absolute `cmd` paths; relative resolution is here for
162
+ * parity with `readCloseScripts` and any future relative-path spec)
163
+ * @returns {{ ok: boolean, spec: any, changes: {stage: string, path: string, old: string|null, new: string}[], missing: {stage: string, path: string}[] }}
164
+ */
165
+ export function signCloseScripts(spec: any, cwd: string): {
166
+ ok: boolean;
167
+ spec: any;
168
+ changes: {
169
+ stage: string;
170
+ path: string;
171
+ old: string | null;
172
+ new: string;
173
+ }[];
174
+ missing: {
175
+ stage: string;
176
+ path: string;
177
+ }[];
178
+ };
179
+ /**
180
+ * A close script is entitled to name a real system path (`/usr/bin/env`,
181
+ * `/etc/hosts`, …) without that being the F8/F129 hazard — only a path a
182
+ * runner would actually relocate (a patient checkout, a scratch dir) is.
183
+ * @type {string[]}
184
+ */
185
+ export const SYSTEM_PATH_PREFIXES: string[];
186
+ /** the exact env var name a close script reads for its own books directory
187
+ * (Part B, PRD item 27/M3) — named once so the detector below and the runner
188
+ * that sets it (`src/ralph.js`'s `runClose`) can never drift into two
189
+ * spellings of the same contract. */
190
+ export const CLOSE_DIR_ENV_VAR: "BARELOOP_CLOSE_DIR";
191
+ export type ByteRed = {
192
+ stage: string;
193
+ path: string;
194
+ expected: string;
195
+ actual: string | null;
196
+ };
@@ -0,0 +1,126 @@
1
+ /**
2
+ * The $0 timing preflight (PRD item 27, M3 §2): run every close STAGE once,
3
+ * ignoring verdicts — first-red-wins stays for the real verdict precheck
4
+ * (`src/planrun.js`'s `check-preflight`); this is a separate, sequential pass
5
+ * (F131/F68: never two closes in flight against one tree) whose only purpose
6
+ * is TIMING. A declared-close stage (no `.cmd`, a kind executor) has nothing
7
+ * a command runner can spawn and is skipped — it contributes no reading, not
8
+ * a zero one (a silent zero would understate the real close's later cost).
9
+ *
10
+ * @param {any[]} stages a close stage chain (`runStages`'s own shape)
11
+ * @param {{cwd?: string, redact?: (s: string) => string, ceilingMs?: number}} [opts]
12
+ * `ceilingMs` (test seam only — never wired to any spec/runner surface):
13
+ * overrides `TIMING_PREFLIGHT_CEILING_MS` so a hang-detection test does not
14
+ * have to wait 10 real minutes. Production callers never pass it.
15
+ * @returns {Promise<{
16
+ * perStage: {name: string, ms: number, exitCode: number|null, timedOut: boolean}[],
17
+ * slowestMs: number, slowestName: string|null, anyTimedOut: boolean,
18
+ * }>}
19
+ */
20
+ export function timeCloseStages(stages: any[], { cwd, redact, ceilingMs }?: {
21
+ cwd?: string;
22
+ redact?: (s: string) => string;
23
+ ceilingMs?: number;
24
+ }): Promise<{
25
+ perStage: {
26
+ name: string;
27
+ ms: number;
28
+ exitCode: number | null;
29
+ timedOut: boolean;
30
+ }[];
31
+ slowestMs: number;
32
+ slowestName: string | null;
33
+ anyTimedOut: boolean;
34
+ }>;
35
+ /**
36
+ * The ceiling formula (PRD item 27, M3): `max(FLOOR, K × slowest measured
37
+ * stage)`, unless an explicit override is given — the override is the
38
+ * OPERATOR'S number (the rates-passthrough shape, F113: guesstimate + loud
39
+ * sign + customer override), so it may legally sit ABOVE or BELOW the
40
+ * autoset estimate; this is not tighten-only.
41
+ * @param {{slowestMs: number, overrideMs?: number|null}} args
42
+ * @returns {{ ceilingMs: number, source: 'estimated'|'override' }}
43
+ */
44
+ export function computeCloseTimeoutCeiling({ slowestMs, overrideMs }: {
45
+ slowestMs: number;
46
+ overrideMs?: number | null;
47
+ }): {
48
+ ceilingMs: number;
49
+ source: "estimated" | "override";
50
+ };
51
+ /**
52
+ * ONE resolver for "what per-stage close timeout does this job get", shared
53
+ * by `src/planrun.js` (the in-run path) and `scripts/run-u.mjs` (which must
54
+ * know the answer BEFORE it spawns the outside watchdog — F67 — so the
55
+ * watchdog's own stale/grace windows are sized off the real ceiling rather
56
+ * than a hardcoded guess). Two callers computing this independently is
57
+ * exactly the class of drift this whole rung exists to close (F129's own
58
+ * lesson, one level up): this function is the only place the precedence is
59
+ * spelled out.
60
+ *
61
+ * Precedence: a signed spec override (`job.closeTimeoutMs`) wins outright,
62
+ * with NO timing pass run (the pass would waste real machine time and, on a
63
+ * deliberately slow/hung stage, block for up to `TIMING_PREFLIGHT_CEILING_MS`
64
+ * for no purpose). Absent that, every stage is timed once (F131's autoset
65
+ * design) and the ceiling is `max(FLOOR, K × slowest)`.
66
+ * @param {{job: any, stages: any[], cwd: string, redact?: (s: string) => string, ceilingMs?: number}} args
67
+ * `ceilingMs` — see `timeCloseStages`; test seam only.
68
+ * @returns {Promise<{
69
+ * closeTimeoutMs: number|null, source: 'estimated'|'override',
70
+ * timing: Awaited<ReturnType<typeof timeCloseStages>>|null, timedOut: boolean,
71
+ * }>} `closeTimeoutMs: null, timedOut: true` means a stage never finished the
72
+ * timing pass — the caller reds `close-timing-red` rather than trusting any
73
+ * number here.
74
+ */
75
+ export function resolveCloseTimeoutMs({ job, stages, cwd, redact, ceilingMs }: {
76
+ job: any;
77
+ stages: any[];
78
+ cwd: string;
79
+ redact?: (s: string) => string;
80
+ ceilingMs?: number;
81
+ }): Promise<{
82
+ closeTimeoutMs: number | null;
83
+ source: "estimated" | "override";
84
+ timing: Awaited<ReturnType<typeof timeCloseStages>> | null;
85
+ timedOut: boolean;
86
+ }>;
87
+ /**
88
+ * The printed banner (build spec M3 §2's exact wording) — ONE spelling, so
89
+ * the run-u tail and the bundle CLI can never drift into two announcements of
90
+ * the same number. `source` accepts 'explicit' too: a caller-passed
91
+ * `closeTimeoutMs` runtime option (the pre-M3 shell/test knob, kept for
92
+ * backward compatibility and test control — never the signed spec field) is
93
+ * a real override but not a SIGNED one, so it earns its own honest word
94
+ * rather than being folded into 'signed override'.
95
+ * @param {{ceilingMs: number, source: 'estimated'|'override'|'explicit', slowestMs?: number, slowestName?: string|null}} args
96
+ * @returns {string}
97
+ */
98
+ export function closeTimeoutBanner({ ceilingMs, source, slowestMs, slowestName }: {
99
+ ceilingMs: number;
100
+ source: "estimated" | "override" | "explicit";
101
+ slowestMs?: number;
102
+ slowestName?: string | null;
103
+ }): string;
104
+ /** hamr, 2026-09-07: the floor, verbatim. Also the value `runClose`'s own
105
+ * un-autoset default has always used — kept as its own named constant here
106
+ * (not re-exported from ralph.js) because the two are different CLAIMS: one is
107
+ * "what runClose does if nobody says otherwise", the other is "the arbiter's
108
+ * chosen floor for autoset" — they happen to be the same number today, and a
109
+ * future change to either must not silently move the other. */
110
+ export const CLOSE_TIMEOUT_FLOOR_MS: 120000;
111
+ /** hamr, 2026-09-07, verbatim: "K = 5 ('2 and 5 are fine')." */
112
+ export const CLOSE_TIMEOUT_K: 5;
113
+ /** `runClose`'s own shipped default when nobody passes a timeout at all
114
+ * (`src/ralph.js:221`) — the ceiling the $0 timing PASS ITSELF runs each
115
+ * stage under is a multiple of this, never of the floor above (the two are
116
+ * the same number today; named separately on purpose, see the constant
117
+ * above). */
118
+ export const LIBRARY_DEFAULT_CLOSE_TIMEOUT_MS: 120000;
119
+ /**
120
+ * The generous PROVISIONAL ceiling the timing preflight runs every stage
121
+ * under, so "a genuinely hung stage still ends" (build spec, M3 §2) rather
122
+ * than blocking the timing pass forever. Deliberately the library default × K
123
+ * (600s) — NOT the autoset/override ceiling this module computes, which is
124
+ * only known after the pass completes.
125
+ */
126
+ export const TIMING_PREFLIGHT_CEILING_MS: number;
package/types/index.d.ts CHANGED
@@ -28,3 +28,4 @@ export { replayRun, formatReplay } from "./replay.js";
28
28
  export { runInterview, authorCloseForJob, assembleSpec, prepareSigning, refusalEvents, refuseLockedKind, GENRE, REFUSAL_LIB, REFUSAL_CATEGORY, AUTHORED_SPEC_FIELDS } from "./authorjob.js";
29
29
  export { PROMPT_REGISTERS, isPromptFile } from "./promptregisters.js";
30
30
  export { exportBundle, bundleHash, readBundle, resolveBundleSpec, checkEnvelope, bless, verifyBlessing, appendHistory, checkBundleDeps } from "./bundle.js";
31
+ export { readCloseScripts, checkCloseAbsolutePaths, absolutePathLiteralsOf, hashCloseScriptBytes, checkCloseByteSignature, checkStageByteSignature, signCloseScripts } from "./close-integrity.js";
@@ -221,7 +221,14 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
221
221
  * ladder (src/ladder.js). Arbiter territory exactly as the count it replaced was —
222
222
  * the runner sets it, the plan cannot express it, and no step may tighten or raise
223
223
  * it.
224
- * @param {number} [opts.closeTimeoutMs] close/check wall-clock cap (shell territory)
224
+ * @param {number} [opts.closeTimeoutMs] close/check wall-clock cap (shell territory).
225
+ * OPTIONAL as of PRD item 27/M3: absent, this run autosets it from a $0 seed
226
+ * timing pass (or the spec's own signed `closeTimeoutMs` override) — an
227
+ * explicit value here still wins outright (backward compatibility/test
228
+ * control only, F133: `scripts/run-u.mjs`/`src/cli.js` never pass this).
229
+ * @param {string|null} [opts.closeDir] the close's own books directory (PRD
230
+ * item 27/M3 Part B), forwarded to every close invocation as
231
+ * `BARELOOP_CLOSE_DIR`
225
232
  * @param {number} [opts.maxStepRounds] the shell's per-step rounds ceiling (validatePlan's bound)
226
233
  * @param {boolean} [opts.scout=true] operator-only switch (docs/product/SCOUT-CONTRAST.md):
227
234
  * `false` on a FRESH run (no `resumeSeed`) skips the survey entirely — `scoutBlob`
@@ -392,12 +399,18 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
392
399
  * precisely the state the person rejected), and the words reach the PLANNER as new
393
400
  * authoring. It is never a `humanRuling` — that answers a close's human STAGE, and
394
401
  * a green-class job has none. Empty words are refused at the same seam every door is.
402
+ * @param {boolean} [opts.resumable=true] PRD item 27(c)/F130 — does THIS runner
403
+ * support `--resume`? Forwarded verbatim from `runJob`; `run-u.mjs` leaves it at
404
+ * the default (byte-identical to before this param existed), the bundle CLI
405
+ * passes `false` so the three `--resume`-naming readouts below (`MONEY_OPTIONS`,
406
+ * the resume-plan-red option, the fix-loop terminal) say "resume is `run-u`-only
407
+ * in v1" instead of naming a flag that would fail if typed.
395
408
  * @returns {Promise<string>} 'green' | 'already-green' | 'escalated' | 'plan-red' |
396
409
  * 'check-red' | 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' |
397
410
  * 'branch-red' | 'cap-halt' | 'wall-halt' | 'provider-red' | 'interpreter-red' |
398
411
  * 'step-stalled' | 'hitl-pause' | 'hitl-decision-red' | `step-red:<id>`
399
412
  */
400
- export function runPlan(job: any, { workdir, provider, nativeProvider, providerFor, judgeProvider, emit, remainingUsd, isUnpriced, spendComplete, capRuns, strikeLimit, closeTimeoutMs, maxStepRounds, layerRoot, readShim, scout, scoutRounds, bridge, now, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, priorSpentUsd, reviewDoor, doorRerun }: {
413
+ export function runPlan(job: any, { workdir, provider, nativeProvider, providerFor, judgeProvider, emit, remainingUsd, isUnpriced, spendComplete, capRuns, strikeLimit, closeTimeoutMs, closeDir, maxStepRounds, layerRoot, readShim, scout, scoutRounds, bridge, now, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, priorSpentUsd, reviewDoor, doorRerun, resumable }: {
401
414
  workdir: string;
402
415
  provider: any;
403
416
  nativeProvider?: ((o: {
@@ -414,6 +427,7 @@ export function runPlan(job: any, { workdir, provider, nativeProvider, providerF
414
427
  capRuns?: number | undefined;
415
428
  strikeLimit?: number | undefined;
416
429
  closeTimeoutMs?: number | undefined;
430
+ closeDir?: string | null | undefined;
417
431
  maxStepRounds?: number | undefined;
418
432
  scout?: boolean | undefined;
419
433
  scoutRounds?: number | undefined;
@@ -449,6 +463,7 @@ export function runPlan(job: any, { workdir, provider, nativeProvider, providerF
449
463
  fromRunid?: string | null;
450
464
  receivedAt?: string | null;
451
465
  } | null | undefined;
466
+ resumable?: boolean | undefined;
452
467
  }): Promise<string>;
453
468
  /**
454
469
  * The recorded bound-reason's ceiling in the WORKER-facing note. The reason is a
package/types/ralph.d.ts CHANGED
@@ -54,17 +54,24 @@
54
54
  *
55
55
  * @param {string[]} close argv, e.g. ['node', '--test', 'close/']
56
56
  * @param {(s: string) => string} [redact] source scrubber; identity by default
57
- * @param {{ timeoutMs?: number, cwd?: string, expect?: number, judged?: {pattern: string, min: number}, gapKeep?: string }} [opts]
57
+ * @param {{ timeoutMs?: number, cwd?: string, expect?: number, judged?: {pattern: string, min: number}, gapKeep?: string, closeDir?: string|null }} [opts]
58
58
  * close wall-clock cap (operator-set via the runner, never the config's — the
59
59
  * agent must not author its arbiter's clock), the directory the close runs in
60
60
  * (the workdir — the tree it is judging), the exit code the SIGNED spec calls
61
61
  * success, the judgment-rendered signal, and the kept-failures pattern. All
62
62
  * five are arbiter territory: the drafted workflow config cannot express any.
63
+ * `closeDir` (PRD item 27/M3 Part B) is the close's OWN books directory —
64
+ * pristine copies, calibration thresholds, logs — deliberately outside the
65
+ * patient tree (the worker must never read the arbiter's books). Handed to
66
+ * the child as `BARELOOP_CLOSE_DIR` (`CLOSE_DIR_ENV_VAR`,
67
+ * src/close-integrity.js); a close script that needs one and gets none
68
+ * instrument-stops itself, by the script's own contract — this seam only
69
+ * ever THREADS the value, it never invents one.
63
70
  * @returns {Promise<{verdict: 'satisfied'|'needs_revision'|'failed'|'timed-out'|'killed'|'crashed',
64
71
  * gap?: string, exitCode?: number, signal?: string, detail?: string,
65
72
  * judgedCount?: number|null, unaudited?: boolean}>}
66
73
  */
67
- export function runClose(close: string[], redact?: (s: string) => string, { timeoutMs, cwd, expect, judged, gapKeep }?: {
74
+ export function runClose(close: string[], redact?: (s: string) => string, { timeoutMs, cwd, expect, judged, gapKeep, closeDir }?: {
68
75
  timeoutMs?: number;
69
76
  cwd?: string;
70
77
  expect?: number;
@@ -73,6 +80,7 @@ export function runClose(close: string[], redact?: (s: string) => string, { time
73
80
  min: number;
74
81
  };
75
82
  gapKeep?: string;
83
+ closeDir?: string | null;
76
84
  }): Promise<{
77
85
  verdict: "satisfied" | "needs_revision" | "failed" | "timed-out" | "killed" | "crashed";
78
86
  gap?: string;
@@ -104,12 +112,13 @@ export function runClose(close: string[], redact?: (s: string) => string, { time
104
112
  *
105
113
  * @param {any[]} stages the stage list (already validated by validateJob)
106
114
  * @param {(s: string) => string} [redact] scrub, applied at capture on EVERY stage
107
- * @param {{timeoutMs?: number, cwd?: string}} [opts]
115
+ * @param {{timeoutMs?: number, cwd?: string, closeDir?: string|null}} [opts]
108
116
  * @returns {Promise<any>} runClose's verdict shape, plus `stage` and `stages`
109
117
  */
110
- export function runStages(stages: any[], redact?: (s: string) => string, { timeoutMs, cwd }?: {
118
+ export function runStages(stages: any[], redact?: (s: string) => string, { timeoutMs, cwd, closeDir }?: {
111
119
  timeoutMs?: number;
112
120
  cwd?: string;
121
+ closeDir?: string | null;
113
122
  }): Promise<any>;
114
123
  /**
115
124
  * Bound an over-long close stream to a head sample + elided middle + tail. When
@@ -210,9 +219,13 @@ export function boundGap(s: string, keepPattern?: string): string;
210
219
  * it to the gate audit's allow-decision writes. Injected like `redact` so the shell
211
220
  * stays stdlib-only and dumb; consulted ONLY at a crashed verdict. With no seam, or
212
221
  * with zero writes, a crash stays what it always was: an instrument stop.
222
+ * @param {string|null} [opts.closeDir] the close's own books directory
223
+ * (PRD item 27/M3 Part B), forwarded to the raw `close` path's `runClose`
224
+ * call unchanged; a caller supplying `judge` instead already closed over
225
+ * its own `closeDir` and this param does nothing for it.
213
226
  * @returns {Promise<'green'|'escalated'>}
214
227
  */
215
- export function ralph({ middle, close, judge, capRuns, ladder, emit, redact, closeTimeoutMs, cwd, expect, judged, gapKeep, workerWrites }: {
228
+ export function ralph({ middle, close, judge, capRuns, ladder, emit, redact, closeTimeoutMs, cwd, expect, judged, gapKeep, workerWrites, closeDir }: {
216
229
  middle: (iteration: number, gap?: string) => void | Promise<void>;
217
230
  close?: string[] | undefined;
218
231
  judge?: (() => Promise<{
@@ -237,6 +250,7 @@ export function ralph({ middle, close, judge, capRuns, ladder, emit, redact, clo
237
250
  } | undefined;
238
251
  gapKeep?: string | undefined;
239
252
  workerWrites?: (() => string[]) | undefined;
253
+ closeDir?: string | null | undefined;
240
254
  }): Promise<"green" | "escalated">;
241
255
  /**
242
256
  * What the close child must NOT be able to read (operator ruling, 2026-07-31:
package/types/reuse.d.ts CHANGED
@@ -377,6 +377,7 @@ export function resumeTreeGate({ head, seed, dirty }: {
377
377
  * @param {number} [opts.capRuns] forwarded
378
378
  * @param {number} [opts.strikeLimit] forwarded (a plan step's strike ceiling)
379
379
  * @param {number} [opts.closeTimeoutMs] forwarded
380
+ * @param {string} [opts.closeDir] forwarded (PRD item 27/M3 Part B)
380
381
  * @param {boolean} [opts.layerRoot] forwarded
381
382
  * @param {boolean|'cap'|'diff'} [opts.readShim] the read shim's ARM (A0/A1/A2/A3), forwarded verbatim
382
383
  * @param {any} [opts.resume] RESUME (module C) — a `readResume` reading of the KILLED
@@ -411,6 +412,7 @@ export function runReuse(opts: {
411
412
  capRuns?: number | undefined;
412
413
  strikeLimit?: number | undefined;
413
414
  closeTimeoutMs?: number | undefined;
415
+ closeDir?: string | undefined;
414
416
  layerRoot?: boolean | undefined;
415
417
  readShim?: boolean | "cap" | "diff" | undefined;
416
418
  resume?: any;
@@ -40,6 +40,7 @@ export function doorAgeGate(door: any, { now, ttlMs }?: {
40
40
  * @param {string} opts.decision one of the three doors
41
41
  * @param {string|null} [opts.text] the rerun's words — the gap, and refused when empty
42
42
  * @param {number} [opts.closeTimeoutMs] the per-stage bound for the accept's re-run
43
+ * @param {string|null} [opts.closeDir] the close's own books directory (PRD item 27/M3 Part B), forwarded to the accept's re-run
43
44
  * @param {string|null} [opts.registryDir] the workflow registry, when the runner keeps one
44
45
  * @param {string|null} [opts.name] the workflow this run wrote its row against
45
46
  * @param {string|null} [opts.runid] the RUN's id, exactly as the bridge row records it
@@ -51,13 +52,14 @@ export function doorAgeGate(door: any, { now, ttlMs }?: {
51
52
  * doorRecorded: boolean, reds: any[], mechanical: any, door: any, text: string|null,
52
53
  * options: string[], ttlMs: number, note: string|null}>}
53
54
  */
54
- export function answerReviewDoor({ job, workdir, events, decision, text, closeTimeoutMs, registryDir, name, runid, at, now, ttlMs, emit, }: {
55
+ export function answerReviewDoor({ job, workdir, events, decision, text, closeTimeoutMs, closeDir, registryDir, name, runid, at, now, ttlMs, emit, }: {
55
56
  job: any;
56
57
  workdir: string;
57
58
  events: any[];
58
59
  decision: string;
59
60
  text?: string | null | undefined;
60
61
  closeTimeoutMs?: number | undefined;
62
+ closeDir?: string | null | undefined;
61
63
  registryDir?: string | null | undefined;
62
64
  name?: string | null | undefined;
63
65
  runid?: string | null | undefined;
package/types/run.d.ts CHANGED
@@ -45,7 +45,22 @@
45
45
  * CLOSE-FIX loop (a plan STEP runs under the strike ladder instead)
46
46
  * @param {number} [opts.strikeLimit] shell-owned strike ceiling for a plan step's ladder
47
47
  * @param {number} [opts.shellCapUsd] the shell's hard USD ceiling
48
- * @param {number} [opts.closeTimeoutMs] close wall-clock cap (shell territory)
48
+ * @param {number} [opts.closeTimeoutMs] close wall-clock cap (shell territory).
49
+ * OPTIONAL as of PRD item 27/M3: absent, the plan flow autosets it from a $0
50
+ * seed timing pass (or reads the spec's own signed `closeTimeoutMs`
51
+ * override) — a caller passing an explicit value here still wins outright
52
+ * (backward compatibility / test control), but production callers
53
+ * (`src/cli.js`, `scripts/run-u.mjs`) pass nothing (F133, run mtqwmb9l: a
54
+ * runner that resolves this itself for its own purposes — e.g. `run-u.mjs`
55
+ * sizing its outside watchdog, F67 — must never feed the number back in
56
+ * here too, or this call's own autoset/override banner prints a SECOND,
57
+ * mislabelled "explicit runner override" line on top of the runner's own
58
+ * correctly-labelled one).
59
+ * @param {string|null} [opts.closeDir] the close's own books directory (PRD item
60
+ * 27/M3 Part B) — pristine copies, calibration thresholds, logs — outside
61
+ * the patient tree. Forwarded to every close invocation as
62
+ * `BARELOOP_CLOSE_DIR`; a close script that needs one and gets none
63
+ * instrument-stops itself (`checkCloseDirRequired`, src/close-integrity.js).
49
64
  * @param {unknown} [opts.bridge] Layer 3 — a bridge-v1 entry to reuse as the drafter's
50
65
  * STARTING DRAFT (design record 2026-08-01, D4), forwarded to the plan flow. Omitted is
51
66
  * the cold path and is byte-identical to a pre-Layer-3 run. WHICH entry (the listing, an
@@ -141,13 +156,19 @@
141
156
  * @param {{text: string, fromRunid?: string|null, receivedAt?: string|null}|null} [opts.doorRerun]
142
157
  * MODULE 8 — this leg IS the fresh engagement a signer commissioned at a previous
143
158
  * run's review door, carrying their words. Forwarded verbatim.
159
+ * @param {boolean} [opts.resumable=true] PRD item 27(c)/F130 — does THIS runner
160
+ * support `--resume` at all? `run-u.mjs` does (the default, `true`, byte-identical
161
+ * to before this param existed); the exported bundle CLI does not (F130) and
162
+ * passes `false` so the escalation tail's option strings say "resume is
163
+ * `run-u`-only in v1" instead of naming a flag that would fail if typed. Forwarded
164
+ * verbatim to the plan flow, which owns the three `--resume`-naming readouts.
144
165
  * @returns {Promise<string>} outcome: 'green' | 'already-green' | 'escalated' |
145
166
  * 'unapproved-spec' | 'job-red' | 'smoke-red' | 'plan-red' | 'check-red' |
146
167
  * 'close-red' | 'close-unsupported' | 'recipe-stale' | 'branch-red' | 'pricing-red' | 'provider-red' |
147
168
  * 'interpreter-red' | 'cap-halt' | 'wall-halt' | 'step-stalled' |
148
169
  * 'hitl-pause' | 'hitl-decision-red' | `step-red:<id>`
149
170
  */
150
- export function runJob(rawSpec: object | string, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider, emit, capRuns, strikeLimit, shellCapUsd, closeTimeoutMs, layerRoot, readShim, scout, bridge, priorSpentUsd, priorSpendComplete, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun }: {
171
+ export function runJob(rawSpec: object | string, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider, emit, capRuns, strikeLimit, shellCapUsd, closeTimeoutMs, closeDir, layerRoot, readShim, scout, bridge, priorSpentUsd, priorSpendComplete, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun, resumable }: {
151
172
  approvals: unknown;
152
173
  workdir: string;
153
174
  provider: any;
@@ -164,6 +185,7 @@ export function runJob(rawSpec: object | string, { approvals, workdir, provider,
164
185
  strikeLimit?: number | undefined;
165
186
  shellCapUsd?: number | undefined;
166
187
  closeTimeoutMs?: number | undefined;
188
+ closeDir?: string | null | undefined;
167
189
  bridge?: unknown;
168
190
  priorSpentUsd?: number | undefined;
169
191
  priorSpendComplete?: boolean | undefined;
@@ -196,6 +218,7 @@ export function runJob(rawSpec: object | string, { approvals, workdir, provider,
196
218
  fromRunid?: string | null;
197
219
  receivedAt?: string | null;
198
220
  } | null | undefined;
221
+ resumable?: boolean | undefined;
199
222
  }): Promise<string>;
200
223
  /** @typedef {{code: string, path: string, detail?: string}} Red */
201
224
  /**