bareloop 0.19.1 → 0.21.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.
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Recompute the bundle hash from what is ACTUALLY on disk right now (never
3
+ * trusts the stored manifest). Throws only if `dir` itself does not exist —
4
+ * every other absence (a missing spec.json, an empty close/) folds into the
5
+ * hash as an omitted file, which is exactly what makes a swapped/deleted
6
+ * script change the hash.
7
+ * @param {string} dir bundle directory
8
+ * @returns {string} sha256 hex
9
+ */
10
+ export function bundleHash(dir: string): string;
11
+ /**
12
+ * EXPORT — mints a bundle directory from a signed job spec (`docs/product/
13
+ * EXPORT-BUILD.md`, "The bundle"). Every check below runs BEFORE anything
14
+ * touches disk (typed reds, nothing written): a partially-written bundle
15
+ * would be worse than no bundle, since `readBundle`'s tamper check has
16
+ * nothing trustworthy to compare against yet.
17
+ *
18
+ * Refusals, each a distinct red code:
19
+ * - `no-bridge-at-hash` — neither of the job's two possible bridge names —
20
+ * `spec.job` itself, or its SHAPE FORK `shapeForkName(spec.job,
21
+ * closeStagesOf(spec))` (the name `writeGreenRow`/`writeRunGreenRow` in
22
+ * `src/reuse.js` mint whenever a run's close-stage set differs from the
23
+ * base bridge's `closeStageNames` — same derivation, imported rather than
24
+ * re-spelled, so the two names cannot drift) — carries a recorded specHash
25
+ * (bridge-level or on some version) equal to `jobSpecHash(spec)`: nothing
26
+ * has ever greened THIS exact spec, so there is nothing proven to ship.
27
+ * Every candidate bridge that DOES exist (base and/or fork) still ships
28
+ * into `bridges/` — the spec says "the job's WHOLE registry history
29
+ * (greens and reds)", not just the one that matched.
30
+ * - `close-cmd-unrelocatable` — a `close[].cmd` is not `node <abs .mjs> …`.
31
+ * - `close-script-missing` — a cmd names a script `closeScripts` does not
32
+ * carry the source for.
33
+ * - `close-script-collision` — two different script paths share a basename;
34
+ * copying both into one flat `close/` directory would silently clobber one.
35
+ * - `close-import-unexported` — a close script imports a name from
36
+ * bareloop's own src that `src/index.js` does not export.
37
+ * - `close-import-unparsed` — a close script's import is a shape this
38
+ * module cannot verify at all (default import, `* as ns`, mixed
39
+ * default+named/namespace, a bare side-effect import, or a dynamic
40
+ * `import()`), or is a relative import that does not point into
41
+ * `src/` (a sibling file `close/` never ships, so it would be missing
42
+ * from the bundle). Fails safe rather than silently accepting what it
43
+ * cannot read.
44
+ * - `close-absolute-path` — a close script's source bakes in a string
45
+ * literal that is an absolute POSIX path which exists on the exporting
46
+ * machine and is outside the system-prefix allow-list (see
47
+ * `absolutePathLiteralsOf`): a close judges the cwd the runner gives it,
48
+ * never a path baked into the script — F8/F129, the live defect this
49
+ * guard would have refused at export time before any run.
50
+ * - `outdir-not-empty` — `outDir` exists and already holds files.
51
+ * - `registry-unreadable` — `registryDir` itself could not be read
52
+ * (bubbled from `loadRegistry`).
53
+ * @param {{ spec: any, closeScripts: Record<string, string>, registryDir: string, outDir: string, bareloopVersion: string }} o
54
+ * @returns {{ ok: boolean, reds: Red[], dir: string|null, bundleHash: string|null, manifest: any|null }}
55
+ */
56
+ export function exportBundle({ spec, closeScripts, registryDir, outDir, bareloopVersion }: {
57
+ spec: any;
58
+ closeScripts: Record<string, string>;
59
+ registryDir: string;
60
+ outDir: string;
61
+ bareloopVersion: string;
62
+ }): {
63
+ ok: boolean;
64
+ reds: Red[];
65
+ dir: string | null;
66
+ bundleHash: string | null;
67
+ manifest: any | null;
68
+ };
69
+ /**
70
+ * READ a bundle directory back: the manifest/spec, tamper-checked against
71
+ * what is actually on disk, plus the record-only bridges/blessing/history a
72
+ * runner needs beside them. Never throws — every failure is a typed red.
73
+ *
74
+ * `bundle-tampered` (N4) is a RED, never a warning, and runs whenever both a
75
+ * manifest and a spec could be read at all — a swapped close script must be
76
+ * caught here, before any caller reaches `runJob`.
77
+ * @param {string} dir bundle directory
78
+ * @returns {{ ok: boolean, reds: Red[], spec: any, manifest: any, bridges: any[], blessing: any, history: any[] }}
79
+ */
80
+ export function readBundle(dir: string): {
81
+ ok: boolean;
82
+ reds: Red[];
83
+ spec: any;
84
+ manifest: any;
85
+ bridges: any[];
86
+ blessing: any;
87
+ history: any[];
88
+ };
89
+ /**
90
+ * The IN-MEMORY `$BARELOOP_BUNDLE` substitution (the frozen spec's "two
91
+ * facts": the runner never touches the spec on disk, it substitutes the
92
+ * absolute bundle directory into a copy before `runJob`, and signs THAT
93
+ * substituted spec's hash — not the on-disk one). `approveHash` is the value
94
+ * an `--approve` flag / a `history.jsonl` row must record.
95
+ * @param {{ spec: any }} bundle a `readBundle` result (or anything carrying `.spec`)
96
+ * @param {string} bundleDir absolute or relative bundle directory
97
+ * @returns {{ spec: any, approveHash: string }}
98
+ */
99
+ export function resolveBundleSpec(bundle: {
100
+ spec: any;
101
+ }, bundleDir: string): {
102
+ spec: any;
103
+ approveHash: string;
104
+ };
105
+ /**
106
+ * TIGHTEN-ONLY envelope check (hamr Q8: raising the ceiling is a re-export,
107
+ * never a runner flag). Each GIVEN number must be ≤ the bundle spec's own —
108
+ * an absent field in the envelope trivially passes (nothing was asked to
109
+ * tighten), and a spec with no ceiling of its own for that field (only
110
+ * `maxWallMs` is ever absent — `budgetUsd` is required) has nothing to
111
+ * widen, so a given number passes there too.
112
+ * @param {any} spec the (already `$BARELOOP_BUNDLE`-resolved) bundle spec
113
+ * @param {{ budgetUsd?: number, maxWallMs?: number }} envelope
114
+ * @returns {{ ok: boolean, reds: Red[] }}
115
+ */
116
+ export function checkEnvelope(spec: any, envelope?: {
117
+ budgetUsd?: number;
118
+ maxWallMs?: number;
119
+ }): {
120
+ ok: boolean;
121
+ reds: Red[];
122
+ };
123
+ /**
124
+ * Write `blessing.json`: the FIRST GREEN RUN on the importer's own machine
125
+ * signs the bundle as trustworthy (hamr Q2), so later runs need no
126
+ * `--approve` (hamr: "no-resign"). Overwrites any prior blessing — there is
127
+ * only ever one, for the bundle's current `bundleHash`.
128
+ * @param {string} dir bundle directory
129
+ * @param {{ bundleHash: string, runid: string, outcome: string, host?: string, blessedAt?: string }} record
130
+ * @returns {any} the blessing object written
131
+ */
132
+ export function bless(dir: string, record: {
133
+ bundleHash: string;
134
+ runid: string;
135
+ outcome: string;
136
+ host?: string;
137
+ blessedAt?: string;
138
+ }): any;
139
+ /**
140
+ * Read `blessing.json` back and judge it against the bundle's CURRENT
141
+ * manifest hash. Three outcomes, never conflated: no blessing at all
142
+ * (`unblessed: true` — the bundle has simply never greened here, not a red);
143
+ * a blessing whose hash no longer matches the manifest (`blessing-stale` —
144
+ * the bundle was re-exported/edited since the run that blessed it); a match.
145
+ * @param {{ manifest: any, blessing: any }} bundle a `readBundle` result
146
+ * @returns {{ ok: boolean, unblessed: boolean, reds: Red[] }}
147
+ */
148
+ export function verifyBlessing(bundle: {
149
+ manifest: any;
150
+ blessing: any;
151
+ }): {
152
+ ok: boolean;
153
+ unblessed: boolean;
154
+ reds: Red[];
155
+ };
156
+ /**
157
+ * F128 preflight — can this bundle's close scripts actually resolve `import
158
+ * ... from 'bareloop'` at runtime? The bundle's own `package.json` declares
159
+ * `bareloop` as ITS dependency (`npm install` must be run INSIDE the bundle
160
+ * directory — validation step 2 was corrected 2026-09-06, see the dated
161
+ * addendum in `docs/product/EXPORT-BUILD.md`); nothing installs it for the
162
+ * bundle as a side effect of the bundle being installed as someone else's
163
+ * dependency. Left unchecked, a missing `node_modules` crashes every close
164
+ * stage deep inside the close-first precheck with a bare
165
+ * `ERR_MODULE_NOT_FOUND`, which surfaces as a generic `close-red` with no
166
+ * cure line (the live defect this fixes) — this check runs first and fails
167
+ * fast with the exact command to run.
168
+ *
169
+ * Resolves the bare specifier `'bareloop'` exactly as a close script would,
170
+ * from inside the bundle's own `close/` directory (the probe path itself
171
+ * need not exist — `createRequire`/`resolve` only use it to anchor the
172
+ * `node_modules` walk). Going through `require.resolve` also walks
173
+ * `bareloop`'s own `exports` map (the package root, `"."` -> `./src/
174
+ * index.js`), so a bare successful resolve already proves a real,
175
+ * exports-map-shaped `bareloop` package is reachable from THIS bundle's own
176
+ * dependency chain — not some unrelated global install. Any resolution
177
+ * counts (a direct `node_modules/bareloop`, a symlink into it, or a parent
178
+ * `node_modules/bareloop` up the directory chain) — it only has to resolve.
179
+ * @param {string} bundleDir
180
+ * @returns {{ ok: boolean, reds: Red[] }}
181
+ */
182
+ export function checkBundleDeps(bundleDir: string): {
183
+ ok: boolean;
184
+ reds: Red[];
185
+ };
186
+ /**
187
+ * Append one row to `history.jsonl` — one `bareloop run` on this machine per
188
+ * line. Same convention as `spine.js`/`ledger.js`: `appendFileSync` opens
189
+ * with O_APPEND, so one line under PIPE_BUF is written atomically with
190
+ * respect to other appenders; there is no read-modify-write here to race.
191
+ * @param {string} dir bundle directory
192
+ * @param {any} row
193
+ */
194
+ export function appendHistory(dir: string, row: any): void;
195
+ export type Red = {
196
+ code: string;
197
+ path: string;
198
+ detail?: string;
199
+ };
package/types/cli.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The one entry point. Never calls `process.exit` — always returns an exit
3
+ * code, so `bin/bareloop.mjs` can set `process.exitCode` and let node flush
4
+ * queued stdout on its own (F-something: `process.exit()` can discard it).
5
+ * @param {string[]} argv
6
+ * @param {{ env?: Record<string,string|undefined>, stdout?: any, stderr?: any, cwd?: string, provider?: any, providerFor?: any, judgeProvider?: any, now?: () => number, stdin?: any }} deps
7
+ * @returns {Promise<number>}
8
+ */
9
+ export function main(argv: string[], deps?: {
10
+ env?: Record<string, string | undefined>;
11
+ stdout?: any;
12
+ stderr?: any;
13
+ cwd?: string;
14
+ provider?: any;
15
+ providerFor?: any;
16
+ judgeProvider?: any;
17
+ now?: () => number;
18
+ stdin?: any;
19
+ }): Promise<number>;
package/types/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { makeSpine } from "./spine.js";
2
2
  export { runPlan } from "./planrun.js";
3
+ export { main as cliMain } from "./cli.js";
3
4
  export { ralph, runClose, runStages, CLOSE_FAULTS } from "./ralph.js";
4
5
  export { globToPrefix, scanSecrets } from "./validate.js";
5
6
  export { validateJob, jobSpecHash, checkApproval, checkMenu, resolveWorkerModel, CLOSE_TYPES, CLASS_BY_CLOSE, GOLD_COMPARE, CADENCE_UNITS, PROVIDERS, CONDITION_KEYS, TOOL_MENU, LOCKED_TOOLS, STORE_VERBS, VERDICT_TYPES, LOCKED_VERDICTS } from "./job.js";
@@ -8,7 +9,7 @@ export { snapshotScope, evalExits } from "./exits.js";
8
9
  export { runJob, ACCOUNTED_ROUND_TYPES } from "./run.js";
9
10
  export { BRIDGE_SCHEMA, validateBridge, deriveStatus, listingRow, loadGate, mintBridge, appendGreen, appendRed, loadBridge, loadRegistry, saveBridge, makeRegistry, registryExists, QUARANTINED_VERDICTS, QUARANTINED_CODE, quarantinesCredit, newestEligibleVersion, reuseEligibility, recordDoor } from "./bridges.js";
10
11
  export { renderListing, selectionPrompt } from "./selection.js";
11
- export { validateEnvelope, resolveTrySpec, resolveReuse, reuseSpecHash, selectBridge, runReuse, REUSE_GRADED_RED, readResume, resumeTreeGate, CHECKPOINT_OUTCOMES, PAUSE_TTL_MS, checkpointAgeGate, applyDoorDecision, writeRunGreenRow } from "./reuse.js";
12
+ export { validateEnvelope, resolveTrySpec, resolveReuse, reuseSpecHash, selectBridge, runReuse, REUSE_GRADED_RED, readResume, resumeTreeGate, CHECKPOINT_OUTCOMES, PAUSE_TTL_MS, checkpointAgeGate, applyDoorDecision, writeRunGreenRow, shapeForkName } from "./reuse.js";
12
13
  export { classifyIncidents, foldLedger, ledgerDeltas, updateLedger, LEDGER_CLASSES, VOUCHED_RATE_SOURCES, rateProvenance, spendProvenance } from "./ledger.js";
13
14
  export { workBranchName, prepareWorkBranch, WORK_BRANCH_PREFIX, WORK_BRANCH_RE } from "./workbranch.js";
14
15
  export { createReadShim, wrapReadTool, readShimArm, readShimStrategy, READ_SHIM_CAP, READ_SHIM_STRATEGY, READ_SHIM_DIFF_STRATEGY, READ_SHIM_ARMS } from "./readshim.js";
@@ -26,3 +27,4 @@ export { runBehaviour, formatBehaviour } from "./behaviour.js";
26
27
  export { replayRun, formatReplay } from "./replay.js";
27
28
  export { runInterview, authorCloseForJob, assembleSpec, prepareSigning, refusalEvents, refuseLockedKind, GENRE, REFUSAL_LIB, REFUSAL_CATEGORY, AUTHORED_SPEC_FIELDS } from "./authorjob.js";
28
29
  export { PROMPT_REGISTERS, isPromptFile } from "./promptregisters.js";
30
+ export { exportBundle, bundleHash, readBundle, resolveBundleSpec, checkEnvelope, bless, verifyBlessing, appendHistory, checkBundleDeps } from "./bundle.js";
@@ -223,6 +223,17 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
223
223
  * it.
224
224
  * @param {number} [opts.closeTimeoutMs] close/check wall-clock cap (shell territory)
225
225
  * @param {number} [opts.maxStepRounds] the shell's per-step rounds ceiling (validatePlan's bound)
226
+ * @param {boolean} [opts.scout=true] operator-only switch (docs/product/SCOUT-CONTRAST.md):
227
+ * `false` on a FRESH run (no `resumeSeed`) skips the survey entirely — `scoutBlob`
228
+ * stays `''`, no `scout-start`/`scout-result`/`scout-empty` is emitted, and a single
229
+ * `scout-skipped {reason:'operator-off'}` records it (a RECORD, never silence — the
230
+ * resume skip's own rule). The planner then drafts from `(no scout notes)`
231
+ * (line ~587) exactly as it does today when a scout returns an empty blob — OFF is
232
+ * byte-identical to today's run everywhere but the survey. Precedence: when
233
+ * `resumeSeed` is set the existing resume skip fires first and wins its own reason
234
+ * (`resumed`); `operator-off` is only ever emitted on a fresh run. A non-boolean
235
+ * THROWS a TypeError at entry (the `readShimArm` guard class) — a knob that
236
+ * silently coerced would mislabel a bench row.
226
237
  * @param {number} [opts.scoutRounds] the read-only survey's round bound (F59: the LAST round is
227
238
  * reserved — a scout that spends every round on tools gets one toolless round to write its
228
239
  * survey, because the bound halts it mid-tool-use and text is its only deliverable)
@@ -386,7 +397,7 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
386
397
  * 'branch-red' | 'cap-halt' | 'wall-halt' | 'provider-red' | 'interpreter-red' |
387
398
  * 'step-stalled' | 'hitl-pause' | 'hitl-decision-red' | `step-red:<id>`
388
399
  */
389
- export function runPlan(job: any, { workdir, provider, nativeProvider, providerFor, judgeProvider, emit, remainingUsd, isUnpriced, spendComplete, capRuns, strikeLimit, closeTimeoutMs, maxStepRounds, layerRoot, readShim, scoutRounds, bridge, now, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, priorSpentUsd, reviewDoor, doorRerun }: {
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 }: {
390
401
  workdir: string;
391
402
  provider: any;
392
403
  nativeProvider?: ((o: {
@@ -404,6 +415,7 @@ export function runPlan(job: any, { workdir, provider, nativeProvider, providerF
404
415
  strikeLimit?: number | undefined;
405
416
  closeTimeoutMs?: number | undefined;
406
417
  maxStepRounds?: number | undefined;
418
+ scout?: boolean | undefined;
407
419
  scoutRounds?: number | undefined;
408
420
  readShim?: boolean | "cap" | "diff" | undefined;
409
421
  layerRoot?: boolean | undefined;
package/types/reuse.d.ts CHANGED
@@ -1,3 +1,17 @@
1
+ /**
2
+ * The name a cold green FORKS to when the entry of the job's own name greened a
3
+ * different close. Derived from the shape itself, so it is deterministic: the same
4
+ * close always lands in the same file, and the next green of that shape appends there
5
+ * instead of minting a third entry.
6
+ *
7
+ * The separator is a HYPHEN, not the `~` first proposed: the registry name IS the
8
+ * filename and `validateBridge` holds it to the same kebab alphabet the job validator
9
+ * uses (`^[a-z0-9][a-z0-9-]*$`), so a `~` name would be refused by the entry's own
10
+ * validator and the green would be lost — which is the exact failure this fork exists
11
+ * to prevent.
12
+ * @param {string} name @param {string[]} stageNames @returns {string}
13
+ */
14
+ export function shapeForkName(name: string, stageNames: string[]): string;
1
15
  /**
2
16
  * Validate the reuse envelope — the three numbers the operator signs before the run.
3
17
  *
package/types/run.d.ts CHANGED
@@ -110,6 +110,12 @@
110
110
  * frozen A0 baseline arm has to be exactly today, so a guard firing under a disabled
111
111
  * shim would make the baseline a treatment arm. The default-flip belongs to a
112
112
  * paid contrast nobody has approved yet (the `layerRoot` precedent, F41).
113
+ * @param {boolean} [opts.scout=true] operator-only switch (docs/product/SCOUT-CONTRAST.md),
114
+ * forwarded verbatim to the plan flow: `false` on a fresh run (no `resumeSeed`) skips the
115
+ * read-only survey and records `scout-skipped {reason:'operator-off'}` instead — the
116
+ * planner drafts from `(no scout notes)`. Runner territory exactly like `readShim`: the
117
+ * spec names no scout, so the signed hash is unaffected. A non-boolean THROWS at this
118
+ * entry, before the ledger, before the approval gate (the `readShim` guard class).
113
119
  * @param {boolean} [opts.layerRoot=false] Layer R (within-run ratchet) — shell
114
120
  * territory, threaded to the plan flow. Defaults OFF
115
121
  * (decided 2026-07-21): fixation is extinct on every current job (F41), so
@@ -141,7 +147,7 @@
141
147
  * 'interpreter-red' | 'cap-halt' | 'wall-halt' | 'step-stalled' |
142
148
  * 'hitl-pause' | 'hitl-decision-red' | `step-red:<id>`
143
149
  */
144
- export function runJob(rawSpec: object | string, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider, emit, capRuns, strikeLimit, shellCapUsd, closeTimeoutMs, layerRoot, readShim, bridge, priorSpentUsd, priorSpendComplete, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun }: {
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 }: {
145
151
  approvals: unknown;
146
152
  workdir: string;
147
153
  provider: any;
@@ -173,6 +179,7 @@ export function runJob(rawSpec: object | string, { approvals, workdir, provider,
173
179
  } | null | undefined;
174
180
  resumeBranch?: string | null | undefined;
175
181
  readShim?: boolean | "cap" | "diff" | undefined;
182
+ scout?: boolean | undefined;
176
183
  layerRoot?: boolean | undefined;
177
184
  humanRuling?: {
178
185
  decision: string;