bareloop 0.8.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,427 @@ 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
+
8
429
  ## [0.8.0] — 2026-08-07
9
430
 
10
431
  ### Changed
package/README.md CHANGED
@@ -43,8 +43,8 @@ it and never disappears.
43
43
  > under an un-gameable outer close and one budget it can only tighten. On the real-model
44
44
  > acceptance battery (F47) the emergent flow converted a job the same worker failed 4/4
45
45
  > before — 3/3 runs, clearing the owned bar every time, composing its own check exits. The
46
- > within-run ratchet (Layer R) ships armed-and-inert by measurement (F41). **In flight:
47
- > N3 — executed inheritance**, where a workflow that greened is kept, offered to the next
46
+ > within-run ratchet (Layer R) ships armed-and-inert by measurement (F41). **Then N3 —
47
+ > executed inheritance**, where a workflow that greened is kept, offered to the next
48
48
  > similar job, and improved as executed instead of drafted from scratch (the ladder's
49
49
  > kill-switch question). As of 0.7.0 N3's machinery **ships** — the bridge registry, the
50
50
  > operator-signed reuse envelope, the selection call and step-level resume are all in the
@@ -52,7 +52,14 @@ it and never disappears.
52
52
  > legal, never *beneficial*. The contrast that was to prove the lift is now retired (F88): a
53
53
  > validation rule hands every cold run the plan shape a bridge was carrying, so the gap it
54
54
  > would have measured is gone. Treat reuse as available and unproven — what a bridge should
55
- > carry instead is open.
55
+ > carry instead is open. **The rung in flight is close authoring** — the last layer where a
56
+ > human still wrote code. You answer a short interview about what *done* means; an LLM then
57
+ > composes a close **declaration** over a catalogue of stage kinds bareloop implements — never
58
+ > code, never prose parsed back — which is validated, run once against your untouched repo so
59
+ > every ruler is seen measuring something real, and gated before you sign its hash. It is
60
+ > proven live on both a JavaScript and a Python patient, and its designed failure mode is an
61
+ > honest refusal: a close that would grade nothing, or a stage whose population does not exist
62
+ > in your tree, comes back decision-ready instead of green.
56
63
 
57
64
  ## Quick start
58
65