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