bareloop 0.7.0 → 0.9.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,710 @@ 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.9.0] — 2026-08-09
9
+
10
+ The **close-authoring rung**. The close stops being a hand-written script and becomes a
11
+ DECLARATION over kinds bareloop owns: a user answers a six-question interview, an LLM
12
+ composes the declaration by calling a schema-derived tool, three mechanical gates run it
13
+ against the real repository, and a human signs the hash. Nothing here judges a close —
14
+ no LLM validates another LLM's close, at any point (D9). Design record (D1–D13, FROZEN):
15
+ `docs/plans/2026-08-07-close-authoring-design.md`; PRD v1.51–v1.58.
16
+
17
+ ### Added
18
+
19
+ - **M1 — `src/kinds.js`, the kind executor: four kinds under three inherited runtime
20
+ contracts.** The four kinds (`command-exit`, `count-not-worse`, `pattern-absent-in-diff`,
21
+ `files-changed`) are the small part; the CONTRACTS every kind inherits are the
22
+ load-bearing part, and they live in the executor where no declaration can weaken them.
23
+ **Instrument-stop** (exit 97, `judged` withheld — a broken instrument is a CASUALTY, never
24
+ a red; and `first`-over-nothing is *unknown* where `sum`-over-nothing is a counted 0).
25
+ **The gap** (`gapKeep` on every line, trims announced and counted). **The changed set**
26
+ (diff plus untracked, minus `.litectx/` by PREFIX and `gate-audit.jsonl` by EXACT path —
27
+ so a worker-authored `src/gate-audit.jsonl` still counts). Graduated from the gate-2 POC
28
+ executor as a REWRITE: async spawn (`spawnSync` freezes the host loop the stall fuse lives
29
+ in, F68), caller-scoped seed worktrees under the OS temp dir rather than a module-global
30
+ cache inside the package, and the arbiter's own `CLOSE_ENV_DENY` imported rather than
31
+ re-spelled. **D12 rides on top: the close stores the COUNTING RULE and never a number** —
32
+ every run measures its own seed, with the route that measured it recorded. 49 integration
33
+ tests over real temp git repos and real child processes.
34
+ - **M2 — the catalogue, the TYPES genre, and the declaration validator: a path is SELECTED
35
+ from the tree, never derived from prose.** One module, because the catalogue, the genre
36
+ and the validator are one contract read from three sides, and a second spelling of any of
37
+ them is the drift class the parser normaliser already exists to prevent.
38
+ - **`KIND_CATALOGUE` IS the whole vocabulary.** The four live kinds are keyed to the
39
+ executor's own `LIVE_KINDS` and asserted equal in BOTH directions. `judged-floor` and
40
+ `human-confirms` are LOCKED menu entries — a distinct `locked-kind` red at validation,
41
+ before any tokens, carrying `verb` + `lib: 'bareloop'` so the demand is counted on
42
+ structured fields and never filed against a bare-suite package (the BA-2 class).
43
+ `harness-loop` (TESTGEN) is **ABSENT** — no entry at all, so declaring it is an
44
+ unknown-kind typo, which is the honest reading of a kind that is out of v1.
45
+ - **The genre template is FROZEN TEXT.** The suite reads addendum 3 out of the prereg and
46
+ compares BYTES, so a paraphrase in source fails the build rather than quietly changing
47
+ what every future close is authored against. The JS 7-pattern and Python 5-pattern
48
+ suppression batteries come from the hand-written closes' own `SUPPRESSIONS` tables —
49
+ operator-owned genre knowledge now. Two residues land as genre DATA, ours to inject and
50
+ never model-filled: guard **SCOPE** (`no-suppressions` ships with NO scope and nothing
51
+ left to fill — the POC's arm A narrowed it to the two target files, which reads stricter
52
+ and is not, because a suppression in a new helper walks straight through; narrowing an
53
+ injected guard is a red, not a taste) and genre **ENV** (`MYPYPATH` is expressed here and
54
+ applied by the flow — it moves no seed number, so no seed-verdict read can ever catch its
55
+ absence, and a declaration that authors it itself reds `genre-owned-env`).
56
+ - **The validator**: schema, kinds, params, F49's static nested-quantifier reject over
57
+ every model-authored regex (the detector itself untouched — monotonic-only), the parser
58
+ through M1's normaliser verbatim, and the **F84 one-population law in both halves** (a
59
+ count over a scoped job must NAME its population; the same cmd+parser+scope declared
60
+ twice is one number wearing two stage names — with the in-scope/outside split, the thing
61
+ the law exists to protect, asserted to stay legal). Plus **hamr's listing rule**: every
62
+ path-like param must SELECT from the seed listing. The POC's arm A derived
63
+ `src/alertEmail.js` from the user's prose and silently reclassified 15 real errors into
64
+ the wrong population; no READING of a declaration catches that, and a prefix match
65
+ against `git ls-tree` does. The red hands back what DOES exist beside the invented path —
66
+ named items, counted, capped (F38/F39), never a paragraph.
67
+ - Three preconditions fail CLOSED, all guarding one failure — *a validator handed nothing
68
+ to check against reports a declaration it never examined*: `listing` and `guards` absent
69
+ are reds, and `envOwned` splits ABSENT from EMPTY (`[]` is a genre that owns no
70
+ variables; `undefined` is a caller that never asked — F59's distinction in the shape it
71
+ bites here). One disagreement the tests caught and the code won: a bare `**` is **NOT a
72
+ root** — `globToPrefix` collapses only the `/**` SUFFIX, so admitting it would put the
73
+ validator and the grader on two spellings of one scope (the F9 red class): green at the
74
+ gate, and a containment guard that reds every file at runtime. It reds. 46 tests TDD,
75
+ watched failing first; 54-mutant battery, ZERO survivors (six holes closed on the first
76
+ pass).
77
+ - **M3 — the declaration is CALLED, not written** (`src/authorscout.js` LOOKS,
78
+ `src/authorflow.js` WRITES; neither ACTS — D11's split). The load-bearing change against
79
+ the gate-2 POC is hamr's schema-forcing ruling: the model answers by calling
80
+ **`declare_close`**, whose input schema IS the declaration shape and is DERIVED from the
81
+ catalogue — kind list, locked-ness, parameter names and every enum read from M1/M2, with
82
+ `schemaCoverage` failing the build loudly if the catalogue ever grows a parameter the
83
+ schema cannot express. A malformed emission is an artifact-red on its own bounded axis; the
84
+ tolerant text parse survives only as a marked fallback for a provider with no tool mode.
85
+ - Two consequences named rather than smoothed: **a locked kind now has no schema branch, so
86
+ declaring one is INEXPRESSIBLE** — which means M2's `locked-kind` demand counter is
87
+ reachable only from the interview layer or the text fallback, and the header says so
88
+ instead of pretending the channel is intact. And the tool is an OUTPUT CHANNEL that takes
89
+ no action, so D3's "no tools at all" is untouched: **hamr's law is about actions, not
90
+ syntax.**
91
+ - **Measured, both arms against the real `Loop`:** stopping the loop inside the tool costs
92
+ 1 provider round and $0.001; unwired costs 2 and $0.002 — tool mode does not cost double
93
+ what text mode did.
94
+ - Three defects found by the tests before a run found them: a FLAT directory beat every
95
+ listing tier (200 names render as 200 names at "counts-only"), so the prompt ingredient
96
+ was unbounded — names now give way, counts never do, with a backstop bounding the block
97
+ that beats every tier; an unreadable listing returned no `files` key at all, which
98
+ destructures to `undefined` and reads as falsy-empty (the exact absent-renders-as-empty
99
+ shape this module exists to refuse); and the genre-env injection rewrote stages it had no
100
+ reason to touch, making the signed form differ from what the model wrote for no stated
101
+ reason. **F59 is enforced five ways and refuses at $0** — an ABSENT survey, an EMPTY facts
102
+ object, an unreadable listing, an empty tree and an unbuildable genre variable each stop
103
+ the flow before a token is spent. 1222/1222; 40 of 41 mutants killed.
104
+ - **M4 — the close is a SIGNED FIELD, not a compiled script** (`closeDecl`,
105
+ `src/declaredclose.js`, `src/authorjob.js`). **The integration decision is made in the open
106
+ and named in the source:** a declared close is carried by a new spec field and executed by
107
+ M1's own kind executor. The cheaper alternative — render each declared stage as a shell
108
+ one-liner and let the existing `close[]` path run it — is REJECTED, and the rejection is the
109
+ point: it would turn owned kinds back into authored shell strings, which is exactly what D3
110
+ makes inexpressible. A declaration whose meaning is a generated `sh -c` is a script with
111
+ extra steps, and the arbiter would be judging text nobody validated.
112
+ - **Two executors, ONE contract.** Every behaviour the command path already had is pinned
113
+ by a test on the declared path: first-red-wins with the deciding stage naming itself; the
114
+ forbidden zone routed by a TYPED fault (`STOP_FAULTS`, stamped in M1 at the site that
115
+ OBSERVED it) so a timeout still offers *"raise the close timeout"* and a spawn failure
116
+ still offers *"fix the close"*; the gap header from ONE template (`stageGap`, now exported
117
+ — `src/trend.js` parses that exact line); the scrub at the emission boundary; and the
118
+ check menu still derived from stage names, one hop, one direction.
119
+ - **`close` and `closeDecl` are ALTERNATIVES, refused rather than merged**
120
+ (`close-duplicated`): a spec with two closes has two arbiters, and *picking one silently
121
+ is how a signed artefact stops meaning what the signer read*. The declared close is
122
+ HARD-class in the SAME table the close types use (`CLASS_BY_CLOSE.declared`), so a locked
123
+ verdict on one reds twice — as counted demand AND as `close-hierarchy`.
124
+ - **The gate is SPLIT honestly, not skipped.** A job spec is validated with no repository
125
+ in hand, so the listing rule and the scoped-job derivation that arms the F84 law are
126
+ DEFERRED there — `deferListing: true`, spelled literally, with `grounded: false` on the
127
+ result — and the RUNNER re-runs them against the real seed before any stage and before any
128
+ token. Proven both ways: the invented path `src/alertEmail.js` passes the spec gate and
129
+ dies at the runner, with **zero provider calls**.
130
+ - **D8/D12 ride together.** The seed is HEAD at run start, READ and recorded on a
131
+ `close-decl` spine event, never typed and structurally absent from the signed spec —
132
+ `seedRef` is an unknown-field red on `closeDecl`, because a baseline frozen at signing
133
+ judges run 5 against run 1's tree. `baseline: "seed"` is measured at each run's own start,
134
+ every run.
135
+ - **Two things the bridge adds**, because the declared executor knows more than an exit code
136
+ does: a `gapKeep` derived from one prefix constant, so Layer R's red-set has an instrument
137
+ on this path too; and a TREND VALUE donated **only** for `lower-is-better` — the omission
138
+ is the load-bearing half, since `trend.js` reads improvement as `value < best`, so donating
139
+ a higher-is-better test-count floor would read a DROPPING count as convergence and
140
+ recommend a top-up on a dying run. Null is the honest answer (F6); the stage position still
141
+ travels.
142
+ - **F90, two findings, neither found by a failure.** The design record's one named defect
143
+ (D13's ledger request-red misattribution) was ALREADY FIXED on `main` four days before the
144
+ record was written, so what M4 owed was the PROOF the new emit site routes through it — a
145
+ regression test walking refusal → `refusalEvents` → `classifyIncidents` → `suggestedAsk`,
146
+ asserting the ask reads "bareloop:" and names no bare-suite package, with both halves
147
+ killed as separate mutants (*a frozen document is frozen against rewriting, not against the
148
+ world moving underneath it* — F63 in its cheapest form). And a SECOND gap renderer silently
149
+ blinded Layer R's trim detector: it refuses to compare a red-set from a trimmed window by
150
+ looking for ralph's own marker, and the kind executor announces its trims in different
151
+ words — the direction that MANUFACTURES a fixation reading out of a blind instrument. Fixed
152
+ by exporting the executor's marker and reading both. *The shared-marker guard held
153
+ perfectly and was still defeated, not by drift but by a producer the reader had never heard
154
+ of.*
155
+ - 46 new tests over real temp git patients, real child processes and real `runPlan` (the
156
+ runner seam is the highest-risk part of the module and nothing there is mocked). 1268/1268;
157
+ mutation battery 22/22 after one honest survivor — a test asserting `reds.every(…)` over an
158
+ array the mutant left EMPTY, the conditional-over-a-never-true-fixture trap in its
159
+ `.every([])` form.
160
+ - **The interview re-keys from GENRE to VERDICT CLASS, and the pick is a PROMISE** (PRD v1.57
161
+ §1–§2 — **this SUPERSEDES D4**). `verdictType` is a USER-facing RADIO (`green` | `soft-green`
162
+ | `hitl` — the spelling already shipped in `job.js`; the design brief's `softgreen` would have
163
+ flipped every signed hash). **v1 admits only `green`**; a locked pick returns the counted
164
+ `request-red` refusal BEFORE its questions run.
165
+ - `QUESTION_SETS` replaces `TYPES_QUESTIONS`. The green set is TYPES' **six** questions
166
+ byte-for-byte — they were already genre-neutral, so generalising meant DELETING the D13
167
+ genre-confirm slot, not rewording. Locked sets carry `questions: null`, never `{}` (absent
168
+ is never empty, F59), and `questionsFor` THROWS on them: reaching questions means admission
169
+ failed to refuse. Genre understanding moves into `CLASS_STATEMENTS` fed to the composer, and
170
+ the genre refusal moves to the composer on the same counted path.
171
+ - **The guard battery re-homes to the class** (hamr: *"battery ties with job type"*).
172
+ `CLASS_BATTERIES` keys attachment — which guards, and their un-removability — off the class;
173
+ the tool-specific contents fill at COMPOSITION from the language the declaration names. **An
174
+ empty battery is IMPOSSIBLE, enforced not asserted**: unknown class, locked class, unknown
175
+ language, empty fill and zero guards all THROW.
176
+ - **Class-vs-ceiling, ruled *"yes to both, go"*:** every catalogue kind carries its
177
+ `verdictClass`, and a declaration whose ceiling EXCEEDS the pick is an honest
178
+ `class-ceiling` red naming the kind — never a silent upgrade or downgrade. Inert in v1 by
179
+ construction, and proven FIRING against an injected soft-green kind.
180
+ - Driver: **`--verdict` is REQUIRED** — a defaulted radio would answer the user's question for
181
+ them; `answers.json` drops key 7. Live smoke: `--verdict hitl` refuses at $0
182
+ decision-ready, `--verdict green` runs to the provider boundary. 14/14 mutants killed; the
183
+ saved PREPARED python spec still validates post-rework. 1354/1354.
184
+ - **The scout gets 3 typed attempts, and every raw lands in the books** (PRD v1.58). hamr's
185
+ rulings in the register, verbatim: *"retry on scout too, tighten-only same as revisions"*;
186
+ *"depending on causes, we can hardcode 3 attempts, but if reasons are beyond malformed then we
187
+ may verge into self-healing … i don't think we should"*; *"for audit, it should come on top of
188
+ existing audit trails … scrub for common things in general"*.
189
+ - `SCOUT_ATTEMPTS = 3`, hardcoded; the `attempts` parameter is TIGHTEN-ONLY and CLAMPS rather
190
+ than throws (floor **1** — a scout that never ran is an ABSENT nobody can act on). The gate
191
+ reads TYPED causes stamped at the detection site and **only the malformed class retries**
192
+ (`empty` | `unparseable`). What is excluded is excluded for a reason: `call-failed` covers
193
+ transport and `truncated:max_tokens` (provider-red, no redraft), `short` is F59's cut-off
194
+ population and already has its own instrument INSIDE the attempt, and `non-object` /
195
+ `empty-object` are valid JSON with vacuous content — a SEMANTIC failure, which F38/F39
196
+ measured as the same distribution sampled twice. **That is the self-healing line, and it is
197
+ not crossed.** The re-ask is TOOLLESS over the survey's own conversation, naming the
198
+ mechanical parse error and position and nothing else — the repository was already read, only
199
+ the emission was unreadable. **There is no JSON repair behind it and there never will be**: a
200
+ repairer decides what the model MEANT and writes it down as though the model had said it, in
201
+ the one artefact whose whole job is to be honest about what a repository contains.
202
+ - **`raws` — what every model call actually SAID**, the scout's attempts absorbed alongside the
203
+ declaration's, in the cost book's own order and under the same labels it meters. `scrubRaw` /
204
+ `RAW_PERSIST_MAX` (8000) / `RAW_TRIM_MARKER` join `src/text.js` as the third one-spelling
205
+ model-output helper — a NEW trim spelling deliberately, because `GAP_TRIM_MARKER` has a
206
+ reader with a different meaning (F90). One recorder writes the cost entry and the raw
207
+ together (labels pinned pairwise by test), the cut is walked back off a continuation byte so
208
+ a multi-byte character is never split, and **raws are present on the `$0` preflight refusal
209
+ paths too** — the path that spends nothing more is exactly the one whose evidence used to die
210
+ with the process. `iterations` records what a turn MEANT; `raws` records what it SAID, and a
211
+ malformation is only ever visible in the second. Attribution fix found mid-build: the
212
+ classified blob is tracked BY INDEX, not `raws.at(-1)` — an F59 recovery discard had
213
+ mis-stamped the verdict. E2E replay of `mslhn707`'s real failure shape: 3 labelled calls,
214
+ raws carrying causes and attempt indexes, reason *"— after 3 attempts"*, and a planted
215
+ key-shaped secret scrubbed to 0 across the serialized artifact. 1405/1405.
216
+ - **The WORK BRANCH is a hard rule, enforced STRUCTURALLY** (PRD v1.57 §3; `src/workbranch.js`).
217
+ hamr's ruling, built as ruled: *"The agent creates a NEW BRANCH before it touches any code — a
218
+ HARD RULE, no exceptions… Named with a meaningful slug."*
219
+ - **Structure over ordering:** `mkWorker` — the single seam that grants write-class verbs —
220
+ THROWS a categorised interpreter-red if asked for a writable worker with no branch prepared.
221
+ That covers both write sites and any future third, which an ordering rule cannot. The seam
222
+ sits at planrun step 0c: after the $0 precheck/preflight (an already-green tree leaves no
223
+ branch behind), before the scout (every branch fault is an instrument stop costing zero
224
+ tokens).
225
+ - The slug is `bareloop-` + the SIGNED spec's `job` field, kebab-validated and INSIDE the spec
226
+ hash, so the name cannot drift from what was signed (goal prose was rejected as a source, and
227
+ the rejection is documented). `WORK_BRANCH_RE`'s alphabet makes `..`, `@{`, `.lock`, a leading
228
+ `-`, and **`main`/`master` inexpressible**; `prepareWorkBranch` re-validates even a
229
+ hand-passed name. Collisions take `-2`/`-3` suffixes (one blast radius per try). **Resume
230
+ returns to ITS OWN recorded branch** (a `work-branch` spine record; `readResume`'s
231
+ `restart.branch`), and a recorded branch that is GONE is a STOP, never a fresh start — the
232
+ surviving mutant taught the assertion to demand the arbiter's own wording. **`branch-red` is
233
+ a distinct terminal, excluded from ledger classification** (an interpreter-red would aim an
234
+ upstream ask at a library for a patient nobody prepared). Adjacent fix: `relay` now passes
235
+ the category through instead of filing wiring faults as transport casualties.
236
+ - **Consequences recorded in `bareloop.context.md`, hamr-agreed:** the patient must be a git
237
+ repository with at least one commit, AND is always a separate COPY of the treated repo, never
238
+ the original — *"keep patients copies separate from original, always"*; the copy is the blast
239
+ radius. A detached HEAD attaches (`from: null`); a subdirectory workdir branches in the
240
+ enclosing repo (pre-existing semantics, now on the record). 18 new tests against real tmp git
241
+ repos plus guard/resume coverage across `planrun`/`run`/`reuse`, watched failing first; 2
242
+ sabotages and a mutation pass. 1387/1387.
243
+ - **`scripts/run-author.mjs` — the authoring driver, now tracked**, and the paid proofs it
244
+ bought. **pulselog (JS) reached SIGNING PREPARED twice** ($0.25 / $0.22), with the trial gate at
245
+ 32s and a **byte-identical spec hash across cold runs**. **aurora (python) refused honestly
246
+ twice**, and both refusals were the product working: run 1 a real uncrafted `mypy` fatal
247
+ (symlink `MYPYPATH`), run 2 the scope filter dropping error lines reported through a symlink
248
+ spelling — where the crash-stop correctly refused a 0-count instead of minting a fake green on a
249
+ tree carrying 16 strict errors. The fix then converted that refusal **for $0 on the same saved
250
+ declaration**: SIGNING PREPARED, all three gates green, `typecheck-spawner` honest RED at seed
251
+ (16 vs 0), `specHash` byte-identical to the failed run's own hash. Second sitting, $0.59 (F93):
252
+ `mslsnnzk` $0.345 diagnosing, `mslwbkz7` $0.245 validating the fix live — same composed shape,
253
+ GREEN `value=0` with the dropped-count note, three gates PASS, SIGNING PREPARED on the reworked
254
+ code. F91 / F92 / F93.
255
+
256
+ ### Changed
257
+
258
+ - **`maxRevisions` joins every other cap as TIGHTEN-ONLY.** The bound predated the law:
259
+ `maxRevisions: 50` bought fifty revise rounds, and unlimited revising is exactly the
260
+ thrash-as-adaptation the prereg forbids. The clamp lives at the ONE seam inside `authorClose`,
261
+ so it is not a rule any caller can be trusted to keep — the loop reads a DERIVED cap and the
262
+ parameter never reaches it. The floor is **0** (author once, no revise round — already a legal
263
+ loop shape) rather than the scout's floor of 1, and a non-finite or negative value lands on the
264
+ floor instead of a bound the loop never enters.
265
+ - **`structureRetries` becomes tighten-only** under the constant that already claimed to be its
266
+ ceiling, and **`effectiveTimeoutMs` with it** — the same direction every cap in this system runs
267
+ in.
268
+ - **Absolute paths join the command deny-floor, rejection-only.** `checkKind` gained a command
269
+ deny-floor plus an absolute-path/traversal refusal during the review rounds; drive-absolute
270
+ spellings (`C:\…`) were the gap left in it. Monotonic: the floor only ever adds rejections.
271
+ - **The ten hand-written close scripts import `JUDGED_MARKER` from `src/kinds.js`** instead of
272
+ respelling `judged=1` as a literal (hamr-approved wiring). Output proven byte-identical by
273
+ EXECUTION on all ten; no signed spec hash flips (`jobSpecHash` covers the resolved spec object,
274
+ not script bytes). **Known new coupling, recorded not hidden:** close scripts now import the
275
+ `src/` graph, which matters if they ever ship standalone.
276
+ - **A GREEN declared close now reports what its stages announced** (`notes`). `translate()`
277
+ computed a stage's gap text and threw it away on GREEN and on INSTRUMENT-STOP — but M1's kinds
278
+ announce some things whichever way a stage lands (a declared env var that was dropped, the scope
279
+ filter's own arithmetic), so the bridge was dropping the **check-both-greens** audit trail on
280
+ the floor. Surfaced as `notes`, absent when empty, per stage as well as on the summary — and
281
+ deliberately NOT `.gap`, because every consumer downstream guards with `if (gap)`, so a green
282
+ carrying one would read as revision-worthy and a stop carrying one would look like a verdict it
283
+ never rendered. Verified inert: every spine reader keys on `verdict`/`gap`/`stage`, nothing
284
+ routes or bounds on this field, and **no verdict moves.**
285
+ - **`assembleSpec` REFUSES a draft that already carries the authored half** instead of clobbering
286
+ it. It used to drop a draft's `close` on the floor and stamp its own `verdictType` over the
287
+ operator's, silently — the case `src/job.js` already rules on in its own words (*"picking one
288
+ silently is how the signed artefact stops meaning what the signer read"*), one step earlier. Now
289
+ a named refusal over `AUTHORED_SPEC_FIELDS` (`close`, `closeDecl`, `verdictType` — the fields
290
+ the fold WRITES, named as data so the refusal and the fold can never disagree about which half
291
+ of the spec is which), and `run-author` asks the same question at **$0 BEFORE the scout**, so
292
+ the answer never arrives after the model has been paid.
293
+ - **`closeStagesOf(job)` is the ONE staging every close consumer reads**, widened to both fields
294
+ — a consumer reading `close` alone sees a declared job as CLOSELESS and offers the drafter an
295
+ empty check menu. `scripts/consolidate-bridges.mjs` was still calling `stageClose(spec.close)`
296
+ and refusing declared-close specs for the one reason untrue of them; migrated (a spec with
297
+ neither field still dies honestly).
298
+
299
+ ### Fixed
300
+
301
+ - **F93 — the broken-ruler guard read match counts AFTER the scope filter, so a LIVE tool over an
302
+ empty population read as a DEAD one** (`src/kinds.js`). `count-not-worse`'s crash guard summed
303
+ `breakdown[].matches`, which `parseValue` tallies after the scope filter has already dropped the
304
+ out-of-population lines. **Two entirely different worlds collapsed into that one number**: a
305
+ tool that crashed before it looked at the tree, and a tool that RAN and printed parseable
306
+ findings which all fell outside this stage's scope. Run `mslsnnzk` paid for it: `npm run
307
+ typecheck -- --strict` exited 2 and printed **67 real `error TS\d+:` lines**, every one under
308
+ `src/`, against a stage scoped `excludePrefixes: ["src/"]` — post-scope the count was 0, the
309
+ stage stopped as a crashed instrument, and **a good close was refused at gate 2**.
310
+ - The same two-faults-read-as-one class the `first` aggregate's own stop had already SPLIT in
311
+ its own comment; the `sum` path carried the unfixed twin. `parseValue` now reports
312
+ `preScopeMatches`, tallied where LIVENESS lives — before the filter has an opinion, kept +
313
+ unattributable + dropped alike — and the guard consults that. A genuinely silent non-zero exit
314
+ still matched nothing pre-scope and still stops: **the fail-safe is untouched in the direction
315
+ it was chosen for**, and the accepted limit for `grep -c` / pytest-5 stands unchanged. One
316
+ `parseValue` call site (`measure`) serves both the current tree and the seed worktree, so the
317
+ baseline-side measurement gets the same reading by CONSTRUCTION; `command-exit` and
318
+ `pattern-absent-in-diff` never touch it.
319
+ - The stop lane also stops throwing away its own diagnostics: `stopped()` takes the notes a
320
+ measurement had already computed and renders them under the stop, so a `first` term whose every
321
+ match was scope-excluded sends the reader to the SCOPE instead of hunting a parser bug.
322
+ - **Validated live, not asserted:** `mslwbkz7` composed the same shape on the post-fix code and
323
+ walked to SIGNING PREPARED — GREEN `value=0` with the dropped-count note, three gates PASS.
324
+ F92's shape-lottery framing is **refuted in part**: the model composed the same correct
325
+ inside/outside split 3/3 (systematic, not a roll); what varied was the AGGREGATE, and only one
326
+ of the two resulting stops was a defect. The attribution was corrected the same night off the
327
+ artifacts — the crash guard fired **once**, on `mslsnnzk`; `mslhpw2v`'s stop was the
328
+ `first`-path fail-safe working as designed. **The parked re-compose-on-refusal lever survives
329
+ as a build and loses this run as its justification:** a retry loop cannot heal a mislabeled
330
+ instrument.
331
+ - **The scope filter and the FENCE compare FILES, not spellings.** The live python rerun refused at
332
+ the seed gates because `mypy` under `MYPYPATH=src` reports every error through aurora's tracked
333
+ symlink spelling (`src/aurora_spawner/…`) while the declared scope held the physical spelling
334
+ (`packages/spawner/src/…`) — the lexical filter dropped all 16 real error lines, and 0 matches +
335
+ exit 1 tripped the crash-stop. **The fail-safe did its job on first live contact:** without it, a
336
+ COUNTED ZERO against baseline 0 would have minted a fake green on a tree carrying 16 strict
337
+ errors. Fixed by resolving BOTH sides to physical identity via `realpathSync` **on a lexical miss
338
+ only**, per-call cache; unresolvable paths fall back to lexical (fail-open killed by sabotage AND
339
+ by a pre-existing shipped test), and a symlink resolving OUTSIDE every prefix stays excluded.
340
+ - Then hamr: *"fence fix approved, wire both."* The two remaining lexical sites get the same
341
+ mechanism through one shared `underPrefixEither` (lexical first, physical only on a miss; a
342
+ null physical resolution is the old fence byte for byte): `runFilesChanged`'s `allowPrefixes`
343
+ containment and `runPatternAbsentInDiff`'s scope. **The gap was REAL, not hygiene** — a
344
+ watched-failing test proved the `no-suppressions` guard (the F87 antidote) scanned NOTHING and
345
+ read GREEN over a real `@ts-ignore` when the scope was declared through a symlink spelling. A
346
+ fake-green lane, now shut; the widening direction (spelled inside, physically OUTSIDE) stays
347
+ shut, killed by sabotage plus three shipped tests. `globToPrefix`, the `..`-prohibition and the
348
+ crash-stop are untouched. **Accepted limit documented in-code:** a DELETED file inside a
349
+ symlinked package has no realpath and falls back to lexical — a possible false red on
350
+ aurora-style deletions, which is the fail-safe direction, left until a live run trips it.
351
+ - **On a RESUME the work branch is prepared BEFORE the $0 instruments** (`src/planrun.js`). Steps
352
+ 0a (close precheck) and 0b (check preflight) are readings OF A TREE, and on a resume the tree
353
+ that matters is the recorded work branch — it holds the work already paid for and IS the run being
354
+ continued — but the seam that puts the run on it sat at 0c, below both. Standing on the handed
355
+ ref, the precheck could read `already-green` off a tree that is not this run's, and every
356
+ `baseline: "seed"` stage would baseline against it: **an instrument reading the wrong subject,
357
+ reading honestly and saying nothing true.** One ordering split, one seam — `prepareBranch` is
358
+ defined ONCE (the branch-red refusal is written once, so two call sites cannot drift into two
359
+ messages) and called before 0* on a resume, at 0c otherwise. The cold ruling is untouched: an
360
+ already-green COLD tree still returns above 0c and leaves no branch behind, because that clause
361
+ is about MINTING and the resume arm creates nothing (`created: false`). A resume whose branch is
362
+ gone or foreign now stops `branch-red` before the arbiter grades anything at all — earlier, free,
363
+ deterministic. (Reported by `/code-review` and deliberately NOT fixed there: the ordering is ruled
364
+ territory, parked for hamr's word and landed on it.)
365
+ - **The persisted-copy scrub class closes its 3rd, 4th and 5th instances** — a string reaching a
366
+ record that OUTLIVES the run by a path the single redaction boundary does not cover.
367
+ - **3rd, both declaration gates:** validator reds were persisted VERBATIM — and gate 1b quotes
368
+ the SEED LISTING via the did-you-mean helper, so a secret-shaped FILENAME in the patient tree
369
+ reaches the persisted red through a channel the spec sweep structurally cannot see (measured:
370
+ spec clean, red leaks). Fixed at both gates with one shared `scrubRed` over every string field
371
+ — a detail-only fix was sabotage-proven INSUFFICIENT, because the structured `cmd` field ships
372
+ verbatim — plus symmetry wraps on the provider-derived channels.
373
+ - **4th, `src/authorscout.js`:** `record()` scrubs a raw at CREATION, when the verdict is not yet
374
+ known, and the verdict was ASSIGNED onto the stored object afterwards. `reason` is the worst
375
+ field to do that to — `classifySurvey`'s `CALL_FAILED` route quotes the transport's own error
376
+ prose verbatim, a string nobody here wrote and nobody bounds — and the raws land in
377
+ `authored.json` and on the spine. **The scrub had become a property of WHEN you knew the
378
+ verdict.** Fixed by making the stamp a FUNCTION rather than an assignment (`stampRaw` in
379
+ `src/text.js`, which `scrubRaw` renders its own two fields through): one place decides how a
380
+ diagnosis becomes persistable, exactly as one place already decided it for the text. `cause` is
381
+ enumerated and passes through untouched — masking a closed set would only hide a bug in the set.
382
+ - **5th, `src/authorjob.js`:** `notes` is the one FREE-TEXT field a model writes straight into
383
+ the SIGNED `closeDecl`, and it was the only string on that boundary not riding `redactSecrets`
384
+ while every sibling does. No live leak today (inputs are pre-scrubbed upstream and
385
+ `sweepSecretLiterals` reds a secret-shaped note on the way to signing), so this is the
386
+ defense-in-depth half of the class. Hashes are untouched: `redactSecrets` returns a
387
+ non-matching string byte-identical, and no fixture or signed spec carries a secret-shaped note.
388
+ - Every regression watched failing first with the RAW TOKEN in the actual, then watched passing,
389
+ then each fix sabotaged in turn and its test watched red again; assertions demand
390
+ `scanSecrets(JSON.stringify(…)) === []` **plus the mask and the surviving prose around it** —
391
+ masked, never deleted.
392
+ - **The whole-branch review's fifteen, in three shrinking rounds** (F91). Round 1: the genre-env
393
+ envelope survives re-validation; the command deny-floor lands in `checkKind`; `cleanup()` never
394
+ throws; the crashed counter (nonzero exit + 0 matched lines) routes to an instrument stop with
395
+ seed baselines included; `effectiveTimeoutMs` becomes tighten-only; plus a `renderSeedReadBlock`
396
+ scrub (a two-headed leak) and a tautological Layer-R trim test rewritten to be ABLE to fail.
397
+ Round 2, a fix-diff review OF round 1: `renderRejectBlock`'s scrub (a second leak channel of the
398
+ same class, found by reviewing the first fix's diff), genre-env checked at the GROUNDED gates,
399
+ `envCapableKind` consolidated so injector and validator cannot drift, and the crash-stop's
400
+ `grep -c` / pytest exit-5 blind spot documented as an accepted fail-safe limit. Round 3, a
401
+ validation sweep: guard-name-keyed `scopeOfJob` + `AT_MOST_ONCE_KINDS`, `resolveSourcePrefixes`
402
+ realpath-dedupe (measured live on aurora), the notes divergence closed, `parseValue`'s first
403
+ aggregate keeping its notes, scout recovery preferring `s2.error`, and `addedLines`' stat-throw
404
+ announcing a note. 19 new tests sabotage-proven; mutation micro-batch 5/5 killed.
405
+ - **`/code-review medium`'s five.** Validation reds are scrubbed WHERE THEY ENTER THE RECORDS — the
406
+ model-facing copy was masked and the persisted copy was not. The diff scanner's header skip is
407
+ scoped to the PREAMBLE, so an added `++` line can no longer hide a suppression from the guard.
408
+ Worktree cleanup READS git's result instead of assuming it. Plus the two caps above. Each fix
409
+ carries a regression test proven failing against the pre-fix code; the suite was independently
410
+ re-run rather than taken from the reviewer's own gate.
411
+ - **`/diff-review`'s ten land as nine fixed and one DROPPED with its reason.** Beyond the four in
412
+ *Changed* above: the dead `d.code !== 0` clause (git's `ok` IS `code === 0`; a spawn fault carries
413
+ `code: null`); one test running the same declaration through BOTH first-red-wins loops, since the
414
+ deliberate duplication had nothing pinning it; the collision walk's check-then-act NAMED rather
415
+ than closed, because `git checkout -b` already refuses an existing branch (measured, exit 128) so
416
+ the race ends in the stop below it; `scoutReaskTurn`'s rendered text; and `buildSeedListing`
417
+ rendering the `full` tier TWICE on every run that never needed to degrade. One finding **REFUTED
418
+ as a bug and kept as a fail-safe**: the retry ladder's re-survey arm is right — a toolless re-ask
419
+ rests entirely on *"the model already read the tree"*, and an empty transcript is that premise
420
+ gone — the HEADER was the thing that lied, so the header tells the truth and the branch gets the
421
+ test its always-non-empty helper had prevented. **DROPPED (S3):** every `process.exit(2)` in
422
+ `run-author` fires in the argument-parsing preamble, before the script's first stdout write, so
423
+ there is no queued output for F71 to discard — and `die` is used in expression position
424
+ (`return die(…)`), where `exitCode`+return would let a fatal unreadable-JSON CONTINUE into the
425
+ run; the post-output paths already use `process.exitCode`.
426
+
427
+ Suite **1422/1422** on a single clean run; `typecheck` and `build:types` exit 0.
428
+
429
+ ## [0.8.0] — 2026-08-07
430
+
431
+ ### Changed
432
+ - **The variance meter REPORTS progress — and still decides nothing on it** (F85, A+B;
433
+ `src/planrun.js`). The meter stopped a step that was CONVERGING and then told the redrafting
434
+ planner that step's exits were *"unmoved"*. Two instruments over one run, and only one of them
435
+ right. Live on `u-msh70zla`: the close gaps went 30 → 24 → 15 → 14 with the ladder recording
436
+ `distinctGaps 3` / `strikes 0`, while the replan brief said nothing had moved and the materials
437
+ said *"0 step(s) completed"* — so the replanner, told it had achieved nothing, rebuilt a file
438
+ that was already at zero errors, wrote nothing twice, and the run died with $1.09 and 7 minutes
439
+ unspent. hamr's ruling, verbatim: *"meter is right but missing a piece … it should give heads up
440
+ on money/time + progress for llm to judge."*
441
+ - **The firing condition is byte-identical** — `moneyShare`/`timeShare >= 0.5`, both axes, the
442
+ same threshold, the same throw. A progress term in the TRIGGER would make a governance
443
+ instrument over an operator-owned allowance judge capability, so a converging step that is
444
+ eating the run is still stopped. What was missing was never a term in the condition; it was
445
+ the READING.
446
+ - **The reading is read off `src/trend.js` — the SAME instance the money halt reads.** Nothing
447
+ new is computed at the meter: no second reader, because two instruments answering one question
448
+ is the exact defect being fixed. Per-stage, stage names and numbers only, and `unknown` stays
449
+ `unknown` — a close that reports no number donates nothing (F6).
450
+ - **The hardcoded `with its exits unmoved` string is DELETED.** It printed on every step-variance
451
+ stop whatever had happened, and on `u-msh70zla` it was simply false. The escalation detail —
452
+ which a human reads and which the replan brief quotes — now states the meter's fact (*a share of
453
+ the run was consumed*) and then the trend instrument's measured reading, as two separate claims
454
+ rather than one fused verdict on the work. The materials `progress` line keeps its structural
455
+ sentence (*"step N of M did not finish; K step(s) completed before it"* — a plan's shape is what
456
+ the planner re-allocates) and gains the close trend beside it.
457
+ - **Spine change:** the `variance` record gains `trend` / `motion` / `reading` / `series` — the
458
+ same four names and the same shapes the money halt already emits, so one reader parses a
459
+ progress reading wherever it appears. ADDED fields, never repurposed ones (the spine is
460
+ append-only forever). Measured post-fix on `u-mshcpdg4`, iteration 3 on `finish-strict-typecheck`:
461
+ `moneyShare 0.497`, `timeShare 0.769`, `axis time`, `trend "converging"`,
462
+ `reading "still progressing — typecheck 30 → 15 → 15 → 1"`,
463
+ `series [{stage:"typecheck",values:[30,15,15,1]}]` — against `u-msh70zla`'s pre-fix record, which
464
+ carried the shares and nothing else.
465
+ - **`runTrend` was blind to everything a STEP achieves** (F85, the prerequisite found en route). The
466
+ run's trend reader was fed only the close PRECHECK and the outer fix loop, so a meter firing
467
+ mid-step had nothing to report about a step that had just gone 24 → 15 → 14. Two folds corrected:
468
+ a step's failing check is an arbiter-rendered close grade (same `runStages`, same chain, same stage
469
+ names) and is now recorded like one; and it is recorded on the RAW gap, because `evalExits` wraps
470
+ stage output as `check "<name>" red: …` and that wrapper line carries the word "red" with no number
471
+ on it — so every step grade used to donate a null. Measured on `u-msh70zla`'s own archived gaps:
472
+ raw reads `typecheck 24 → 15 → 14`, wrapped reads nothing at all. Plus a **preflight seed** so the
473
+ counter has a baseline: the precheck reds at the close's FIRST failing stage (first-red-wins) and
474
+ every shipped close opens with `changed-from-seed`, so the numeric stages had no baseline at all and
475
+ a run would read `unknown` on exactly the stop the readout exists for. Seeded ONCE PER STAGE, off the
476
+ reader's own books — the precheck and the preflight menu grade the same unchanged tree back to back,
477
+ and a repeated baseline in a series reads as an attempt that achieved nothing, which is the phantom
478
+ flat step this change exists to remove.
479
+ - **`tests/resume-u.test.js` is decoupled from the live job spec.** It read
480
+ `jobs/bareagent-u-types.json` while hardcoding $8 / 45min, so an operator budget edit on a live spec
481
+ turned the suite red. Every number now derives from the spec it reads, proven by passing at a third
482
+ budget value. Suite: **1023 tests** (1027 minus the 4 deleted with the `never wrote` feature below);
483
+ typecheck and `build:types` exit 0.
484
+ - **The per-step model tier menu narrows to `['sonnet']`** (`STEP_MODELS`, `src/plan.js` — a PUBLIC
485
+ surface: a plan declaring `model: "haiku"` is now REDDED by `validatePlan` with `invalid-value` at
486
+ `steps.N.model`, carrying the menu). hamr's order, verbatim: *"haiku should be dropped to see if it
487
+ passes, if it's agent poor work or harness, like always."* **This is a reversible ATTRIBUTION PROBE,
488
+ not a finding that haiku is incapable** — what the $0 archive read behind it shows is a CONFOUND, in
489
+ both directions. Over 186 spines / 71 accepted plans / 255 steps, only **20 steps ever declared a tier
490
+ at all**: haiku **12** — 9 of them on a REPLANNED plan, 6 the LAST step of their plan, round bounds
491
+ 3–12, **2 ever green** — against explicitly-declared sonnet **8**, 2 on a replan, 3 last, bounds 14–32,
492
+ **5 green**. The planner reaches for haiku on steps it judges mechanical, and a replanned step sits on
493
+ a run already in trouble, so neither column is a controlled read; and all three bareagent-u runs of
494
+ 2026-08-06 tiered their replanned step down (haiku/6, haiku/8, haiku/12) and all three failed. That
495
+ confound sat directly on top of the programme's most recent capability read (F86's tail). Removing the
496
+ tier is how it gets attributed: with one tier expressible, a still-failing replan is agent or harness,
497
+ never the tier.
498
+ - **The `model` field and its validator branch STAY.** A one-entry menu means a plan — or a stored
499
+ bridge — declaring `model: "sonnet"` still validates, and restoring haiku is a one-token edit. No
500
+ stored bridge declares haiku (verified).
501
+ - **The drafter prompt's `model` line is DELETED** rather than reduced to a one-item menu: it
502
+ advertised *"a cheaper tier for mechanical steps"* and there is no cheaper tier now, and reciting a
503
+ single legal value spends tokens to invite a no-op field (`attempts` is the existing
504
+ legal-but-not-offered precedent). Verified by RENDERING the real prompt — 0 lines match
505
+ `/model|tier|haiku|sonnet|cheaper/i` in either the fresh or the replan form.
506
+ - **Only what the AGENT may express narrows**, which is the safe direction. `--model haiku` remains
507
+ the OPERATOR's probe knob (PRD v1.36), untouched in `scripts/run-u.mjs`; two comments there called
508
+ the runner's tier map *"the same closed menu the planner uses"* and now say plainly that it is
509
+ deliberately wider.
510
+ - **No spec hash moved.** `jobSpecHash` covers the resolved spec, not `STEP_MODELS`; all 10 job specs
511
+ were re-hashed and none moved — bareagent-u and bareguard-u are still the hashes hamr signed.
512
+ - Measured on the first sonnet-only run against the last haiku-era one, same patient: **10.9 → 17.5
513
+ seconds per round** (1.6× slower), 120 → 87 rounds, and the failure CLASS changed — struck out at 2
514
+ remaining errors with money and clock left, versus still converging at 1 error when the wall
515
+ expired. Better rounds are slower rounds, and the binding constraint moved from the strike ladder to
516
+ the clock. That sonnet-only run was also the first to reach the outer close, where it was redded for
517
+ **silencing the type checker instead of typing the code** (F87's suppression genre) — the close
518
+ refused it, and the eventual green removed the suppressions.
519
+ - **`bareloop.context.md` named `sonnet | haiku` in two places and is corrected** — the shipped adopter
520
+ contract carried the old menu in the `steps[].model` schema row and in the `providerFor` (P) section,
521
+ so an integrating agent reading it would have drafted a plan that reds. Both now name the one-entry
522
+ menu and say why it is one entry.
523
+
524
+ ### Fixed
525
+ - **A locked-VERDICT `request-red` no longer files as a bare-agent upstream ask** (`src/job.js`,
526
+ `src/ledger.js`). `classifyIncidents` hardcoded `lib: 'bare-agent'` for every `request-red`, but the
527
+ code covers two territories: a locked TOOL verb (bare-agent, the original case) and a locked VERDICT
528
+ type (`soft-green`/`hitl`) — demand against bareloop's OWN `VERDICT_TYPES` menu, live-misfiled for
529
+ every `LOCKED_VERDICTS` declaration (the BA-2 misattribution class). Per the typed-lib rule the
530
+ territory is now stamped at the EMIT site (`Red` gains `lib?`; `verdictType` → `bareloop`, `tools` →
531
+ `bare-agent`) and the ledger reads it, keeping the hardcode only as the pre-field legacy-spine
532
+ fallback (zero archived rows carry the code, verified). The `suggestedAsk` template renders the
533
+ stamped target, so a bareloop-catalogue refusal can never seed a bare-agent ask; D13's genre refusals
534
+ (close-authoring design record) route by stamping their own lib with no ledger change. 5 new
535
+ failing-first tests; `capability-gap` still hardcodes bare-agent — flagged, structurally unreachable
536
+ today (a validation red returns before any provider call; the module header says so).
537
+ - **…and `capability-gap` no longer hardcodes it either — the flag above is CLOSED** (`src/ledger.js`).
538
+ `capability-gap` is `request-red`'s cap-halted form and inherits both of its territories, but the fix
539
+ landed at one of the two sites and not the other: the synthesis loop still wrote `'bare-agent'`
540
+ literally, and so did its `suggestedAsk` template — the one field a human actually files from, where
541
+ fixing only the occurrence's `lib` would have left the misattribution fully intact. The territory now
542
+ comes from the OCCURRENCE (the `request-red` rows already carry the lib stamped at their emit site),
543
+ and the **dedup key becomes `(verb, lib)` rather than the verb alone**: the ledger's own occurrence
544
+ key carries `lib`, so a `Set` over verbs would merge two territories into one row and attribute it to
545
+ whichever the set happened to hold first — an arbitrary target, which is the defect itself. One gap
546
+ per (territory, verb) costs nothing and each carries an honest one. **Reachability is unchanged and
547
+ still DORMANT** — `validateJob` returns not-ok on any red and `run.js` returns on a validation red, so
548
+ a `request-red` and a `cap-halt` cannot share one spine today; this is a latent-consistency fix at a
549
+ seam the module header documents as dormant, and it claims no live bug. 3 new failing-first tests,
550
+ each labelled in-file as a FUNCTION-CONTRACT fixture (`classifyIncidents` is pure) so a later reader
551
+ cannot mistake the synthetic stream for something a run can produce. Closed now rather than left
552
+ flagged because a fix that lands at one of two identical sites is the class this repo has already
553
+ paid for once — the ripgrep install that went into `ci.yml` and not `publish.yml`, which then failed
554
+ on the identical cause.
555
+ - **The replan ceiling reset on EVERY resume leg — a kill refilled a bound the operator signed**
556
+ (`src/planrun.js`, `src/run.js`, `src/reuse.js`, `scripts/run-u.mjs`). `replanned`/`varianceGrantUsed`
557
+ were locals in `runPlan`, and a resume is another `runPlan` call. Confirmed by execution before any
558
+ remedy: leg 1 replanned and stopped; leg 2, driven with a `resumeSeed`, replanned again — a run total
559
+ of 2 with every record showing 1. Two became four by being killed once, which is precisely the creep
560
+ the latch exists to make impossible (*"unlimited replanning launders thrash as adaptation"*, PRD
561
+ v1.12).
562
+ - **Fixed on the MONEY-FOLD precedent, not the grade-seed one, and the choice is the fix.**
563
+ `readGradeSeed`'s documented known limit is that it reads ONE spine, so a resume of a resume
564
+ inherits leg 2's grades and not leg 1's — the chain shortens by a leg per kill. That is fail-safe
565
+ for a READOUT (a short chain can only under-claim a direction) and it is the DANGEROUS direction
566
+ for a CEILING, where an under-claimed ledger is a refilled allowance. So each leg DECLARES the
567
+ ledger it inherited on its own spine (`priorSpentUsd`'s mechanism) and the next reader adds only
568
+ its own window's `replan` records. Leg 3 inherits the whole chain, not leg 2's slice.
569
+ - **The counter-argument was answered, not stepped over.** `src/trend.js` deliberately refuses to
570
+ seed the fix loop's ITERATIONS from history, and that stays right: an attempt allowance is a LEG
571
+ bound, and a restarted leg buys its own attempts with its own money. A replan ceiling bounds how
572
+ many times the WORKFLOW may be redrawn, which is the run's span — the same span the halt readout
573
+ already covers. The two seeds disagree on purpose and both now say so in-source.
574
+ - **SPINE FIELDS — `job-start` gains `priorReplans` / `priorReplanGrantUsed`.** Emitted only when
575
+ there is a fold (a decorative `0` is indistinguishable from a run nobody folded), on the same
576
+ declaration `priorSpentUsd`/`priorWallMs` already ride. The grant latch travels BESIDE the count
577
+ rather than being derived from it: a leg can inherit a spent ordinary ceiling with the arbiter's
578
+ extra still unearned (`1`, `false`), and deriving one from the other would collapse two real states
579
+ into one. `readResume`'s `direct` mode reads the fold off these very fields, so a U-path run
580
+ resumed twice inherits leg 1.
581
+ - **SPINE MEANING CHANGE — `plan-executed.replans` counts the CHAIN, not the leg** (and `replanned`
582
+ with it). **An adopter parsing spines must re-read this field.** The change was made rather than
583
+ shadowed by a second field precisely because `replans` shipped only on this unreleased branch, so
584
+ no archived spine carries the leg reading for a reader to be confused by. The leg's own number is
585
+ not lost: a leg emits one `replan` record per replan it drafts, so `chain − records-in-this-window`
586
+ is the fold it inherited — the same arithmetic `readResume` runs — and a second field stating a
587
+ derivable number would be a second reader of one question.
588
+ - **New option: `runJob(spec, { resumeReplans })`** (and `runPlan`'s identically-named option),
589
+ `{count, grantUsed} | null`, defaulting to `null` = the cold path, byte-identical to a run nobody
590
+ resumed. Belted like every declared fold entering the arbiter's arithmetic: a garbage, negative or
591
+ non-finite seed reads as `0` rather than poisoning later comparisons as `NaN`, and a negative one
592
+ can never cancel a real replan and widen the ceiling. `scripts/run-u.mjs --resume` and `runReuse`
593
+ both pass it. **The disarm control holds** — a resumed leg with an UNSPENT ceiling still replans, so
594
+ the bound is bounded and not switched off.
595
+ - **The replan brief's `closeGapBlock` re-bounded an ALREADY-bounded gap and deleted the red names the
596
+ first envelope had rescued** (`src/planrun.js`, `src/exits.js`). `src/exits.js` states the rule for
597
+ this same artifact one seam earlier: the check gap arrives already enveloped by `runClose`'s
598
+ `boundGap` under the stage's own `gapKeep`, which deliberately rescues the `not ok`/`FAILED` names
599
+ out of the elided middle — the mechanical gap F46's conversion mechanism feeds the worker. A second
600
+ 400/1500 envelope deleted exactly those a second time, which is F28 reintroduced; measured on a
601
+ 120-red-line close output, the first bound rescued **78** names and the second dropped **12** of
602
+ them. `CHECK_GAP_MAX` (12,000) is now **exported** from `src/exits.js` and imported here rather than
603
+ respelled, so the two seams cannot drift on how big a check gap may be, and it is applied as a
604
+ **backstop, not an envelope**: under it the gap passes through VERBATIM. The headroom is measured,
605
+ which is why the ceiling is not smaller — `runClose`'s own envelope tops out at ~10.1KB (400 head +
606
+ 8192 keep + 1500 tail + labels), and joined with a `tree-changed` detail that is ~10.2KB, leaving
607
+ ~1.8KB to spare, so the shipped path is untouched and the backstop fires only where `src/exits.js`
608
+ says it should: a seam that returned something never bounded at all. Length is measured AFTER the
609
+ secret scrub, because a mask is wider than most literals it replaces and bounding the raw string
610
+ would let a gap cross the ceiling on its way through the one transformation that always runs.
611
+ - **The replan brief's block was LABELLED as the close's own output but could lead with ours**
612
+ (`src/planrun.js`). `res.gap` is `lastGap` — the join of EVERY failing exit's detail in exit order —
613
+ and with `MAX_EXITS_PER_STEP` at 2 plus the mandatory `tree-changed` pairing on a write step, a step
614
+ that wrote nothing opens the block with the EVALUATOR's own prose (`0 files changed under src/** …`)
615
+ under a label reading *"the verification's own output"*. Captured verbatim from a real replan prompt.
616
+ The **label** is corrected to *"What this step's exits reported on its last attempt (their own
617
+ output, verbatim)"*; the **payload is deliberately kept** — "the step wrote nothing" is exactly what
618
+ a replanner should be told, and stripping it to make an old label true would trade a real fact for a
619
+ tidy sentence. F86's thesis (the artifact travels as TEXT, never parsed) is restated, not weakened.
620
+ - **F49's ReDoS reject now covers the OPERATOR's two regex fields, not just the agent's**
621
+ (`src/job.js`, `src/validate.js`, `src/plan.js`). `hasNestedQuantifier` shipped wired at exactly ONE
622
+ site — the agent-authored `artifact-written.pattern`. Two operator-authored patterns reach an
623
+ equally untimed evaluator and were uncovered: `judged.pattern`, exec'd by `runClose` against the
624
+ close's whole stdout+stderr, and `gapKeep`, compiled by `boundGap` and run **once per line** of
625
+ close output. Both now red `invalid-value` at the validation gate, before any tokens burn.
626
+ - **Measured, not argued:** `boundGap(stream, '(a+)+$')` against a 40-character line does not finish
627
+ in 20 seconds (`timeout` exit 124).
628
+ - **Why this is worse than an operator self-DoS, and F67 is the reason.** On the agent side F49's
629
+ own scope note holds — the agent authors both the pattern and the artifact, so a hang burns only
630
+ its own wall-clock and compromises no arbiter. These two run in **bareloop's own process**, so the
631
+ hang blocks the MAIN EVENT LOOP — and the in-process stall fuse is a timer in that same loop, so
632
+ it cannot fire (*a guard living inside the process it guards shares that process's fate*). The run
633
+ then dies to the OUTSIDE watchdog and reads as a model stall: **a bad regex in a SIGNED spec
634
+ presents as a provider problem.** The cost is a misdiagnosis, not just a stop.
635
+ - **The detector MOVED to `src/validate.js` — one inventory, never a second copy.** `plan.js`
636
+ imports `job.js`, so `job.js` cannot import `plan.js`; housing the scan with the other shared
637
+ primitives is what lets both documents reject through the same function. `plan.js` re-exports the
638
+ name (the `WRITE_VERBS` precedent), so the F49 site is byte-identical in behaviour. A second copy
639
+ would be precisely the drift class `SECRET_PATTERNS` exists to prevent — a shape one validator
640
+ rejects and the other admits. `hasNestedQuantifier` is not exported from `src/index.js`, so the
641
+ move is internal and breaks no adopter.
642
+ - **Admissibility swept before the change and re-run after:** all **92** operator regexes across all
643
+ **10** signed specs in `jobs/` — **zero** newly red. No spec breaks, no spec-hash churn (no spec
644
+ file is touched). The shipped shapes are pinned in the suite as the control that can produce the
645
+ negative: if the reject ever reds one of them, it has broken the live fleet.
646
+ - **Known limit, PARKED not chased:** the scan misses the alternation-overlap class (`(a|aa)+$`,
647
+ `(x|xy)*$` — no inner quantifier exists to find; the blowup comes from overlapping branches under
648
+ a repeat). Widening it changes which SIGNED specs are admissible, which is arbiter-adjacent.
649
+ Docked to the close-authoring rung, where hand-authored operator regexes may cease to be a surface
650
+ at all — see **PRD v1.55**. Recorded there too, so it is not re-chased: `([^])*$` is NOT caught and
651
+ that is CORRECT (`[^]` is the any-char idiom, so the pattern is linear); the genuinely nested
652
+ `([^]*)*$` IS caught.
653
+
654
+ ### Added
655
+ - **A SECOND variance stop can earn ONE more replan — granted by the ARBITER, on a mechanical
656
+ reading** (F85, C). A second `step-variance` after the one-replan ceiling was spent used to be a
657
+ hard stop, and on a run that was still converging that stop threw away work already paid for. Every
658
+ clause of the grant is a constraint: **ONE**, bounded by a LATCH rather than a comparison so the
659
+ ceiling cannot creep by arithmetic (unlimited replanning launders thrash as adaptation, v1.12) — a
660
+ third variance stop is the stop, however well it is going; **the arbiter's**, read mechanically off
661
+ the same `runTrend` instance the meter and the money halt read, with the agent given no channel to
662
+ it — it never asks, is never offered it, and cannot influence it (no self-adjusted budgets, ever);
663
+ **`converging` only**, the trend instrument's own category with no fresh number invented here
664
+ (threshold-setting is the operator's), so `flat` and `unknown` stop exactly as before and an unknown
665
+ is never rounded up into a reason to spend (F6); and **`step-variance` only** — an exhaustion or a
666
+ stall after the ceiling is unchanged. `remainingUsd() > MONEY_MIN` still gates it, as it gates the
667
+ ordinary replan. **Spine change:** `replan` gains `replan` (which replan this is) and — only when the
668
+ arbiter granted one past the ordinary ceiling — `granted` with the reading it was granted on, so a
669
+ reader can never mistake the ceiling for a grant; `plan-executed` gains a `replans` count riding
670
+ BESIDE the shipped `replanned` boolean, never repurposing it. Live on `u-mshcpdg4`: three
671
+ `plan-accepted` records — the initial draft and two replans, the second granted on `converging`.
672
+ - **`closeGapBlock(gap)` — the replan brief carries the close's OWN output** (F86; `src/planrun.js`).
673
+ `u-mshcpdg4` reached ONE remaining strict error and still died. The close gap named the work exactly
674
+ — `src/recurse.js(978,115)` — and the worker read it every attempt; the REPLANNER, which is the
675
+ component that chooses which files the next plan targets, saw only
676
+ *"still progressing — typecheck 30 → 15 → 15 → 1"*: a number with no address. It aimed the last plan
677
+ at `src/loop.js`, already at zero errors, wrote nothing twice, and took two strikes — with $1.10 and
678
+ 82 seconds still on the clock at the moment that replan was granted (the run itself ended $0.8174 and
679
+ ~6 seconds short of its caps). F85 gave the brief the trajectory; this gives it the artifact the trajectory
680
+ is a summary OF. Handed over as **TEXT**, with both boundaries reused rather than respelled: scrubbed
681
+ through the one secret inventory (`redactSecrets`/`SECRET_PATTERNS`) because a prompt is an egress
682
+ point and the next caller will not remember the upstream scrub, and bounded by ralph's own `boundGap`
683
+ — now **exported** from `src/ralph.js` for it, since a private slice would be a second truncation
684
+ scheme that drifts on exactly the property that matters (whether the red lines survive, and whether
685
+ the trim announces itself, F28); the close path still calls it with the stage's own `gapKeep`,
686
+ byte-identically. The red-set is `REPLAN_GAP_KEEP = '\S'` — every non-blank line — because a shipped
687
+ `gapKeep` is `^`-anchored (`^BAREAGENT `, `^red`, `^FAILED`) and the exit evaluator wraps stage output
688
+ in `check "x" red: …`, so the anchor no longer sits where the pattern expects it; a gap that reaches
689
+ here is ALREADY a red-set (`judge` builds it out of the failing exits only), so keeping every line is
690
+ the correct instrument, not a widened one. **No spine field** — `src/trend.js`'s rule is that no
691
+ record carries a close byte. An empty gap yields the empty string, so the brief renders
692
+ byte-identically to the pre-F86 one (a labelled empty section would invite the planner to explain an
693
+ absence the run never observed). Verified by execution, not by reading: sabotaging the wiring
694
+ (`gapBlock = ''`) turns the `u-mshcpdg4` regression test red (`not ok 121`); restored byte-clean.
695
+
696
+ ### Removed
697
+ - **`gapFilesNeverWritten()` and the `never wrote` replan advisory** (F86) — an EXPORT of
698
+ `src/planrun.js` is gone (it was never re-exported through the package entry, whose `exports` map
699
+ admits only `.`, so no adopter import breaks). hamr's order, verbatim: *"delete it and rerun
700
+ bareagent"*. The measured reason, on `u-mshcpdg4`'s own gap: the advisory resolved to
701
+ `src/loop.js` — the file already at zero errors — and said so as a DIRECTIVE, beside the artifact
702
+ that said the opposite; this worker follows directive prose (positive-scope confinement). It also had
703
+ no evidence of ever converting anything: it fired by construction on `u-msdsmkid`, and that run still
704
+ step-redded. The general rule it broke is why `closeGapBlock` hands over text instead: **a parsed
705
+ file list cannot tell a summary line from a detail line**. Line 1 of that very gap reads
706
+ `reports 1 error(s) in src/recurse.js, src/loop.js` — every file in SCOPE — and only line 2 names the
707
+ culprit; and the shapes differ per close anyway (tsc `file:line`, pytest test ids, a count close that
708
+ names no file at all). A model reading the artifact can make that distinction; a regex reproduces the
709
+ bug. Two readers of one question with the parsed one wrong is the same failure the variance meter just
710
+ had. Its 4 tests went with it, and its sole caller was the brief — no speculative code left behind.
711
+
8
712
  ## [0.7.0] — 2026-08-05
9
713
 
10
714
  ### Changed