bareloop 0.2.0 → 0.3.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,349 @@ 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.3.0] — 2026-07-19
9
+
10
+ ### Fixed
11
+
12
+ - **A `close.judged` pattern with more than one capture group now reds at validation
13
+ (F40).** `runClose` reads capture group 1 only, so an alternation carrying the count
14
+ in another branch left group 1 `undefined` → `NaN` → `judgedCount` null → an exit-0
15
+ **green stamped `crashed`** (the mirror of the fake green the floor exists to catch);
16
+ at precheck that escalates `close-crashed` before the worker runs. The validator's
17
+ message already promised ONE group and only enforced "not zero". Alternation stays
18
+ expressible with non-capturing branches: `(?:a|b) (\d+)`.
19
+ - **Upstream-ledger attribution reads a typed `lib` field instead of sniffing error
20
+ prose (F40).** `interpret` prefixes every worker-loop error with `worker loop:`, and
21
+ the verb sniff ran first — so a bare-agent transport failure whose text merely
22
+ contained "recall" was billed to litectx. The throw site now stamps the owner it
23
+ knows, `ralph` relays it, and prose remains the fallback for older spines (the
24
+ `request-red` contract).
25
+ - **An unrecognised escalation category is counted, not silently dropped (F40).** The
26
+ dispatch keyed on four bare literals with no default, so a new or renamed category
27
+ vanished exactly like a deliberate exclusion. Exclusions are now an executable set;
28
+ anything outside {classified} ∪ {excluded} is charged to bareloop as a stale mapping.
29
+ - **The drafter is offered a PER-STEP share of the budget, not the whole pot (F40).** The
30
+ config is drafted once and re-validated at every step against what is left, so a ceiling
31
+ sized to the whole budget went stale the moment step 1 spent: step 2 then red `bounds` on
32
+ an unchanged config with money still in the pot. The advertised ceiling is now
33
+ `(budgetUsd − drafting reserve) ÷ predicate steps`, which still fits after earlier steps
34
+ have spent (the shares sum to the reserve-less budget). A drafting bound, not an
35
+ enforcement one — cap-not-estimate is unchanged and still tested, and a step needing more
36
+ than its share cap-halts cleanly rather than starving the steps after it. **Every shipped
37
+ job has one predicate step and is arithmetically identical.**
38
+ - **`jobs/aurora-fix.json` counts tests EXECUTED, not tests PASSED (F40).** A passed-count
39
+ floor conflated "did the close judge?" with "did the tests pass?", so the red tree the job
40
+ exists to fix fell under the floor and escalated as an instrument crash at precheck,
41
+ before the worker ever ran. Pattern is now `collected (\d+) items`; the patient's
42
+ `close.sh` swaps `-q` for `-ra` (`-q` prints no executed-count line at all, and `-ra` is
43
+ required or the job's `gapKeep "^FAILED "` loses every line it carries to the worker).
44
+ - **Revisor rounds no longer spend the worker's per-attempt bound (F40).**
45
+ `roundsThisAttempt` resets once, *before* the revisor phase, and revisor turns share
46
+ the worker's metered handler — so R revisor rounds silently left the worker 40−R while
47
+ the prompt still advertised 40, and a revisor burning the bound stopped the worker loop
48
+ before its first tool call. Money still meters on the run's axis (F12); only the round
49
+ charge moved. Restores "the advertised bound and the enforced bound stay the same
50
+ numbers on both axes", and matches the carve-out the summarizer fold already had.
51
+ - **Two F6 cost launderings closed (F40).** `run-job1`/`run-job2` printed
52
+ `spent: $0.0000` for provider-red/pricing-red runs (whose `job-end` carries no
53
+ `spentUsd`, and which can end after real priced spend) — now `UNKNOWN`, the
54
+ `run-battery` spelling. The `revision-red`/`revision-accepted` spine events recorded
55
+ `costUsd: 0` for an unpriced revisor — now the honest null.
56
+
57
+ ### Added
58
+
59
+ - **`scanSecrets(raw)` — the ONE spelling of the raw-text secret scan (F40).** The scan
60
+ was hand-rolled at seven call sites off `SECRET_PATTERNS`; detection and redaction
61
+ must never disagree about what a secret looks like. Exported beside the inventory,
62
+ `sweepSecretLiterals`'s text-side twin.
63
+
64
+ ### Changed
65
+
66
+ - **Tool-mode attempt bound raised 24→40 rounds (F37).** The TESTGEN calibration curve
67
+ measured that no prompt condition (bound undisclosed / disclosed / disclosed+pacing
68
+ strategy) produced a graded one-shot at 24 rounds — the worker's read-first prelude
69
+ eats the window — while one run proved a form-passing suite fits when writing starts
70
+ by mid-attempt. The per-attempt cutoff and the Gate's run-wide `maxTurns` now derive
71
+ from ONE hoisted `TURNS_PER_ATTEMPT` constant (they must agree or the enforced bound
72
+ drifts from the advertised one).
73
+
74
+ ### Added
75
+
76
+ - **F32 — worker-crash attribution: a close crash the worker caused is a gap, not a stop.**
77
+ Battery pass 1 (F31) measured the gap: 4 of 7 rows whole-file-rewrote an orchestrator,
78
+ broke imports, the close crashed under the judged floor, and the run **escalated** — the
79
+ worker was never told "your edit crashed the suite", so no plant that needed a second
80
+ attempt ever got one. F17's forbidden zone was built against instrument crashes and could
81
+ not see worker-attributable ones. Now: a `crashed` verdict with worker writes on record
82
+ routes as the DISTINCT verdict **`worker-crash`** (spine event with the file list), the
83
+ gap tells the worker which files it wrote and to fix or revert, and the loop continues
84
+ under the same caps. Attribution instrument: the gate audit's allow-decision write/edit
85
+ lines, run_id-scoped, read through an injected `workerWrites` seam (`ralph` stays
86
+ stdlib-only and dumb). Escalation is UNCHANGED for true instrument crashes: crash at
87
+ precheck (structurally pre-worker) or crash with zero writes stays `close-crashed`, never
88
+ retried; an unreadable audit attributes nothing (fail toward the old behavior). Validated
89
+ against the real instrument (P3 rerun, sonnet, $0.77): all three crashes routed and fed
90
+ back, zero escalations-eaten rows, honest `cap-halt` stop — pass 1's same plant died at
91
+ attempt 1 with the worker never told. TDD, suite 292 → 299.
92
+ - **BA-13 consumed — the anchored edit verb (`bare-agent` 0.27.0 → 0.29.0).** `TOOL_MENU`
93
+ gains **`edit`** (job-spec grantable; `run` stays locked), `TOOL_BY_VERB` maps it to
94
+ `shell_edit` (anchored exact-once replace: BA-4 param guards, atomic rename, anchor-miss
95
+ as a refusal RESULT so the worker re-anchors). Judged by the SAME `writeScope` fence as
96
+ `write` (bareguard action type `'edit'`, already in its FS vocabulary); the F32
97
+ attribution instrument counts edit actions as worker writes; the tool-mode persona
98
+ carries the strategy (prefer the edit verb — F31: 4 of 5 big-file whole-writes broke the
99
+ tree). The frozen battery spec pins its grant explicitly, so the menu widening does NOT
100
+ change what the signed hash buys — granting `edit` to the battery is a new spec version.
101
+ Suite 299 → 303.
102
+
103
+ ### Fixed
104
+
105
+ - **F33 — two verb-blind reporting instruments (battery pass 2's audit).** Pass 2 ran
106
+ 7/7 attempt-1 green at $0.94 under the newly signed `edit`-granting spec — and the
107
+ battery's printed table said `writes=0` on a pass whose every fix was one anchored edit:
108
+ the collector counted only `write` actions, built before the `edit` verb existed and
109
+ never re-audited when the menu widened (F32's lesson, re-learned at the reporting layer
110
+ in the same session it was minted). `culpritRead` was equally blind to the retrieval
111
+ channels: P5 greened with the culprit's body handed to it by the drafted config's
112
+ `before-attempt` recall hook (`body: true` + litectx recency boost ranking the
113
+ just-planted chunk into the hits) — invisible to an instrument that only saw gated
114
+ reads. Fixed: the collector counts write AND edit as write-class; `culpritRead` sees
115
+ every read-class channel; both recall emit sites (`ctx-tool`, `hook-op`) now carry
116
+ `paths` so downstream instruments can see what reached the worker's context. The
117
+ archived pass-2 results JSON keeps its wrong zeros; FINDINGS F33 is the corrected read.
118
+
119
+ - **F28 — the gap bound cut every failure line out of the worker's feedback.** The first
120
+ real end-to-end firing of the N2 loop delivered a 1,927-char gap containing **zero**
121
+ `not ok` lines: `ralph`'s `boundGap` keeps a head sample + elided middle + tail, and a
122
+ large TAP suite (`npm test`, ~67KB) prints its failing tests in the *middle* — so the
123
+ worker was told "5 fail" and never *which*, three attempts running, and never navigated to
124
+ the culprit file. New optional **`close.gapKeep`** (job-spec, `predicate` only): a regex
125
+ **source** whose matching close-output lines are preserved in a capped, clearly-delimited
126
+ kept-failures block between head and tail — the failing-test NAMES reach the worker
127
+ regardless of where they print. Hard-capped (50 lines / 8192 bytes, whichever binds) so a
128
+ pathological close cannot rebuild the bloat; a trimmed block announces the trim (no silent
129
+ truncation). Validated like `judged.pattern` (must compile as a RegExp, else a spec red
130
+ before any tokens); **arbiter territory** — the drafted workflow config cannot express it.
131
+ Also fixed the adjacent hazard: the gap now combines **both** streams (stdout + stderr), so
132
+ a failure on stdout survives stderr noise (the old `err || out` returned stderr alone and
133
+ lost it). `jobs/mailproof-fix.json` gains `"gapKeep": "^not ok"` (spec re-signed). Threaded
134
+ spec → `runJob` → `interpret` → `ralph` parallel to `judged`. TDD, suite 281 → 291.
135
+
136
+ ### Changed
137
+
138
+ - **Consume `bare-agent@0.27.0` — the N2 build gate (BA-4) is cleared.** 0.27.0 ("Provider
139
+ Fidelity & Honest Termination") shipped the entire upstream ask queue this rung filed:
140
+ BA-4, BA-5, BA-3, BA-6, BA-7, BA-1 (+ BA-10/BA-12). **BA-4** (`shell_write` truncating a
141
+ file to zero bytes on absent `content`) was the hard N2-exit blocker — its four acceptance
142
+ criteria are re-verified locally against the published tarball, so a write-granting tool
143
+ mode can ship honestly. The tool-mode middle (`src/interpret.js`) is updated to the new
144
+ contract: a **`truncated:max_tokens`** round (BA-6 — a round the API cut off, previously
145
+ laundered into a clean `error:null` finish, F25) now escalates as **provider-red** (retry,
146
+ the F11 transport class) instead of being scored as an empty attempt; **`loop.stop()`**'s
147
+ new `error:null`+text return (BA-3/BA-5) let the `stoppedByBound` shim be **deleted** (it
148
+ was dead under the new contract and could have swallowed a genuine halt); and
149
+ **`cacheMessages: true`** (BA-1) is wired on the worker loop — the transcript cache the
150
+ job #1 cost wall (F18: 754k full-price tokens, died at cap) needed. Regression test for
151
+ the truncation path added (mutation-checked). Suite 280/280, typecheck clean.
152
+
153
+ - **Agent/IDE scratch gitignored and de-tracked.** `.gitignore` now default-denies every dot-directory (`.*/`), re-admitting only what ships (`.github/`). Per-machine agent/IDE state (`.claude/`, `.litectx/`, `.idea/`, …) regenerates locally and only added noise and churn; any already-committed copies are removed from tracking (local files kept on disk). Repo hygiene only.
154
+
155
+ ### Added
156
+ - **N2 — the headless single-job loop (rung 3 of the ladder), modules 1–4 + 2b.**
157
+ - **`runJob(spec, opts)`** (`src/run.js`): the runner — approval gate (human-signs-always,
158
+ refuses before ANY token: `unapproved-spec`) → litectx known-answer smoke before tokens
159
+ (`smoke-red`, adaptlearn A3) → config drafting through the PRICED path, one sealed shot
160
+ + one redraft with reds fed back (`config-red` on a second red, zero grinding) →
161
+ sequential per-step interpret loops under the ONE cumulative ledger (a step-red stops
162
+ the job with attribution: `step-red:<id>`) → the hitl step opens a **draft PR
163
+ deterministically** (branch → stage the job fence ONLY → commit → push →
164
+ `gh pr create --draft`, through an injectable shell-owned exec seam — model tools never
165
+ touch git; a failure is `pr-red` + the escalation still fires) and ends `escalated` by
166
+ design, the PR URL riding the decision-ready escalation. New spine vocabulary:
167
+ `job-start/end`, `step-start/end`, `primitive-smoke`, `draft-result`, `pr-opened`,
168
+ `pr-red`.
169
+ - **Tool-mode middle** (module 2b): `job-v1` steps gain `mode: "text"|"tools"` and
170
+ `tools` (unique subset of `read|grep|write` — `TOOL_MENU`, frozen; requesting `run`
171
+ reds: locked-but-listed, admission waits on request-red evidence). The SPEC owns the
172
+ grant; the drafted config cannot express either. In tool mode the worker drives
173
+ bare-agent's shell tools with every call policy-checked against the same fence
174
+ (`actionTranslator` maps tool calls onto write/read actions; paths resolve exactly as
175
+ the tools resolve them), reads pinned to the workdir (`readScope`), a denial streak
176
+ stopping as `gate-red`. `STEP_MODES`/`TOOL_MENU` exported.
177
+ - **`artifact-red` + fence-robust extraction** (module 3): ONE parser (`extractArtifact`)
178
+ for every model-output parse — prose-wrapped and mid-text fences extract clean; a
179
+ non-artifact response reds on its OWN axis, writes nothing, and the retry is told why
180
+ (non-terminal, under ralph's cap). Text mode only — in tool mode the tools write
181
+ directly and the close judges the tree (there is no response artifact to red).
182
+ - **ralph options** (module 1): `closeTimeoutMs` (close wall-clock cap, was hardcoded
183
+ 120s) and the tail-biased gap bound (400 head + 1500 tail — the assertion diff lives
184
+ at the end; head-only truncation fed the worker pure preamble).
185
+ - **The upstream ledger** (module 4): `updateLedger({ledgerFile, spineFiles})`
186
+ (`src/ledger.js`) folds run spines into ONE append-only incident JSONL — the
187
+ A1/A2/A3 upstream-ask flow, mechanized: evidence in, human judgment out
188
+ (`suggestedAsk` is a template seed, never an auto-file; status rows
189
+ `filed → fixed → consumed` stay human-appended). Keys `lib:verb:class:sig` dedupe
190
+ the same bug across runs (short hash of the path/number-normalized detail); rows
191
+ are cumulative deltas, the fold is current state, and the collector is idempotent
192
+ over the same corpus — the ledger is derived and reconstructible, spines stay
193
+ ground truth. Classes worst-first (`LEDGER_CLASSES`, frozen): `silent-degradation`,
194
+ `runtime-red`, `provider-red`, `pricing-red` (added vs the design doc — F6),
195
+ `capability-gap` (ships dormant until in-loop admission), `broken-close`,
196
+ `request-red`, `retention-red`, `config-red` (attributed to bareloop's own
197
+ drafting schema). Excluded by doctrine: bare `cap-halt` (budget story),
198
+ `close-verdict`/`artifact-red` (worker stories), `gate-red` (governance working
199
+ as intended), `pr-red` (operator environment). Pure pieces exported:
200
+ `classifyIncidents`, `foldLedger`, `ledgerDeltas`. Design record:
201
+ `docs/plans/2026-07-11-upstream-ledger-design.md` + 2026-07-13 addendum (the
202
+ bareloop event mapping). CLI lands at N5; the panel reads the same file at N6.
203
+ - **Resume-to-cap: close-first skip** (module 4.5): every predicate step runs its
204
+ close BEFORE any tokens (`close-precheck` on the spine, output scrubbed at capture
205
+ like every close). Already-green skips the step for zero tokens as a DISTINCT
206
+ record (`step-end` outcome `already-green`, never plain `green` — nothing was done,
207
+ so it mints no learning credit and runs no on-green retention); a close that cannot
208
+ RUN stops `broken-close` before any provider call. Config drafting is deferred to
209
+ the first step that actually needs a worker (still one sealed shot + one redraft,
210
+ always drafted fresh per run) — so the resume story is: a `cap-halt` stop is the
211
+ checkpoint (the workdir + the closes), the human raises `budgetUsd` (new spec hash,
212
+ re-sign) and reruns, finished steps skip in seconds, and a clean cadenced rerun
213
+ costs ZERO provider calls. Design record: the 2026-07-13 addendum on
214
+ `docs/plans/2026-07-12-n2-headless-loop-design.md`.
215
+ - **The cadenced no-op is silent (F7).** A `hitl` step now checks the fence
216
+ (`git status --porcelain -- <fence>`) before touching git: an affirmatively-clean
217
+ fence emits `pr-skipped` + `step-end: already-green` and the job ends **green** —
218
+ no PR, no escalation. (Before: a green-repo cadence opened a branch, and `git
219
+ commit` correctly failed "nothing added to commit" → a broken-PR escalation every
220
+ single day. A hitl close is a human decision point; with no changes there is no
221
+ decision.) A FAILED check — not a repo, broken git — falls through to the PR path
222
+ and reds honestly: an unknown fence state is never a green.
223
+ - **The PR step hands the checkout back (F7).** The starting branch is read before
224
+ anything moves and restored on every path, success or failure; a restore that fails
225
+ is a loud `workdir-red` naming the stranded branch, and never un-opens a real PR.
226
+ (Before: the workdir was left on `bareloop/<job>-<id>`, so the next cadenced run
227
+ branched off the previous run's unmerged branch and judged its close against that
228
+ state.) New spine vocabulary: `close-precheck`, `pr-skipped`, `workdir-red`;
229
+ `step-end` gains the `already-green` outcome.
230
+
231
+ - **Nine defects found by the first REAL-MODEL runs of job #1 (F8–F16)** — all fixed
232
+ TDD-first, all invisible to a stubbed seam:
233
+ - **`cwd` (F8):** `runClose` spawned the close with NO cwd, so a cwd-relative close
234
+ (`npm test`) ran in the RUNNER's directory — the arbiter judged the wrong repository.
235
+ `cwd` now threads runner → `ralph` → `spawnSync` (`ralph({cwd})`, `runClose(…, {cwd})`).
236
+ - **Drafting ceiling (F9):** the prompt advertised the JOB budget while the validator
237
+ enforced budget − drafting-spend — a bound the drafter was never told, so every real run
238
+ (the model claims the ceiling it is given) deadlocked `config-red`. The shell now reserves
239
+ its own drafting allowance and advertises `budget − reserve`: one number, advertised and
240
+ enforced.
241
+ - **Repository root (F10):** tool mode now tells the worker the absolute workdir. bare-agent's
242
+ shell tools resolve relative paths against the PROCESS cwd, so a worker with no root is
243
+ blind — the real one groped `/home/…`, the runner's dir, then `/`, and the fence denied it.
244
+ - **`provider-red` in the worker path (F11):** a transport throw out of `loop.run()` (the real
245
+ run: `read ENETUNREACH`) was filed `interpreter-red` ("fix the middle"). It is a provider
246
+ failure — retry, don't debug.
247
+ - **Per-round metering (F12):** the ledger accounted `worker-result`, emitted only after
248
+ `loop.run()` RETURNS — so an attempt that HALTS reported nothing: the real run spent $1.4375
249
+ and the ledger said $0.0048. Money is now metered per ROUND (`worker-round`, at
250
+ `onLlmResult`), including the round that trips the cap. Unpriced is never free (F6).
251
+ - **The close's current output (F13):** the precheck's gap now reaches the first attempt as the
252
+ tree's state (never as "your previous attempt"). The `run` verb is locked, so without it the
253
+ worker cannot see the failure it was hired to fix.
254
+ - **The arbiter's books (F14):** tool mode denies reads of `gate-audit.jsonl`, `.smoke` and
255
+ `.litectx` — the real worker read its own gate audit and spine. The agent does not author its
256
+ arbiter, and does not read its records.
257
+ - **The loop contract (F16):** the tool persona now tells the worker it is ONE attempt inside
258
+ `while close-red and under-cap` and will be re-run with the close's verdict. Without it a
259
+ model one-shots — the real run read for 12 rounds, never wrote, and ate the whole budget.
260
+ - **Job #1's close (F15):** `node --test --test-reporter=dot`, not `npm test` — the tail-biased
261
+ gap bound buries mid-stream failures in a 391-test TAP stream, so the worker was told "3
262
+ failed" and nothing else. A close's output format is part of its contract with the worker.
263
+
264
+ ### Changed
265
+ - **`job-v1`: requesting a locked tool is now a DISTINCT red.** `tools` containing
266
+ `run` reds with code `request-red` (was a generic `invalid-value`) so the ledger can
267
+ tally admission demand — a generic code buried the evidence as a typo. An unknown
268
+ tool name stays `invalid-value`. `LOCKED_TOOLS` (frozen, `['run']`) exported.
269
+ - **`interpret` opts:** `target` is now optional (required in text mode only); new
270
+ `mode`/`tools` opts thread the spec's grant. Additive — existing callers unchanged.
271
+ - **Cost contract:** `extractRules` returns `costUsd: number|null` — null is the honest
272
+ "spend unknown" (F6); callers must not coerce it to 0.
273
+
274
+ ### Fixed
275
+ - **Review round 2026-07-13 (8 confirmed findings, all execution-verified):**
276
+ - **extractArtifact wrapper-vs-content gate:** a fence counts as the artifact's
277
+ wrapper only when it opens within the first 5 lines of the response; a fence
278
+ buried deeper inside an unfenced reply is the artifact's OWN content and the
279
+ whole reply is the artifact, verbatim. Before: an unfenced doc-generator module
280
+ containing a ```js example``` was silently truncated to the 2-line fragment
281
+ with `red: null`, corrupting the close signal. Trade-off pinned in tests:
282
+ past the window, prose + fence is treated as the artifact (rare under the
283
+ no-fences persona); fence-heavy artifacts belong in tool mode.
284
+ - **Secret-leak channel closed:** `openDraftPr` now scrubs git/gh subprocess output
285
+ with the ONE shape inventory (`SECRET_PATTERNS`) at capture — a credentialed
286
+ remote URL echoed by a failed `git push` never reaches `pr-red`/the escalation's
287
+ `pr.error` on the append-only spine (same doctrine as the close path).
288
+ - **Plan-shape spend is metered:** the job ledger accounts `worker-plan` events too
289
+ (a separate `loop.run` whose metrics never fold into the implement call's) —
290
+ plan calls now drain `spentUsd` and an unpriced plan call halts `pricing-red`.
291
+ - **The spine never dangles:** a provider transport throw during drafting is a
292
+ decision-ready **`provider-red`** terminal (new outcome + escalation category,
293
+ classified by the ledger); an interpreter throw outside the loop (e.g. a broken
294
+ gate audit path) escalates `interpreter-red` with a terminal `job-end`.
295
+ - **Reds-before-tokens for the call, not just the spec:** a text-mode job invoked
296
+ without `opts.target` is a `job-red` before ANY provider call (`interpret`
297
+ throws a loud TypeError for direct callers); previously it burned a draft + a
298
+ worker call, the gate default-allowed the absent path, and `writeFileSync`
299
+ crashed as a misfiled interpreter-red.
300
+ - **Whitespace-padded `close.cmd` reds** (`invalid-value`) — a leading space made
301
+ `spawnSync('')` throw synchronously past every belt; the runner also trims
302
+ before splitting (defense in depth). **`cap-halt` job outcome:** drafting spend
303
+ that consumes the whole budget stops honestly (no paid redraft over a blown
304
+ budget, no config-red blaming the drafter).
305
+ - **One instrument for the F6 cost read:** `priceOf(result)` (`src/text.js`)
306
+ replaces four hand-copied `metrics ? costUsd : (cost ?? null)` spellings;
307
+ `REMEMBER_KINDS` is exported from the validator so the drafting prompt
308
+ advertises the menu the validator enforces (no drift). `request-red` reds carry
309
+ the locked verb as a **structured `verb` field** — the ledger keys on it (prose
310
+ stays a legacy fallback). Stale "clamped by validation" JSDoc corrected (the
311
+ validator REDS bounds; it never clamps).
312
+ - **Three silent $0 cost launderings (the F6 class) in shipped code:** `interpret`'s
313
+ worker cost emit (`?? cost` chain), `extract.js`'s rules-path cost (`?? cost ?? 0`),
314
+ and `extract.js`'s transport-throw path reporting unmeasured spend as `$0`. All now
315
+ carry the honest null + `unpricedRounds`; `runJob` halts `pricing-red` on either
316
+ signal (unpriced is never free — F6, PRD v1.8).
317
+
318
+ ### Fixed (release review, 2026-07-19 — fresh full gates over the whole branch)
319
+
320
+ - **The plan-only call no longer carries the tool menu.** In tool mode with a drafted
321
+ `loop.shape: 'plan'`, the decompose call ("Plan only, no code") was offered the full
322
+ granted menu — a model calling `shell_write` during the plan round mutates the tree
323
+ before the implement round exists. The menu IS the grant (2b): the plan call now gets
324
+ an empty menu. Reachable by any tool-mode job on any run (the drafter picks the shape);
325
+ the combination was unexercised by any test until now.
326
+ - **`extractRules` consumes the parser's own red.** It took `extractArtifact(...).code`
327
+ and dropped `.red`, so an empty model response surfaced as a generic JSON
328
+ `parse-error` instead of the already-computed `'empty response'` — the ONE-parser
329
+ doctrine requires both callers to consume the red field (`interpret` already did).
330
+ Now a distinct `artifact-red`.
331
+ - **`run-job1` couples `shellCapUsd` to `spec.budgetUsd`** like every sibling runner —
332
+ the library default cap of $2 was a second, silent ceiling: a signed resume top-up
333
+ above $2 would red `bounds` on a budget the human explicitly approved (the advertised
334
+ budget must BE the enforced one).
335
+ - **`ctx_get`'s repo-relative conversion is boundary-aware** (`workdir + sep`): a bare
336
+ prefix match would garble a sibling path like `<workdir>-backup/x`. Defense in depth —
337
+ the gate independently denies such paths before the tool executes.
338
+ - **`jobs/litectx-maintainer.json`: `gapKeep "^✖ "` + the `edit` grant** — the two
339
+ omissions vs every sibling spec. The keep pattern is derived from the real
340
+ `--test-reporter=spec` red output (failing tests repeat unindented in the summary
341
+ block, so each failing-test NAME survives the gap bound exactly once — F28). Spec
342
+ edit = new hash; `run-job1` refuses until re-approved.
343
+ - **`scanSecrets` + `CLOSE_FAULTS` are exported from the package root.**
344
+ `bareloop.context.md` documented both as public API; the exports map admits only
345
+ `"."`, so neither was actually reachable by an adopter. The contract is now true.
346
+ - **Recorded, parked (arbiter territory):** symlink write-through is bareloop's
347
+ caller-contract debt — bareguard documents that its fence resolves lexical traversal
348
+ only and callers must canonicalise. No granted verb can create a symlink; the vector
349
+ needs a pre-existing one inside the patient's writeScope. UPSTREAM-ASKS "OUR SIDE" §7.
350
+
8
351
  ## [0.2.0] — 2026-07-12
9
352
 
10
353
  ### Added
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  <p align="center">
14
14
  <img src="https://img.shields.io/github/package-json/v/hamr0/bareloop?label=version&color=2a4f8c" alt="version (auto from package.json)">
15
15
  <img src="https://img.shields.io/badge/license-Apache%202.0-2a4f8c" alt="license: Apache 2.0">
16
- <img src="https://img.shields.io/badge/status-pre--code%20%C2%B7%20PRD%20locked-8a6d1a" alt="status: pre-code, PRD locked">
16
+ <img src="https://img.shields.io/badge/status-N2%20%C2%B7%20headless%20loop-2a6d3a" alt="status: N2, headless loop">
17
17
  </p>
18
18
 
19
19
  **"Automate this job — I don't know the best workflow."** For tasks that are **repeated,
@@ -25,8 +25,10 @@ inheritance with ledger-counted attribution.
25
25
  The pitch in one line: **workflows that earn their own design, with receipts** — every
26
26
  inherited rule carries the green that minted it and the contrast that attributed it.
27
27
 
28
- > **Status: pre-code.** The name is reserved, the PRD is locked, and the build ladder is
29
- > in flight (roadmap below). The first usable release is the headless loop.
28
+ > **Status: N2 — the headless loop is real.** `runJob()` takes a signed job spec through
29
+ > draft → per-step worker loops → close verdicts → draft PR, under one budget the agent
30
+ > can only tighten. Rungs N0–N2 have landed (roadmap below); next is executed
31
+ > inheritance (N3), where the ladder's kill-switch question gets its answer.
30
32
 
31
33
  ## Quick start
32
34
 
@@ -93,9 +95,9 @@ ladder, and the stop is a result.
93
95
 
94
96
  | Rung | What lands |
95
97
  |---|---|
96
- | **N0** | Port + outer shell + spine (token-free) |
97
- | **N1** | Job/close schema + validator |
98
- | **N2** | Single-job headless loop — job #1 minimal (review→fix→PR, hard greens only) |
98
+ | **N0** ✅ | Port + outer shell + spine (token-free) — v0.1.0 |
99
+ | **N1** ✅ | Job/close schema + validator — v0.2.0 |
100
+ | **N2** ✅ | Single-job headless loop — job #1 minimal (review→fix→PR, hard greens only) — v0.3.0 |
99
101
  | **N3** | Executed inheritance + contrast-bit extractor — **kill-switch: rules must transmit across non-identical runs** |
100
102
  | **N4** | Verdict classes complete (soft/HITL minting) |
101
103
  | **N5** | Scheduler + budget ops |