bareloop 0.21.0 → 0.23.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/run.js CHANGED
@@ -106,7 +106,22 @@ async function primitiveSmoke(workdir) {
106
106
  * CLOSE-FIX loop (a plan STEP runs under the strike ladder instead)
107
107
  * @param {number} [opts.strikeLimit] shell-owned strike ceiling for a plan step's ladder
108
108
  * @param {number} [opts.shellCapUsd] the shell's hard USD ceiling
109
- * @param {number} [opts.closeTimeoutMs] close wall-clock cap (shell territory)
109
+ * @param {number} [opts.closeTimeoutMs] close wall-clock cap (shell territory).
110
+ * OPTIONAL as of PRD item 27/M3: absent, the plan flow autosets it from a $0
111
+ * seed timing pass (or reads the spec's own signed `closeTimeoutMs`
112
+ * override) — a caller passing an explicit value here still wins outright
113
+ * (backward compatibility / test control), but production callers
114
+ * (`src/cli.js`, `scripts/run-u.mjs`) pass nothing (F133, run mtqwmb9l: a
115
+ * runner that resolves this itself for its own purposes — e.g. `run-u.mjs`
116
+ * sizing its outside watchdog, F67 — must never feed the number back in
117
+ * here too, or this call's own autoset/override banner prints a SECOND,
118
+ * mislabelled "explicit runner override" line on top of the runner's own
119
+ * correctly-labelled one).
120
+ * @param {string|null} [opts.closeDir] the close's own books directory (PRD item
121
+ * 27/M3 Part B) — pristine copies, calibration thresholds, logs — outside
122
+ * the patient tree. Forwarded to every close invocation as
123
+ * `BARELOOP_CLOSE_DIR`; a close script that needs one and gets none
124
+ * instrument-stops itself (`checkCloseDirRequired`, src/close-integrity.js).
110
125
  * @param {unknown} [opts.bridge] Layer 3 — a bridge-v1 entry to reuse as the drafter's
111
126
  * STARTING DRAFT (design record 2026-08-01, D4), forwarded to the plan flow. Omitted is
112
127
  * the cold path and is byte-identical to a pre-Layer-3 run. WHICH entry (the listing, an
@@ -202,13 +217,27 @@ async function primitiveSmoke(workdir) {
202
217
  * @param {{text: string, fromRunid?: string|null, receivedAt?: string|null}|null} [opts.doorRerun]
203
218
  * MODULE 8 — this leg IS the fresh engagement a signer commissioned at a previous
204
219
  * run's review door, carrying their words. Forwarded verbatim.
220
+ * @param {boolean} [opts.resumable=true] PRD item 27(c)/F130 — does THIS runner
221
+ * support `--resume` at all? `run-u.mjs` does (the default, `true`, byte-identical
222
+ * to before this param existed); the exported bundle CLI does not (F130) and
223
+ * passes `false` so the escalation tail's option strings say "resume is
224
+ * `run-u`-only in v1" instead of naming a flag that would fail if typed. Forwarded
225
+ * verbatim to the plan flow, which owns the three `--resume`-naming readouts.
205
226
  * @returns {Promise<string>} outcome: 'green' | 'already-green' | 'escalated' |
206
227
  * 'unapproved-spec' | 'job-red' | 'smoke-red' | 'plan-red' | 'check-red' |
207
228
  * 'close-red' | 'close-unsupported' | 'recipe-stale' | 'branch-red' | 'pricing-red' | 'provider-red' |
208
229
  * 'interpreter-red' | 'cap-halt' | 'wall-halt' | 'step-stalled' |
209
230
  * 'hitl-pause' | 'hitl-decision-red' | `step-red:<id>`
231
+ * NOTE: `job-end` can ALSO land with outcome 'runner-drained' (F140/PRD item
232
+ * 28(c)) — the `beforeExit` backstop's own terminal, emitted from OUTSIDE this
233
+ * function's normal control flow when the process drains with no `job-end` at
234
+ * all (a provider promise that never settled). It never appears as this
235
+ * function's own RETURN value (nothing is left to receive it), only as the
236
+ * spine's terminal record. Non-zero exit, `spendComplete:false` always, and
237
+ * deliberately NOT in the resumable/checkpoint set (`src/reuse.js`) — an
238
+ * unknown in-flight state is not a known-safe resume point.
210
239
  */
211
- export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider = null, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, layerRoot = false, readShim = false, scout = true, bridge = null, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null, heldRuling = null, reviewDoor = null, doorRerun = null }) {
240
+ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider = null, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, closeDir = null, layerRoot = false, readShim = false, scout = true, bridge = null, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null, heldRuling = null, reviewDoor = null, doorRerun = null, resumable = true }) {
212
241
  // THE READ SHIM's ARM, resolved at the door — the FIRST thing this entry does,
213
242
  // before the ledger, before the approval gate, before a byte of the spec is read.
214
243
  // An unrecognised spelling throws here at zero cost instead of being coerced by
@@ -324,6 +353,15 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
324
353
  // than guessing.
325
354
  verdictType: job.verdictType,
326
355
  ...(typeof provider?.model === 'string' ? { model: provider.model } : {}),
356
+ // PRD item 28 / F156 — WHICH provider actually ran, and against which
357
+ // endpoint. Report-only, but without it an archived spine cannot be
358
+ // segmented by provider at all: every cost, duration and outcome row from
359
+ // the day openai-api joined the menu would pool anthropic and non-anthropic
360
+ // runs into one contaminated aggregate, which is the reading error this repo
361
+ // has already paid for once. `baseUrl` rides along only when the job set one
362
+ // (an absent baseUrl is the vendor's own host, not an unknown).
363
+ provider: typeof job.provider === 'string' ? job.provider : null,
364
+ ...(typeof job.baseUrl === 'string' ? { baseUrl: job.baseUrl } : {}),
327
365
  // F118: replay reads this; report-only. `dirty` is deliberately omitted —
328
366
  // it can't be known without a shell, and null = unknown, never faked.
329
367
  code: (() => { const { version, sha } = codeVersion(); return { version, sha }; })(),
@@ -359,12 +397,46 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
359
397
  : {}),
360
398
  });
361
399
 
400
+ // F140/PRD item 28 (c) — the backstop that fires on the ABSENCE of a
401
+ // `job-end`. A provider promise that never settles (BA-25's class; 0.42.0
402
+ // closed the known drop cases per F141 but the class itself is "an
403
+ // instrument fires on absence", not a specific bug) lets node drain with
404
+ // nothing else holding the event loop, and the process exits with no
405
+ // terminal record at all — no escalation, no outcome, no way to tell a
406
+ // silent drain from an operator kill. `ended` is the one flag every
407
+ // job-end site sets via the local `end()` helper below (never a second
408
+ // spelling of "did we finish" that could fall out of sync with the real
409
+ // emits). One `beforeExit` listener per call, removed in the `finally` so
410
+ // multiple runs in one process (tests included) never leak listeners.
411
+ let ended = false;
412
+ /** @type {(outcome: string, extra?: object) => object} */
413
+ const end = (outcome, extra = {}) => {
414
+ ended = true;
415
+ return emit('job-end', { outcome, ...extra });
416
+ };
417
+ const onBeforeExit = () => {
418
+ if (ended) return;
419
+ // A drained run's in-flight spend is unknowable — the floor already
420
+ // accumulated is honest, an exact-looking total would not be (F6).
421
+ end('runner-drained', {
422
+ ...spend(),
423
+ spendComplete: false,
424
+ detail: 'process drained with no job-end — a provider call never settled (F140)',
425
+ });
426
+ // Never process.exit() (can discard queued stdout) — set the code and
427
+ // let node exit on its own; preserve a more specific code if one is
428
+ // already set.
429
+ process.exitCode = process.exitCode || 1;
430
+ };
431
+ process.once('beforeExit', onBeforeExit);
432
+
433
+ try {
362
434
  // 2. known-answer smoke before tokens (A3: silent degradation throws nothing)
363
435
  const smoke = await primitiveSmoke(workdir);
364
436
  emit('primitive-smoke', smoke);
365
437
  if (!smoke.ok) {
366
438
  emit('escalation', { category: 'smoke-red', decisionReady: true, decision: `The ${smoke.primitive} primitive failed its known-answer check — no run verdict is trustworthy on a degraded primitive.`, options: ['fix the primitive/store', 'abandon the run'], detail: smoke.detail });
367
- emit('job-end', { outcome: 'smoke-red', ...spend() });
439
+ end('smoke-red', spend());
368
440
  return 'smoke-red';
369
441
  }
370
442
 
@@ -405,7 +477,7 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
405
477
  };
406
478
  const pricingRed = () => {
407
479
  emit('escalation', { category: 'pricing-red', decisionReady: true, decision: 'A provider result carried no priced cost — the hard cap cannot govern spend it cannot see (unpriced is never free, F6).', options: ['bind a priced provider/model', 'abandon the run'], spentUsd });
408
- emit('job-end', { outcome: 'pricing-red', ...spend() });
480
+ end('pricing-red', spend());
409
481
  return 'pricing-red';
410
482
  };
411
483
 
@@ -415,14 +487,14 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
415
487
  // accounts it natively (F12) and the job-end money contract is unchanged.
416
488
  {
417
489
  const outcome = await runPlan(job, {
418
- workdir, provider, nativeProvider, providerFor, judgeProvider, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, layerRoot, readShim, scout, bridge, priorWallMs: chainWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun, priorSpentUsd: chainFoldUsd,
490
+ workdir, provider, nativeProvider, providerFor, judgeProvider, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, closeDir, layerRoot, readShim, scout, bridge, priorWallMs: chainWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun, priorSpentUsd: chainFoldUsd, resumable,
419
491
  remainingUsd: () => Math.min(shellCapUsd, job.budgetUsd - spentUsd),
420
492
  isUnpriced: () => unpriced, // F6: let the plan flow bail in-flight, not just after it returns
421
493
  spendComplete, // …and let its money-halt readout say whether the remaining it quotes is exact
422
494
  });
423
495
  if (unpriced) return pricingRed();
424
496
  if (outcome.startsWith('step-red:')) {
425
- emit('job-end', { outcome: 'step-red', step: outcome.slice('step-red:'.length), ...spend() });
497
+ end('step-red', { step: outcome.slice('step-red:'.length), ...spend() });
426
498
  } else if (outcome === 'provider-red' || outcome === 'step-stalled') {
427
499
  // F44: a transport-throw provider-red never returned a usage figure for the
428
500
  // failed call, so the priced sum is a FLOOR, not the total — spendComplete
@@ -432,10 +504,13 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
432
504
  // the known cost of self-heal — and an unbilled reissue is indistinguishable
433
505
  // from a billed one from here. Reporting the priced sum as exact would be F6
434
506
  // in a self-heal coat.
435
- emit('job-end', { outcome, ...spend(), spendComplete: false });
507
+ end(outcome, { ...spend(), spendComplete: false });
436
508
  } else {
437
- emit('job-end', { outcome, ...spend() });
509
+ end(outcome, spend());
438
510
  }
439
511
  return outcome;
440
512
  }
513
+ } finally {
514
+ process.removeListener('beforeExit', onBeforeExit);
515
+ }
441
516
  }
package/src/transport.js CHANGED
@@ -3,14 +3,18 @@
3
3
  //
4
4
  // A transport failure is fetch THROWING — the request never produced an HTTP
5
5
  // response at all (a TLS fault mid-record, a reset socket, a DNS/network
6
- // failure). That is a DIFFERENT unknown from an HTTP response the provider
7
- // DID send (4xx/5xx/429): a response means the provider answered, so
8
- // bare-agent's own retry policy for those stays exactly what it is — this
9
- // module never widens that. Retrying only the no-response case is the whole
10
- // point: a call that dies mid-read may already have been billed (retry can
11
- // pay twice), so the retry budget stays as tight as hamr authorized it —
12
- // exactly one extra attempt, never configurable upward from a job spec or
13
- // argv (tighten-only doctrine).
6
+ // failure) — OR bare-agent's own BA-25 `guardResponseSettles` rejecting a
7
+ // response cut after headers but before the body completed (a peer
8
+ // `'aborted'`/`'error'`/premature `'close'`), tagged `context.bound:
9
+ // 'transport'` (bare-agent 0.42.0, node_modules/bare-agent/src/provider-http.js).
10
+ // That is a DIFFERENT unknown from an HTTP response the provider DID send
11
+ // (4xx/5xx/429): a response means the provider answered, so bare-agent's own
12
+ // retry policy for those stays exactly what it is — this module never widens
13
+ // that. Retrying only the no-response case is the whole point: a call that
14
+ // dies mid-read may already have been billed (retry can pay twice), so the
15
+ // retry budget stays as tight as hamr authorized it — exactly one extra
16
+ // attempt, never configurable upward from a job spec or argv (tighten-only
17
+ // doctrine).
14
18
  //
15
19
  // Scope: this module wires ONE seam — the worker Loop built in
16
20
  // src/planrun.js's `newLoop` (~line 2239), which is the ONE factory behind
@@ -31,11 +35,17 @@ export const TRANSPORT_MAX_ATTEMPTS = TRANSPORT_RETRIES + 1;
31
35
  * Is `err` a transport-class failure — the request never produced an HTTP
32
36
  * response? True for: a TLS fault mid-record ("bad record mac" and kin), a
33
37
  * reset/broken/timed-out socket (ECONNRESET, EPIPE, ETIMEDOUT, ECONNREFUSED,
34
- * ENETUNREACH — by code on the error itself or on `err.cause`), and
35
- * `fetch failed` wrapping a network `cause`. False for anything carrying an
36
- * HTTP status (`err.status`/`err.statusCode`, 4xx/5xx/429) or an explicit
38
+ * ENETUNREACH — by code on the error itself or on `err.cause`), `fetch
39
+ * failed` wrapping a network `cause`, and — as of bare-agent 0.42.0 (BA-25)
40
+ * — a `ProviderError` whose `err.context.bound === 'transport'`: the
41
+ * response stream ended (`'aborted'`/`'error'`/premature `'close'`) after
42
+ * headers but before the body completed, which bare-agent itself rejects as
43
+ * retryable with no HTTP status. False for anything carrying an HTTP status
44
+ * (`err.status`/`err.statusCode`, 4xx/5xx/429) or an explicit
37
45
  * `err.retryable === false` — those are provider RESPONSES, not transport
38
- * casualties, and bare-agent's own policy governs them unchanged.
46
+ * casualties, and bare-agent's own policy governs them unchanged; both
47
+ * guards run BEFORE the `context.bound` check, so a status or an explicit
48
+ * `retryable:false` wins even if `bound` says 'transport'.
39
49
  * @param {any} err
40
50
  * @returns {boolean}
41
51
  */
@@ -47,6 +57,13 @@ export function isTransportFailure(err) {
47
57
  if (typeof status === 'number') return false;
48
58
  if (err.retryable === false) return false;
49
59
 
60
+ // BA-25 (bare-agent 0.42.0): bare-agent's own transport classification —
61
+ // `guardResponseSettles` tags a body-cut-after-headers rejection with
62
+ // `context.bound: 'transport'`. Only 'transport' qualifies here — a
63
+ // sibling bound like 'idle' (BA-19's deadline-idle guard) already
64
+ // qualifies via its ETIMEDOUT code below, never by bound alone.
65
+ if (err.context && err.context.bound === 'transport') return true;
66
+
50
67
  const codeOf = (e) => (e && typeof e.code === 'string' ? e.code : null);
51
68
  const TRANSPORT_CODES = new Set(['ECONNRESET', 'EPIPE', 'ETIMEDOUT', 'ECONNREFUSED', 'ENETUNREACH', 'EAI_AGAIN']);
52
69
  const code = codeOf(err) ?? codeOf(err.cause);
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.
@@ -91,7 +91,7 @@ export function defaultSurveyor({ workdir, granted, auditPath, ctx }: {
91
91
  *
92
92
  * @param {{workdir: string, provider?: any,
93
93
  * rounds?: number, minBytes?: number, blobMax?: number, maxTokens?: number, ctx?: boolean,
94
- * attempts?: number, ceilingUsd?: number|null,
94
+ * attempts?: number, ceilingUsd?: number|null, callTimeoutMs?: number,
95
95
  * onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void,
96
96
  * createLoop?: (o: {system: string, policy: any, onLlmResult: any, provider: any}) => any,
97
97
  * createSurveyor?: (o: {workdir: string, granted: readonly string[], ctx: boolean}) => Promise<any>}} o
@@ -99,7 +99,7 @@ export function defaultSurveyor({ workdir, granted, auditPath, ctx }: {
99
99
  * calls: {label: string, costUsd: number|null, unpricedRounds: number}[],
100
100
  * budgetStop: 'cap-halt'|'pricing-red'|null}>}
101
101
  */
102
- export function runAuthorScout({ workdir, provider, rounds, minBytes, blobMax, maxTokens, ctx, attempts, ceilingUsd, onCall, createLoop, createSurveyor, }: {
102
+ export function runAuthorScout({ workdir, provider, rounds, minBytes, blobMax, maxTokens, ctx, attempts, ceilingUsd, callTimeoutMs, onCall, createLoop, createSurveyor, }: {
103
103
  workdir: string;
104
104
  provider?: any;
105
105
  rounds?: number;
@@ -109,6 +109,7 @@ export function runAuthorScout({ workdir, provider, rounds, minBytes, blobMax, m
109
109
  ctx?: boolean;
110
110
  attempts?: number;
111
111
  ceilingUsd?: number | null;
112
+ callTimeoutMs?: number;
112
113
  onCall?: (call: {
113
114
  label: string;
114
115
  costUsd: number | null;
@@ -205,6 +206,30 @@ export function buildSeedListing({ workdir, seedRef, sourcePaths, testPaths, lis
205
206
  tier: string;
206
207
  };
207
208
  }>;
209
+ /**
210
+ * F152/PRD 30.4 — the scout's per-call IDLE bound. Every `loop.run()` in this
211
+ * module carried NO time bound at all: a live endpoint that accepts and never
212
+ * answers hangs the process forever (measured) — an open socket is an active
213
+ * handle, so node never drains and no backstop can fire.
214
+ *
215
+ * UNLIKE the run path (`callBounds()`, src/planrun.js:1518), this pipeline has
216
+ * NO wall clock to derive a bound FROM — the authoring pipeline is genuinely
217
+ * clockless (`src/authorjob.js`/`src/authorflow.js`/`scripts/run-author.mjs`
218
+ * carry a money ceiling, `ceilingUsd`/`capStop`, and never a `maxWallMs`).
219
+ * Rather than inventing a fresh number, this reuses the EXACT ceiling the run
220
+ * path itself falls back to when ITS OWN wall is unbounded
221
+ * (`PROVIDER_TIMEOUT_MS`, `src/clock.js` — BA-18's idle timeout, already an
222
+ * approved "no wall present" default one layer up): an authoring-time call is
223
+ * the identical situation, one call earlier.
224
+ *
225
+ * It bounds SOCKET INACTIVITY only (reset by every byte, per BA-18) — a call
226
+ * that is merely slow keeps streaming and is untouched; only a truly silent
227
+ * endpoint trips it. TIGHTEN-ONLY, like every cap in this file: this is an
228
+ * operator-set ceiling fixed here, not a param a caller or a signed spec may
229
+ * widen (a defaulted cap is a silent second ceiling, and there is no clock
230
+ * whose narrower reading could ever tighten it further).
231
+ */
232
+ export const AUTHOR_CALL_TIMEOUT_MS: 600000;
208
233
  /** src/planrun.js:42 — the survey's hard round bound. */
209
234
  export const AUTHOR_SCOUT_ROUNDS: 8;
210
235
  /** src/planrun.js:44 — the survey is a PROMPT INGREDIENT for the authoring call. */
@@ -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
+ };