bareloop 0.20.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/CHANGELOG.md CHANGED
@@ -5,6 +5,240 @@ All notable changes to bareloop are documented here. Format:
5
5
  [SemVer](https://semver.org/spec/v2.0.0.html). Pre-1.0: **minor** = a ladder rung or
6
6
  feature lands, **patch** = docs, fixes, scaffolding.
7
7
 
8
+ ## [0.22.0] — 2026-09-07
9
+
10
+ ### Added
11
+
12
+ - **Close integrity M1** (`docs/product/CLOSE-INTEGRITY-BUILD.md`, PRD item 27, F129/F131).
13
+ - `src/close-integrity.js`: the ONE `close-absolute-path` detector, shared by
14
+ `src/bundle.js` (export time) and a new $0 run-start precheck in `runPlan`
15
+ (`src/planrun.js`) — every job, not only exported bundles, now refuses at $0
16
+ (before the close-first precheck, before any provider call) when a close
17
+ script's CONTENT bakes in an absolute path that exists on disk. Exports
18
+ `readCloseScripts`, `checkCloseAbsolutePaths`, `absolutePathLiteralsOf` from
19
+ `src/index.js` — `readCloseScripts` is the pure reader M2's sha256 fingerprint
20
+ will reuse.
21
+ - `runJob`/`runPlan` gain an optional `resumable` flag (default `true`,
22
+ `run-u.mjs` unchanged); the bundle CLI (`src/cli.js`) passes `resumable: false`
23
+ so its escalation tail says "resume is `run-u`-only in v1" instead of naming a
24
+ `--resume` flag it does not implement (F130).
25
+
26
+ - **Hermetic test runner** (F136, closed at the runner). `scripts/hermetic.mjs`
27
+ runs `node --test` with `HOME` redirected to a fresh empty `mkdtemp` dir and
28
+ `GIT_CONFIG_GLOBAL`/`GIT_CONFIG_SYSTEM`/`GIT_CONFIG_NOSYSTEM` set to neutralise git
29
+ config, plus any inherited `GIT_AUTHOR_*`/`GIT_COMMITTER_*`/`EMAIL` deleted — the
30
+ local suite is now blind to this machine's identity the same way CI's runner already
31
+ is, so a test leaning on either fails here first. `npm test` now runs it in place of
32
+ a bare `node --test`; not a sandbox (PATH, `node_modules`, shelled-out binaries
33
+ untouched). See `docs/logs/FINDINGS.md` F136.
34
+
35
+ ### Fixed
36
+
37
+ - **F129, remaining 8 close scripts**: `scripts/testgen-cold-check-close.mjs`,
38
+ `testgen-close.mjs`, `types-close.mjs`, `l2poc-check-close.mjs`,
39
+ `u-bareagent-close.mjs`, `u-bareguard-close.mjs`, `u-litectx-close.mjs`,
40
+ `u-baremobile-close.mjs` now read `WORKDIR = process.cwd()` instead of a
41
+ hardcoded absolute patient path, following `u-spawner-close.mjs`'s fix
42
+ (`3b987d4`). Behaviour-preserving: `run-u` always passes `cwd` = the patient.
43
+
44
+ - **Close integrity M2 — close-bytes signature** (`docs/product/CLOSE-INTEGRITY-BUILD.md`,
45
+ PRD item 27, N4). A job's signature now covers its close scripts' CONTENT, not only
46
+ the path naming them.
47
+ - New signed spec field `close[].sha256`: hex sha256 of the script file's bytes, for
48
+ every stage whose `cmd` names an addressable script (an interpreter cmd —
49
+ `node`/`sh`/`bash`/`python`/`python3`/`npx` — or a bare directly-executable absolute
50
+ path, e.g. a `.sh` wrapper). It sits inside the signed spec, so `jobSpecHash` covers
51
+ it with no hash-function change. `validateJob` (`src/job.js`) demands it
52
+ (`missing-required`) on every addressable stage and shape-checks it
53
+ (`invalid-value`, 64 lowercase hex chars).
54
+ - `src/close-integrity.js` gains `hashCloseScriptBytes`, `checkCloseByteSignature`,
55
+ `checkStageByteSignature`, `signCloseScripts` (all exported from `src/index.js`).
56
+ `runPlan` verifies the signature at run start (alongside `close-absolute-path`, $0,
57
+ before the close-first precheck and before any provider call) AND before EVERY
58
+ close run thereafter (the `runCloseStages` seam the precheck/preflight/check-passes/
59
+ close-fix-loop all share) — a mismatch reds the new typed fault `close-tampered`
60
+ (`src/ralph.js`'s `CLOSE_FAULTS`), distinct from `close-red`/`close-crashed`.
61
+ - `src/bundle.js`'s `exportBundle` reds `close-sha-mismatch` when a spec's signed
62
+ sha256 disagrees with the script bytes actually being packed — the manifest hash
63
+ and the spec's own signature must never drift apart.
64
+ - `scripts/sign-close.mjs`: a thin CLI wrapper around `signCloseScripts` that fills
65
+ `close[].sha256` from disk and prints the old→new `jobSpecHash`; never writes
66
+ unless `--write`. The agent never writes this field — `src/authorjob.js`'s
67
+ `assembleSpec` refuses a draft carrying `sha256` the same way it refuses
68
+ `close`/`closeDecl`/`verdictType`.
69
+ - **Widened scope (orchestrator audit, folded into this build):** the addressable-script
70
+ shape test (`closeScriptCandidateToken`, `src/validate.js`) now also recognizes a
71
+ bare directly-executable absolute-path cmd (no interpreter prefix) — previously
72
+ invisible to BOTH `close-absolute-path` and this fingerprint. Every `jobs/*.json`
73
+ whose close names a script was re-signed with `scripts/sign-close.mjs --all --write`.
74
+
75
+ - **Close integrity M3 — close timeout: autoset + signed override** (PRD item 27,
76
+ hamr 2026-09-06: "both … autoset and can be override, same like api pricing"; arbiter
77
+ constants set 2026-09-07: floor 120,000ms, K = 5).
78
+ - New `src/closetimeout.js`: `CLOSE_TIMEOUT_FLOOR_MS`, `CLOSE_TIMEOUT_K`,
79
+ `TIMING_PREFLIGHT_CEILING_MS`, `timeCloseStages`, `computeCloseTimeoutCeiling`,
80
+ `resolveCloseTimeoutMs`, `closeTimeoutBanner` (all exported from `src/index.js`). A
81
+ $0 timing preflight runs every close stage once, ignoring verdicts, before the
82
+ close-first precheck and before any provider call; the per-stage ceiling every close
83
+ call in the run then uses is `max(FLOOR, K × slowest measured stage)`, printed on
84
+ every run (`close timeout: <n>s per stage (estimated from seed timing: …)`).
85
+ - New OPTIONAL signed spec field `closeTimeoutMs` (`>= CLOSE_TIMEOUT_FLOOR_MS`,
86
+ `src/job.js`): the operator's own override, which wins outright over the estimate and
87
+ may legally sit above OR below it (the rates-passthrough shape, F113 — not
88
+ tighten-only). Skipped when set: the timing pass never runs. Arbiter territory —
89
+ `src/authorjob.js`'s `assembleSpec` refuses a draft carrying it.
90
+ - New `runPlan` run-start check `wall-under-close-timeout`: refuses at $0 when
91
+ `job.maxWallMs` is under the just-resolved EFFECTIVE close timeout — the honest shape
92
+ once the real (autoset or signed) ceiling is known, never a silent clamp.
93
+ - `scripts/run-u.mjs`'s hardcoded `CLOSE_TIMEOUT_MS = 900_000` retires; the per-stage
94
+ ceiling is resolved once (via the same `resolveCloseTimeoutMs`) before the outside
95
+ watchdog (F67) is even spawned, so its stale/grace windows size off the real number.
96
+ `src/cli.js`'s bundle runner passes nothing — both runners now go through the
97
+ identical autoset/override path.
98
+ - **Part B — `BARELOOP_CLOSE_DIR`.** `runClose` (`src/ralph.js`) accepts an optional
99
+ `closeDir` and sets it as `BARELOOP_CLOSE_DIR` in the close's (already-stripped) env —
100
+ a `BARELOOP_` name matches none of `CLOSE_ENV_DENY`'s rules. New
101
+ `checkCloseDirRequired`/`CLOSE_DIR_ENV_VAR` (`src/close-integrity.js`): a close script
102
+ whose source mentions the variable and gets no `closeDir` refuses at $0
103
+ (`close-dir-required`), the same seam as `close-absolute-path`/`close-tampered`. The
104
+ four hand-authored scripts that hardcoded an external `SPINE_DIR`
105
+ (`testgen-cold-check-close.mjs`, `testgen-close.mjs`, `l2poc-check-close.mjs`,
106
+ `types-close.mjs`) now read `process.env.BARELOOP_CLOSE_DIR` and instrument-stop
107
+ (exit 97) when it is absent — no baked fallback. `scripts/u-pulselog-close.mjs`'s
108
+ hardcoded `--workdir` DEFAULT (a separate F129 cwd-class hazard — that literal existed
109
+ on disk and already tripped the shipped `close-absolute-path` guard for
110
+ `jobs/pulselog-u-types.json`) now falls back to `process.cwd()`, the same M1 template
111
+ every other close script uses. `src/cli.js` sets `closeDir` to
112
+ `<bundleDir>/runs/<runid>/close/`; `scripts/run-u.mjs` sets it to its existing
113
+ `spineDir`. Every `jobs/*.json` whose close bytes changed was re-signed
114
+ (`scripts/sign-close.mjs --all --write`).
115
+
116
+ - **F132 — a real paid fire of an exported bundle refused itself `close-tampered`
117
+ at $0** (run `mtqwmb9l` → export → `mtqwydl4`). `exportBundle`'s import rewrite
118
+ (`../src/x.js` → `'bareloop'`) changes a close script's bytes, but the written
119
+ bundle's `spec.json` kept the SOURCE spec's `close[].sha256` unchanged — two
120
+ signatures over two different byte strings, so the run-start
121
+ `checkCloseByteSignature` correctly refused a bundle that was never actually
122
+ tampered with. `exportBundle` (`src/bundle.js`) now re-signs `close[].sha256`
123
+ in the written spec over the RELOCATED bytes it actually packs, for every
124
+ stage that already carries a signature — the export-time `close-sha-mismatch`
125
+ check against the SOURCE spec/bytes is unchanged (it still guards a tampered
126
+ source).
127
+
128
+ - **F133 — double/mislabelled close-timeout banner** (run `mtqwmb9l`). `runPlan`
129
+ printed/emitted "estimated from seed timing" (`scripts/run-u.mjs`'s own
130
+ resolution, needed early to size the outside watchdog, F67) AND, on the same
131
+ run, "explicit runner override" (the library's own print, because run-u also
132
+ fed the resolved number back in as a bare `closeTimeoutMs`) — two lines for
133
+ one number, the second lying about its source. `scripts/run-u.mjs` no longer
134
+ passes `closeTimeoutMs` into `runJob` at all; `runJob`/`runPlan` resolve
135
+ (autoset, or the spec's own signed field) and announce the ceiling themselves
136
+ — the ONE banner/spine record per run. The runtime `closeTimeoutMs` option on
137
+ `runJob`/`runPlan` stays, test/backward-compat only. Same fire, second (real)
138
+ catch: run-u's own pre-fix early `emit('close-timing', …)` landed BEFORE
139
+ `job-start` on the spine (`runJob` is what emits `job-start`, and run-u called
140
+ it before ever calling `runJob`) — every spine reader assumes `job-start`
141
+ opens the file. Fixed by the same change (run-u no longer emits it early);
142
+ `tests/readshim-battery.test.js`'s archive check now tolerates `job-start`
143
+ within the first 3 records (the archived `u-mtqwmb9l.jsonl` is real and never
144
+ rewritten), and a new test pins `job-start` as literally the first record on
145
+ a fresh scripted run.
146
+
147
+ - **F135 — review-door accept re-run bypassed the close byte signature**. Every
148
+ other close execution goes through `checkStageByteSignature` before running
149
+ a stage (`runCloseStages`, `src/planrun.js`); the review door's `accept`
150
+ re-proof (`proveMechanically`, `src/reviewdoor.js`) called
151
+ `runStages`/`runDeclaredStages` directly and skipped it — a close script
152
+ swapped on disk after the run ended but before the signer's accept ran
153
+ unchecked and could mint a false accept. Fixed: `proveMechanically` now
154
+ verifies the mechanical stages' bytes against their signed `sha256` first,
155
+ refusing through the existing `door-accept-red` path on a mismatch
156
+ (`tests/reviewdoor-tamper.test.js`).
157
+
158
+ - **F134 — live re-fire validation, no code change**. Two real paid fires of the
159
+ same exported bundle (`mtr0icky` cap-halt, `mtr4t1u1` green re-fire) confirmed
160
+ M2's relocated-bytes signature and M3's close-timing banner both hold under a
161
+ second real fire, not just the one that motivated each fix; the cap-halt
162
+ itself read as a cap-shaped negative (an `Any`-suppression cheat correctly
163
+ caught by `no-suppressions`, not an instrument defect). See
164
+ `docs/logs/FINDINGS.md` F134.
165
+
166
+ - **F136 — CI-only red: patient test fixtures committed without the neutralised
167
+ git identity**. `tests/close-integrity.test.js` and `tests/close-timeout.test.js`
168
+ each added a second commit on top of `initPatientRepo`'s seed via a raw
169
+ `execFileSync('git', ...)` with no env, so it depended on the host's own git
170
+ identity — green on hamr's machine (a global config papers over it), red on
171
+ CI (none). `tests/helpers.js` now exports `patientGitEnv`/`gitInPatient`
172
+ reusing `initPatientRepo`'s identity env; both files' `makePatient` go through
173
+ it. See `docs/logs/FINDINGS.md` F136.
174
+
175
+ ## [0.21.0] — 2026-09-06
176
+
177
+ ### Added
178
+
179
+ - **Export/run a bundle** (`docs/product/EXPORT-BUILD.md`, frozen 2026-09-05). New
180
+ `src/bundle.js`: `exportBundle` mints a plain directory bundle (signed spec with
181
+ `close[].cmd` rewritten under `$BARELOOP_BUNDLE`, the close scripts, and the job's
182
+ registry history) from a job that already carries a real green at its current spec
183
+ hash — refuses (typed reds, nothing written) on an unrelocatable close cmd, a script
184
+ collision, an import this module cannot verify or that bareloop's own `src/index.js`
185
+ does not export, or no bridge at the spec's hash. `bundleHash(dir)` / `readBundle(dir)`
186
+ recompute the signature from disk and red `bundle-tampered` on any mismatch — the load-
187
+ bearing check that stands between a swapped close script and a fake green (N4).
188
+ `resolveBundleSpec`, `checkEnvelope` (tighten-only budget/wall), `bless`/`verifyBlessing`
189
+ (first-run approval, no-resign on later runs), and `appendHistory` round out the module.
190
+ - **`bareloop` CLI and `bin/`.** New `bin/bareloop.mjs` (the package's first `bin` entry)
191
+ and `src/cli.js`'s `main(argv, deps)` — exported from the package root as `cliMain` —
192
+ implement `bareloop export|run|history` plus a bare numbered menu. `run` orders its
193
+ steps as: tamper check, tighten-only envelope check, provider key (or the injected
194
+ test-seam provider), blessing/approval, a fresh `git worktree add --detach` per run,
195
+ then `runJob` itself; the tail prints the outcome, spend, and the `git merge` command —
196
+ merge stays human, the CLI never merges. `bin/` added to `package.json`'s `files`.
197
+ - **`checkBundleDeps(bundleDir)`** (`src/bundle.js`, F128): resolves `bareloop` from the
198
+ bundle's own `close/` directory (`createRequire(...).resolve('bareloop')`, walking the
199
+ package's own `exports` map) and reds typed `bundle-deps-missing` — with the exact cure
200
+ line `cd <bundleDir> && npm install` — when it cannot. `bareloop run` calls it as literal
201
+ step 1b, right after the tamper check and before the envelope check, the key, and any
202
+ worktree, so a bundle installed the wrong way (as someone else's dependency, never `npm
203
+ install`ed inside its own directory) fails fast with a fix instead of crashing deep
204
+ inside a close stage with a bare `ERR_MODULE_NOT_FOUND`.
205
+ - **`close-absolute-path` export guard** (`src/bundle.js`, F129): `exportBundle` now refuses
206
+ a close script whose source bakes in a string literal that is an absolute POSIX path
207
+ which exists on the exporting machine and is outside an allow-listed system prefix — a
208
+ close judges the `cwd` the runner gives it, never a path baked into its own source. Pins
209
+ the live defect where a hardcoded `WORKDIR` made the close judge the original patient
210
+ checkout instead of a fresh worktree, minting `already-green` at $0.
211
+ - **The aurora close now reads `process.cwd()`** (`scripts/u-spawner-close.mjs`) instead of a
212
+ hardcoded absolute `WORKDIR`, so it judges the tree the runner actually hands it rather than
213
+ the original patient checkout — the fix behind F129, validated live by a fourth paid fire
214
+ that went green from a fresh detached worktree (`docs/logs/FINDINGS.md`, F130's closing
215
+ pointer).
216
+
217
+ ### Fixed
218
+
219
+ - **F128 — `bareloop run` exit code now reflects the run outcome.** Previously every
220
+ invocation that reached the tail exited `0` regardless of outcome — a close-red, plan-
221
+ red, escalated, cap/wall-halted, or crashed run all read as CLI success. `run` now
222
+ returns `0` only for `green`/`already-green`; every other outcome exits `1`, so a caller
223
+ scripting off the exit code can no longer mistake a red run for a green one. The tail's
224
+ printed lines are unchanged.
225
+ - **F128 — the "minting run" line named the wrong run.** On a bundle's first (unblessed)
226
+ run, the notice printed the bridge's FIRST history row's `runid`, not the run that
227
+ actually minted the bundle's own signed spec. It now searches every bridge version
228
+ across ALL shipped bridges (base + shape fork) for the one whose `specHash` equals the
229
+ resolved `$BARELOOP_BUNDLE` spec's `jobSpecHash`, and prints "no version at this hash"
230
+ (honestly, never a guess) when none matches — which cross-machine is the common case,
231
+ since the historic mint's real script path and the importer's own bundle path rarely
232
+ coincide.
233
+ - **`bin/bareloop.mjs`: a closed stdout reader (`| head`) no longer crashes with an
234
+ unhandled `EPIPE`.** `process.stdout` now has an `'error'` listener that exits `0` on
235
+ `EPIPE` (the one legitimate `process.exit()` in this file — the reader is gone, so
236
+ there is no queued output left to lose) and re-throws anything else.
237
+ - **`scripts/replay-row.mjs`** — a $0 registry-row replay from an archived green spine: mints
238
+ the row a real run would have written via `writeRunGreenRow` had it been launched with
239
+ `--registry`, using the same seam and the same input derivation `scripts/run-u.mjs` uses.
240
+ Given it a permanent home this session (was scratchpad-only).
241
+
8
242
  ## [0.20.0] — 2026-09-05
9
243
 
10
244
  ### Added
package/README.md CHANGED
@@ -133,6 +133,20 @@ works, **export the whole workflow** — with its self-healing harness — and r
133
133
  anywhere. The point is to stop trial-and-erroring your way to the workflow that fits a job.
134
134
  Find it once, keep it, reuse it. It is a seed of something bigger over time.
135
135
 
136
+ ### Export a job
137
+
138
+ ```bash
139
+ bareloop export jobs/my-maintainer.json --registry ./bridges --out ./my-maintainer.bareloop
140
+ bareloop run ./my-maintainer.bareloop --repo /path/to/checkout --approve <bundleHash printed above>
141
+ ```
142
+
143
+ `export` only ships a job that has already earned a real green — it refuses to bundle an
144
+ unproven draft. The first `run` on a new machine asks you to approve the bundle's printed
145
+ hash; once that run goes green, later runs need no re-approval as long as the bundle stays
146
+ unchanged. Every run works in its own fresh git worktree and leaves the merge to you. Full
147
+ contract — flags, approval/blessing rules, exit codes — in
148
+ [`bareloop.context.md`](bareloop.context.md).
149
+
136
150
  ## Running it
137
151
 
138
152
  bareloop runs locally, as you. Secrets load from the environment and never enter the tree or