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/CHANGELOG.md CHANGED
@@ -5,6 +5,298 @@ 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.23.0] — 2026-09-09
9
+
10
+ ### Added
11
+
12
+ - **`openai-api` on the provider menu, behind a provider FACTORY** (PRD item 28,
13
+ F149/F150/F156/F157). `src/providers.js` holds one table — provider name →
14
+ constructor, env key, tier models, per-model request-key gating — and both
15
+ runners go through it instead of naming `AnthropicProvider` directly. An
16
+ unknown provider name THROWS; there is no silent default. `openai-api` takes
17
+ an optional validated job-level `baseUrl` (https, or http on loopback only)
18
+ and admits ONE model, `deepseek-chat`, which earned the slot with a real paid
19
+ green through the shipped runner (run `mtu12vks`: green, $1.64 of $4, 10.3 of
20
+ 30 min). DeepSeek silently ignores `max_completion_tokens`, so its table entry
21
+ sets bare-agent's `legacyMaxTokens` — an output cap that does not bind is a
22
+ money hazard. The judge stays PINNED to `anthropic-api` and `JUDGE_MODEL`
23
+ whatever the worker's provider is. `bareloop run` (the bundle runner) is
24
+ `ANTHROPIC_API_KEY`-only and REFUSES at $0, naming the key it would have
25
+ needed, rather than building another provider with the wrong key.
26
+ - **`provider` and `baseUrl` on the `job-start` spine record** (F157),
27
+ report-only. Without them an archive cannot separate anthropic rows from
28
+ `openai-api` rows, and every pooled cost or duration figure would be a
29
+ contaminated aggregate.
30
+ - **`stopReason` on every `worker-round`** (BA-13, report-only, null when
31
+ absent, never invented).
32
+
33
+ - **`runner-drained` — a `beforeExit` backstop for the absent-`job-end` class**
34
+ (PRD item 28's ruling (c); F140). A provider promise that never settles
35
+ (BA-25's class) used to let node drain and exit with NO terminal record at
36
+ all — no escalation, no outcome, no way to tell a silent drain from an
37
+ operator kill (F140: 10-of-235 archived spines with no `job-end`, one real
38
+ silent death). bare-agent 0.42 closed the known drop cases (F141), but the
39
+ class itself is "an instrument fires on absence," not a specific bug, so
40
+ the backstop stays needed. `runJob` (`src/run.js`) now registers ONE
41
+ `process.once('beforeExit', …)` right after `job-start`, tracked via a
42
+ local `ended` flag every `job-end` site sets; if the process still drains
43
+ with no `job-end`, it mints one with outcome `runner-drained`,
44
+ `spendComplete:false` (a drained run's in-flight spend is unknowable — the
45
+ floor already banked is honest, F6), and sets `process.exitCode` non-zero
46
+ (never `process.exit()`). The listener is removed in a `finally` so a
47
+ normal run never leaks it. `runner-drained` is a new terminal outcome,
48
+ documented in `bareloop.context.md`'s outcomes list — deliberately **not**
49
+ added to the resumable/checkpoint set (`CHECKPOINT_OUTCOMES`, `src/reuse.js`):
50
+ an unknown in-flight state is not a known-safe resume point.
51
+
52
+ - **Bounded 429 retry — a FALLBACK, not the primary answer** (PRD item 28's
53
+ parked a/b ruling, hamr verbatim: "do a/b as a fallback, and verify/validate
54
+ + no regression"; F143). A vendor HTTP 429 used to end the whole run as
55
+ `provider-red`, discarding all completed work, even though the vendor's own
56
+ response states exactly how long to wait. `src/ratelimit.js` (new,
57
+ `transport.js`'s sibling) adds ONE bounded retry that honours that stated
58
+ delay, capped at 60s (`RATE_LIMIT_MAX_WAIT_MS`) — a wait longer than that is
59
+ not honoured at all. Wired into `src/planrun.js`'s provider-retry seam
60
+ (`withTransportRetry` renamed `withProviderRetries`) as a SECOND,
61
+ independent one-shot budget alongside the existing F115 transport retry;
62
+ the wall always wins (no retry past the deadline); each retry emits a
63
+ report-only `rate-limit-retry` spine record and does NOT floor
64
+ `spendComplete` (a 429 is a refusal — nothing was billed — unlike a
65
+ transport throw). **The primary answer stays the primary answer**: picking
66
+ a worker model with adequate rate headroom (F139/F143's measured TPM
67
+ table) — this retry only softens the failure mode when that was not
68
+ followed or headroom was still exceeded.
69
+
70
+ ### Changed
71
+
72
+ - Bumped `bare-agent` pin from `^0.39.0` to `^0.42.0`. 0.42.0 brings: BA-24 —
73
+ `OpenAIProvider` now sends `max_completion_tokens` by default (GPT-5-safe),
74
+ with a new `legacyMaxTokens` option to send the legacy `max_tokens` key
75
+ instead; BA-25 — a response body cut after headers (peer `'aborted'`,
76
+ stream `'error'`, or a premature `'close'` before `'end'`) now REJECTS
77
+ `generate()` with a retryable `ProviderError` (`context.bound:'transport'`)
78
+ instead of leaving the promise pending forever; a new `loop:truncated`
79
+ stream event plus `stopReason` on the `onLlmResult` metering payload; and
80
+ `options.toolChoice` on `OpenAIProvider`.
81
+
82
+ ### Fixed
83
+
84
+ - **F154** — every provider call bareloop makes now carries a deadline. Seven
85
+ sites had none: the softgreen judge, the calibration gate, the authoring
86
+ scout's three rounds, the declaration/revise boundary, and the bridge picker.
87
+ A silent endpoint HANGS the process (an open socket is an active handle, so
88
+ the drain backstop cannot see it); a deadline is the only instrument that
89
+ fires on the absence of events. Tighten-only.
90
+ - **F155** — the judged floor graded whatever LOCATE reported, so a silently
91
+ omitted function passed on the subset. `decide()` now diffs the artifact's own
92
+ top-level functions against the reported set and reds, naming what was
93
+ missing. The declaration shapes live ONCE (`FN_SHAPES`), from which both the
94
+ locate prompt's prose and the detector are derived.
95
+ - **F153** — a provider's own error sentence now reaches the operator instead of
96
+ a bare status number, redacted through the one secret inventory and capped.
97
+ - **F151** — the bounded 429 retry, added last release, fired on a real vendor
98
+ 429 for the first time: it parsed the stated wait from prose, waited it, and
99
+ retried once. Mechanism proven; on a TPM-saturated model the retry is not
100
+ expected to recover, and did not.
101
+ - **F147** — the F59 scout summary round (and the authoring scout's recovery
102
+ round, the same shape in `src/authorscout.js`) re-fed a transcript bare-agent
103
+ had already system-prepended into a new `Loop.run()`, which prepended the
104
+ persona AGAIN: two identical system messages at index 0/1. Real OpenAI
105
+ tolerates that; vLLM-class OpenAI-compatible backends reject it (HTTP 400
106
+ "System message must be at the beginning" — the Qwen death F146 left
107
+ unexplained). Both sites now strip the leading system message before
108
+ continuing, as bare-agent's own `Loop.chat()` does. Proven live: the captured
109
+ failing request goes 400 → 200 with one line removed; deepseek-chat then
110
+ greened a full job (F149/F150).
111
+ - **F141** — `src/transport.js`'s `isTransportFailure` now also recognizes
112
+ bare-agent's own BA-25 transport classification
113
+ (`err.context.bound === 'transport'`), after the existing HTTP-status and
114
+ `retryable:false` guards. Before this fix, the exact class hamr's F115
115
+ one-retry ruling names — a call that dies mid-read, no full response —
116
+ would have ended provider-red with no retry once the 0.42.0 pin landed,
117
+ because the new rejection shape carried none of the signals the classifier
118
+ previously looked for (no `status`, `retryable:true`, no matching error
119
+ code/message). Retry budget unchanged: `TRANSPORT_RETRIES = 1`.
120
+ - **Review door refuses `close-timing-red` instead of defaulting the close timeout**
121
+ (F137, `docs/product/CLOSE-INTEGRITY-BUILD.md` "Ruled 2026-09-07", found by
122
+ orchestrator review of M3, run `mtqwmb9l`). `scripts/run-u.mjs`'s `accept` door path
123
+ resolved the mechanical re-run's close timeout via `resolveCloseTimeoutMs` and, when
124
+ that timing pass itself timed out, silently fell through to `closeTimeoutMs:
125
+ undefined` — the library's 120s default — substituting an unauthorized ceiling for a
126
+ door decision, the exact second silent-default class M3 was built to remove one door
127
+ up. A door whose own timing pass times out now REFUSES outright as a named
128
+ `close-timing-red` door stop (same wording/options as the in-run escalation, new pure
129
+ renderer `doorTimingRedLines` in `scripts/u-readout.mjs`): `answerReviewDoor` is never
130
+ called, nothing is recorded (no door record), nothing is released, nothing is spent,
131
+ and the process exits non-zero.
132
+
133
+ ## [0.22.0] — 2026-09-07
134
+
135
+ ### Added
136
+
137
+ - **Close integrity M1** (`docs/product/CLOSE-INTEGRITY-BUILD.md`, PRD item 27, F129/F131).
138
+ - `src/close-integrity.js`: the ONE `close-absolute-path` detector, shared by
139
+ `src/bundle.js` (export time) and a new $0 run-start precheck in `runPlan`
140
+ (`src/planrun.js`) — every job, not only exported bundles, now refuses at $0
141
+ (before the close-first precheck, before any provider call) when a close
142
+ script's CONTENT bakes in an absolute path that exists on disk. Exports
143
+ `readCloseScripts`, `checkCloseAbsolutePaths`, `absolutePathLiteralsOf` from
144
+ `src/index.js` — `readCloseScripts` is the pure reader M2's sha256 fingerprint
145
+ will reuse.
146
+ - `runJob`/`runPlan` gain an optional `resumable` flag (default `true`,
147
+ `run-u.mjs` unchanged); the bundle CLI (`src/cli.js`) passes `resumable: false`
148
+ so its escalation tail says "resume is `run-u`-only in v1" instead of naming a
149
+ `--resume` flag it does not implement (F130).
150
+
151
+ - **Hermetic test runner** (F136, closed at the runner). `scripts/hermetic.mjs`
152
+ runs `node --test` with `HOME` redirected to a fresh empty `mkdtemp` dir and
153
+ `GIT_CONFIG_GLOBAL`/`GIT_CONFIG_SYSTEM`/`GIT_CONFIG_NOSYSTEM` set to neutralise git
154
+ config, plus any inherited `GIT_AUTHOR_*`/`GIT_COMMITTER_*`/`EMAIL` deleted — the
155
+ local suite is now blind to this machine's identity the same way CI's runner already
156
+ is, so a test leaning on either fails here first. `npm test` now runs it in place of
157
+ a bare `node --test`; not a sandbox (PATH, `node_modules`, shelled-out binaries
158
+ untouched). See `docs/logs/FINDINGS.md` F136.
159
+
160
+ ### Fixed
161
+
162
+ - **F129, remaining 8 close scripts**: `scripts/testgen-cold-check-close.mjs`,
163
+ `testgen-close.mjs`, `types-close.mjs`, `l2poc-check-close.mjs`,
164
+ `u-bareagent-close.mjs`, `u-bareguard-close.mjs`, `u-litectx-close.mjs`,
165
+ `u-baremobile-close.mjs` now read `WORKDIR = process.cwd()` instead of a
166
+ hardcoded absolute patient path, following `u-spawner-close.mjs`'s fix
167
+ (`3b987d4`). Behaviour-preserving: `run-u` always passes `cwd` = the patient.
168
+
169
+ - **Close integrity M2 — close-bytes signature** (`docs/product/CLOSE-INTEGRITY-BUILD.md`,
170
+ PRD item 27, N4). A job's signature now covers its close scripts' CONTENT, not only
171
+ the path naming them.
172
+ - New signed spec field `close[].sha256`: hex sha256 of the script file's bytes, for
173
+ every stage whose `cmd` names an addressable script (an interpreter cmd —
174
+ `node`/`sh`/`bash`/`python`/`python3`/`npx` — or a bare directly-executable absolute
175
+ path, e.g. a `.sh` wrapper). It sits inside the signed spec, so `jobSpecHash` covers
176
+ it with no hash-function change. `validateJob` (`src/job.js`) demands it
177
+ (`missing-required`) on every addressable stage and shape-checks it
178
+ (`invalid-value`, 64 lowercase hex chars).
179
+ - `src/close-integrity.js` gains `hashCloseScriptBytes`, `checkCloseByteSignature`,
180
+ `checkStageByteSignature`, `signCloseScripts` (all exported from `src/index.js`).
181
+ `runPlan` verifies the signature at run start (alongside `close-absolute-path`, $0,
182
+ before the close-first precheck and before any provider call) AND before EVERY
183
+ close run thereafter (the `runCloseStages` seam the precheck/preflight/check-passes/
184
+ close-fix-loop all share) — a mismatch reds the new typed fault `close-tampered`
185
+ (`src/ralph.js`'s `CLOSE_FAULTS`), distinct from `close-red`/`close-crashed`.
186
+ - `src/bundle.js`'s `exportBundle` reds `close-sha-mismatch` when a spec's signed
187
+ sha256 disagrees with the script bytes actually being packed — the manifest hash
188
+ and the spec's own signature must never drift apart.
189
+ - `scripts/sign-close.mjs`: a thin CLI wrapper around `signCloseScripts` that fills
190
+ `close[].sha256` from disk and prints the old→new `jobSpecHash`; never writes
191
+ unless `--write`. The agent never writes this field — `src/authorjob.js`'s
192
+ `assembleSpec` refuses a draft carrying `sha256` the same way it refuses
193
+ `close`/`closeDecl`/`verdictType`.
194
+ - **Widened scope (orchestrator audit, folded into this build):** the addressable-script
195
+ shape test (`closeScriptCandidateToken`, `src/validate.js`) now also recognizes a
196
+ bare directly-executable absolute-path cmd (no interpreter prefix) — previously
197
+ invisible to BOTH `close-absolute-path` and this fingerprint. Every `jobs/*.json`
198
+ whose close names a script was re-signed with `scripts/sign-close.mjs --all --write`.
199
+
200
+ - **Close integrity M3 — close timeout: autoset + signed override** (PRD item 27,
201
+ hamr 2026-09-06: "both … autoset and can be override, same like api pricing"; arbiter
202
+ constants set 2026-09-07: floor 120,000ms, K = 5).
203
+ - New `src/closetimeout.js`: `CLOSE_TIMEOUT_FLOOR_MS`, `CLOSE_TIMEOUT_K`,
204
+ `TIMING_PREFLIGHT_CEILING_MS`, `timeCloseStages`, `computeCloseTimeoutCeiling`,
205
+ `resolveCloseTimeoutMs`, `closeTimeoutBanner` (all exported from `src/index.js`). A
206
+ $0 timing preflight runs every close stage once, ignoring verdicts, before the
207
+ close-first precheck and before any provider call; the per-stage ceiling every close
208
+ call in the run then uses is `max(FLOOR, K × slowest measured stage)`, printed on
209
+ every run (`close timeout: <n>s per stage (estimated from seed timing: …)`).
210
+ - New OPTIONAL signed spec field `closeTimeoutMs` (`>= CLOSE_TIMEOUT_FLOOR_MS`,
211
+ `src/job.js`): the operator's own override, which wins outright over the estimate and
212
+ may legally sit above OR below it (the rates-passthrough shape, F113 — not
213
+ tighten-only). Skipped when set: the timing pass never runs. Arbiter territory —
214
+ `src/authorjob.js`'s `assembleSpec` refuses a draft carrying it.
215
+ - New `runPlan` run-start check `wall-under-close-timeout`: refuses at $0 when
216
+ `job.maxWallMs` is under the just-resolved EFFECTIVE close timeout — the honest shape
217
+ once the real (autoset or signed) ceiling is known, never a silent clamp.
218
+ - `scripts/run-u.mjs`'s hardcoded `CLOSE_TIMEOUT_MS = 900_000` retires; the per-stage
219
+ ceiling is resolved once (via the same `resolveCloseTimeoutMs`) before the outside
220
+ watchdog (F67) is even spawned, so its stale/grace windows size off the real number.
221
+ `src/cli.js`'s bundle runner passes nothing — both runners now go through the
222
+ identical autoset/override path.
223
+ - **Part B — `BARELOOP_CLOSE_DIR`.** `runClose` (`src/ralph.js`) accepts an optional
224
+ `closeDir` and sets it as `BARELOOP_CLOSE_DIR` in the close's (already-stripped) env —
225
+ a `BARELOOP_` name matches none of `CLOSE_ENV_DENY`'s rules. New
226
+ `checkCloseDirRequired`/`CLOSE_DIR_ENV_VAR` (`src/close-integrity.js`): a close script
227
+ whose source mentions the variable and gets no `closeDir` refuses at $0
228
+ (`close-dir-required`), the same seam as `close-absolute-path`/`close-tampered`. The
229
+ four hand-authored scripts that hardcoded an external `SPINE_DIR`
230
+ (`testgen-cold-check-close.mjs`, `testgen-close.mjs`, `l2poc-check-close.mjs`,
231
+ `types-close.mjs`) now read `process.env.BARELOOP_CLOSE_DIR` and instrument-stop
232
+ (exit 97) when it is absent — no baked fallback. `scripts/u-pulselog-close.mjs`'s
233
+ hardcoded `--workdir` DEFAULT (a separate F129 cwd-class hazard — that literal existed
234
+ on disk and already tripped the shipped `close-absolute-path` guard for
235
+ `jobs/pulselog-u-types.json`) now falls back to `process.cwd()`, the same M1 template
236
+ every other close script uses. `src/cli.js` sets `closeDir` to
237
+ `<bundleDir>/runs/<runid>/close/`; `scripts/run-u.mjs` sets it to its existing
238
+ `spineDir`. Every `jobs/*.json` whose close bytes changed was re-signed
239
+ (`scripts/sign-close.mjs --all --write`).
240
+
241
+ - **F132 — a real paid fire of an exported bundle refused itself `close-tampered`
242
+ at $0** (run `mtqwmb9l` → export → `mtqwydl4`). `exportBundle`'s import rewrite
243
+ (`../src/x.js` → `'bareloop'`) changes a close script's bytes, but the written
244
+ bundle's `spec.json` kept the SOURCE spec's `close[].sha256` unchanged — two
245
+ signatures over two different byte strings, so the run-start
246
+ `checkCloseByteSignature` correctly refused a bundle that was never actually
247
+ tampered with. `exportBundle` (`src/bundle.js`) now re-signs `close[].sha256`
248
+ in the written spec over the RELOCATED bytes it actually packs, for every
249
+ stage that already carries a signature — the export-time `close-sha-mismatch`
250
+ check against the SOURCE spec/bytes is unchanged (it still guards a tampered
251
+ source).
252
+
253
+ - **F133 — double/mislabelled close-timeout banner** (run `mtqwmb9l`). `runPlan`
254
+ printed/emitted "estimated from seed timing" (`scripts/run-u.mjs`'s own
255
+ resolution, needed early to size the outside watchdog, F67) AND, on the same
256
+ run, "explicit runner override" (the library's own print, because run-u also
257
+ fed the resolved number back in as a bare `closeTimeoutMs`) — two lines for
258
+ one number, the second lying about its source. `scripts/run-u.mjs` no longer
259
+ passes `closeTimeoutMs` into `runJob` at all; `runJob`/`runPlan` resolve
260
+ (autoset, or the spec's own signed field) and announce the ceiling themselves
261
+ — the ONE banner/spine record per run. The runtime `closeTimeoutMs` option on
262
+ `runJob`/`runPlan` stays, test/backward-compat only. Same fire, second (real)
263
+ catch: run-u's own pre-fix early `emit('close-timing', …)` landed BEFORE
264
+ `job-start` on the spine (`runJob` is what emits `job-start`, and run-u called
265
+ it before ever calling `runJob`) — every spine reader assumes `job-start`
266
+ opens the file. Fixed by the same change (run-u no longer emits it early);
267
+ `tests/readshim-battery.test.js`'s archive check now tolerates `job-start`
268
+ within the first 3 records (the archived `u-mtqwmb9l.jsonl` is real and never
269
+ rewritten), and a new test pins `job-start` as literally the first record on
270
+ a fresh scripted run.
271
+
272
+ - **F135 — review-door accept re-run bypassed the close byte signature**. Every
273
+ other close execution goes through `checkStageByteSignature` before running
274
+ a stage (`runCloseStages`, `src/planrun.js`); the review door's `accept`
275
+ re-proof (`proveMechanically`, `src/reviewdoor.js`) called
276
+ `runStages`/`runDeclaredStages` directly and skipped it — a close script
277
+ swapped on disk after the run ended but before the signer's accept ran
278
+ unchecked and could mint a false accept. Fixed: `proveMechanically` now
279
+ verifies the mechanical stages' bytes against their signed `sha256` first,
280
+ refusing through the existing `door-accept-red` path on a mismatch
281
+ (`tests/reviewdoor-tamper.test.js`).
282
+
283
+ - **F134 — live re-fire validation, no code change**. Two real paid fires of the
284
+ same exported bundle (`mtr0icky` cap-halt, `mtr4t1u1` green re-fire) confirmed
285
+ M2's relocated-bytes signature and M3's close-timing banner both hold under a
286
+ second real fire, not just the one that motivated each fix; the cap-halt
287
+ itself read as a cap-shaped negative (an `Any`-suppression cheat correctly
288
+ caught by `no-suppressions`, not an instrument defect). See
289
+ `docs/logs/FINDINGS.md` F134.
290
+
291
+ - **F136 — CI-only red: patient test fixtures committed without the neutralised
292
+ git identity**. `tests/close-integrity.test.js` and `tests/close-timeout.test.js`
293
+ each added a second commit on top of `initPatientRepo`'s seed via a raw
294
+ `execFileSync('git', ...)` with no env, so it depended on the host's own git
295
+ identity — green on hamr's machine (a global config papers over it), red on
296
+ CI (none). `tests/helpers.js` now exports `patientGitEnv`/`gitInPatient`
297
+ reusing `initPatientRepo`'s identity env; both files' `makePatient` go through
298
+ it. See `docs/logs/FINDINGS.md` F136.
299
+
8
300
  ## [0.21.0] — 2026-09-06
9
301
 
10
302
  ### Added
package/README.md CHANGED
@@ -82,7 +82,7 @@ A signed job spec goes in. A verdict comes out.
82
82
 
83
83
  ```js
84
84
  import { runJob, jobSpecHash, makeSpine } from 'bareloop';
85
- import { AnthropicProvider } from 'bare-agent';
85
+ import { AnthropicProvider } from 'bare-agent/providers';
86
86
 
87
87
  const spec = {
88
88
  schema: 'job-v1',