bareloop 0.8.0 → 0.10.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,631 @@ 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.10.0] — 2026-08-13
9
+
10
+ ### Added
11
+
12
+ - **The close-AUTHORING pipeline gets a money ceiling** (`--budget`, `ceilingUsd`) — the lever
13
+ parked at v0.9.0 as arbiter territory, approved by hamr. Until now the flow **metered** spend
14
+ and nothing **bounded** it: the runner printed a total and no number anywhere could stop a
15
+ call. It has **NO DEFAULT** — omitting it runs UNBOUNDED and the runner **prints that** before
16
+ the provider is built, because a defaulted cap is a silent second ceiling (the `maxWallMs`
17
+ precedent) and an unbounded run must be a visible operator choice. A malformed value is an
18
+ error, never a silent fall back to unbounded. The ceiling **binds BETWEEN metered calls** —
19
+ the survey's attempts and its F59 recovery round, and the declaration loop's author call,
20
+ each revise, and each malformed-emission retry — because a cap that binds mid-call kills the
21
+ row before it can be graded (F45). ONE number reaches BOTH paid seams, and spend already
22
+ incurred folds in, so re-entering cannot silently widen it. **F6 keeps its own axis**: a
23
+ null/unknown cost never counts as $0, so spend that cannot be *known* stops as `pricing-red`
24
+ rather than passing silently — while known spend at or over the cap is `cap-halt` even when
25
+ the total is unknown, since that breach is certain on the priced half alone. The stop is a
26
+ governance stop, not an error: it names the cap and the spend, nothing retries, partial
27
+ artifacts stay on disk, and it is DISTINCT from `artifact-red` and `provider-red`. A survey
28
+ the ceiling stopped is named a money stop (`budgetStop`, typed cause `not-funded`, outside
29
+ `SCOUT_RETRY_CAUSES`) instead of blaming a model that never spoke. One predicate, one
30
+ spelling (`capStop`/`tallyCalls` in `src/text.js`, beside `priceOf` and for the same reason).
31
+
32
+ - **A resume may DERIVE `wall-halt` from a recorded `step-red`.** Only when that run's OWN
33
+ spine shows the wall crossed BEFORE the terminal was minted — a derivation from the primary
34
+ record, which never rewrites the recorded outcome and announces itself with a banner. The
35
+ terminal's NAME is what gates resume (`readResume`'s `resumableOutcomes`, which the operator
36
+ runner supplies as `cap-halt` / `wall-halt`), so a run mislabelled `step-red` past its own
37
+ wall silently lost its resumability. **It is a PERMANENT read-side safety net, not a
38
+ migration shim** (ruled 2026-08-13; the expiry question that was parked here is closed). The
39
+ defect that minted the shape is fixed at source, but spines are append-only forever, so the
40
+ population it protects can never shrink; it fires only on the run's own un-forgeable
41
+ `wall-bounded` record, cannot rescue a `step-red` that has no wall record in front of it, and
42
+ is a never-taken branch on a healthy spine.
43
+
44
+ ### Changed
45
+
46
+ - **The two revise-loop tighten-only clamps get a detector.** `maxRevisions` and
47
+ `structureRetries` have been clamped since `40672ae`, but nothing could *fail* if they
48
+ stopped clamping: MEASURED, three mutations survived the whole suite — including deleting
49
+ the `structureRetries` clamp outright, a straight revert of the fix that added it. The two
50
+ existing tests pinned only the ceiling and the floor, and neither can fail when the clamp
51
+ stops honouring values *between* them; the direction the rule exists to permit — the
52
+ operator **lowering** a bound — was the untested one. Seven tests now cover the middle of
53
+ both ranges, the non-numeric floor, and `Infinity` (a widening, which clamps to the ceiling,
54
+ matching `runAuthorScout`'s own reading). The three floors are pinned as *deliberately
55
+ different* (`SCOUT_ATTEMPTS` at 1, both revise axes at 0) so a later "harmonisation" has to
56
+ argue with a test. No production behaviour changed — this is the detector the rule was
57
+ missing (F45's class: a frozen rule without a wired detector is prose, not protection).
58
+
59
+ - **A stop past the wall never funds a replan draft, on ANY trigger.** W-2's *"no new step
60
+ starts"* now covers the drafting call itself (hamr: *"it's a replan that is doomed to
61
+ die"*), and the guard is trigger-agnostic rather than wired to one path. A declined grant is
62
+ not consumed. With time left on the clock every one of these paths is byte-identical to
63
+ before — pinned as a control.
64
+ - **The drafter is told its worker has no shell.** A replan wrote an action opening *"Run
65
+ `npx tsc --strict --noEmit`…"*; `run` is locked out of `TOOL_MENU` permanently, so the
66
+ worker obeyed as far as its verbs allowed — 25 reads and greps, zero edits, a whole step
67
+ spent hunting six errors by hand, on a plan that validated clean. The no-shell law now sits
68
+ in the DRAFTER prompt unconditionally, beside the exit-freedom law, with `read`/`grep`
69
+ glossed as the worker's only eyes; the scout and worker prompts already carried it, and the
70
+ one component that WRITES instructions did not. **A regex over action prose was explicitly
71
+ refused** (F86's anti-precedent): the same sentence is legitimate in an operator's signed
72
+ goal, so the instrument is a prompt register, never a matcher. F95.
73
+ - **The worker persona REGISTERS the arbiter's books instead of leaving them to be
74
+ discovered.** `PERSONA_TOOLS` now names `gate-audit.jsonl`, `.smoke`, `.litectx` and the
75
+ run's spine as always-denied — records of how the worker is judged, holding nothing about
76
+ its task — beside the absolute-path law it is the twin of, so it renders for every worker on
77
+ every grant (a `write`-only grant gets no component strategy paragraph at all and must still
78
+ be told). **The fence is unchanged**: nothing widened, no book became readable. The only way
79
+ to learn the rule was previously to spend rounds of a bounded attempt on it. F98.
80
+ - **Both signing surfaces show the GOAL and the judged stages as ONE reading.** F87's law is
81
+ that the goal must state everything the close judges while NOTHING derives one from the
82
+ other or checks them against each other — so the sole defence is the signer reading both
83
+ halves at once, and neither surface offered it: `run-author` printed the declaration and
84
+ never the goal, `run-u --approve` printed the goal and never the declaration. `run-u`'s gate
85
+ now names every close stage (name + kind) under the goal, **for BOTH spec forms** — the
86
+ pairing was first gated on `closeDecl`, so 10 of the 11 shipped specs (the command `close[]`
87
+ form) still met the signer with a goal and none of the stages that judge it, the exact
88
+ half-reading this entry claimed to end; `closeStagesOf(spec)` now renders either form, and a
89
+ command stage prints `[command]` rather than the catalogue kind name `command-exit` it does
90
+ not carry (spec hashes and exit codes byte-identical, proven live). `run-author`'s block moves to
91
+ **`scripts/author-readout.mjs`** (`declarationLines(spec)`) and gains the goal above it,
92
+ rendered from the RESOLVED spec — the bytes that get hashed. It moved because the block was
93
+ otherwise reachable only after a paid scout and a paid model call, and a readout no test can
94
+ reach is a readout nothing checks. An absent goal renders as ABSENT, never as a bare label.
95
+ Still a READING and not a validator — comparing the two is the move F87 forbids.
96
+ - **README rewritten value-first** (hamr's framing, straight to `main`). The pitch now leads
97
+ with the trust-but-verify lineage (the ralph loop, code machine MCP, and RLMs converged on
98
+ the same principle), the three kinds of done (green / softgreen / hitl, one everyday
99
+ example each), why the agent authors the workflow (forty-plus primitives — counted 51+
100
+ across the suite's real surfaces — and no one recipe picked), and "build it once, keep it"
101
+ (localhost UI, whole-workflow export with its self-healing harness). One `[WIP]` badge is
102
+ the only status marker; all mechanics, layer tables, findings numbers and rung history
103
+ moved out of the pitch — `bareloop.context.md` stays the contract, the README the pitch.
104
+ - **`step-stalled` joins the RESUMABLE halts** (hamr's go, 2026-08-13). A stall that trips with
105
+ time left and only reaches the replan gate after the wall has expired kept its NAME — `run.js`
106
+ keys the F44 spend floor on that exact outcome, and renaming it would report unknown spend as
107
+ an exact total — but the name is also what `readResume` gates on, so keeping it cost the run
108
+ its checkpoint. A `step-stalled` spine now previews as **RESUME**, folds the spend floor in as
109
+ `>=$x`, and re-enters at the recorded branch and step. A pure widening of the resumable set:
110
+ nothing else about the terminal moved, and `reuse.js`'s D4a path was checked unaffected.
111
+ - **The arbiter-book names get ONE home.** `gate-audit.jsonl`, `.smoke` and `.litectx` were
112
+ spelled independently in `isArbiterBook`, in BOTH fence deny lists, in the `PERSONA_TOOLS`
113
+ prose and in `run.js`'s smoke root — five sites, the exact drift class F98 had just paid for,
114
+ with the prose copy unguardable by any test. `kinds.js` now exports
115
+ `SMOKE_STORE`/`LITECTX_STORE`/`GATE_AUDIT_FILE`/`ARBITER_BOOK_STORES` and all five consume it.
116
+ Purely mechanical and verified as such: the deny-list joins and the persona prose are
117
+ **byte-identical** to the strings they replace, so no fence decision and no worker prompt
118
+ changed, and there is no import cycle (`kinds.js` sits below `tools.js`/`run.js`).
119
+ `tools.test.js`'s literal-spelling guard now asserts the fence consumes the constant — the
120
+ drift it guarded is impossible by construction.
121
+
122
+ ### Fixed
123
+
124
+ - **A time-stop is NAMED a time-stop — W-2 closes over the sibling terminals.** The ruling
125
+ (*"when time is up, keep the grade we already have and stop"*) was implemented on the
126
+ cap-halt path and not on the variance path: one run minted `step-red` **9.9 seconds past its
127
+ own wall**, having recorded the crossing in its own spine 5.6 seconds earlier. Now **every
128
+ variance and cap-halt terminal re-reads the clock before minting**, through ONE shared
129
+ `wallHaltTerminal` emission site — one site, one spelling —
130
+ and the ladder's cap-halt past the wall is a `wall-halt`. **`step-stalled` is pinned OUT of
131
+ the class by test:** `run.js` keys `spendComplete:false` on that exact outcome name because
132
+ an abandoned-and-reissued call may already be billed, and relabelling it would report a
133
+ floor as an exact total (F6 in a self-heal coat). **With both allowances exhausted the MONEY
134
+ cut is reported** (`cap-halt` wins over `wall-halt`) — hamr: *"understood"*; recorded so the
135
+ choice stays visible. F96 / PRD v1.59.
136
+ - **A declared `count-not-worse` red gap carries the LINES it counted, not just how many.**
137
+ `"8 match(es)"` with no file, no line and no error code is a number with nowhere to open —
138
+ F28's rule (*the close's output format is part of the contract*) recurring one rung later
139
+ inside the authored-close executor, with the `gapKeep` machinery it needed sitting in the
140
+ same file. `parseValue` now returns `matched`: the **KEPT** lines only — those that survived
141
+ the scope filter and reached aggregation, so a scope-DROPPED line is never named and the
142
+ worker is never aimed outside its own population (F84). The harvest is **per-term**, not
143
+ global: each term's kept lines nest under that term's own breakdown row — the one place its
144
+ `+n`/`-n` already prints — a `first` aggregate echoes only the one line it read, and dedup is
145
+ WITHIN a term, so a line feeding two terms plays both roles and appears under each. Harvested
146
+ globally, a negative-sign term's lines and a `first` term's never-read lines were echoed flat
147
+ as *the lines the count is made of*, handing the worker a line to go fix when that term
148
+ SUBTRACTED; labelling per term rather than dropping negatives keeps the subtracted term
149
+ visible, because under higher-is-better that is where the news is. The value is
150
+ still computed from the parsed values and never from the list. The lines ride the EXISTING
151
+ channel and invent none: the stage's `gapKeep` prefix on every line (Layer R's `redKeep` is
152
+ DERIVED from it), the existing `GAP_LINE_CAP`, and the announced trim on overflow.
153
+ **Nothing here can flip a signed spec hash** — verdict logic, baselines, counting,
154
+ `closeDecl` schema and `detail` untouched. Measured, not asserted: the identical job, same
155
+ patient, same hash, went from stalling at 8 and dying to **67 → 8 → 1 → 0** in three
156
+ iterations. F98.
157
+ - **A fence deny streak ends the ATTEMPT, never the RUN — `gate-red` is extinct as a mintable
158
+ terminal.** A converging run (close reading 46 → 10 → 8, no strikes) probed three of the
159
+ arbiter's own books, the fence denied all three CORRECTLY, bare-agent's BA-11 deny-spin
160
+ guard ended the loop, and the plan flow mapped that to a terminal `gate-red` — escalate,
161
+ never retry. The run died with **$1.77 and 21.6 wall-minutes unspent, on a fence that had
162
+ worked perfectly.** BA-11's stop now joins `max_turns` in the **bounded-attempt lane on BOTH
163
+ worker surfaces**: `attempt-bounded` carries the `reason`, the close judges the partial
164
+ work, the gap feeds forward, caps unchanged, the loop continues. **And the next attempt is
165
+ told which bound actually fired.** All three attempt bounds wrote the same bare iteration
166
+ number, so the following prompt rendered *"CUT OFF after N tool rounds"* for a denial streak —
167
+ false on cause and count, aiming the worker at its read budget instead of at the fence, while
168
+ `r.error` reached only the spine. The bound now carries `{iteration, cause, reason}`, and the
169
+ denial branch quotes the recorded `denied:<tool>` terminal verbatim — bare-agent returns the
170
+ tool name only, so no path and no streak count is invented — trimmed under `GAP_TRIM_MARKER`
171
+ and scrubbed once at capture, one scrub shared by prompt and spine (an append-only spine that
172
+ captures a key captures it forever). The round-bound sentence stays **byte-identical**, pinned
173
+ by block-equality after an `includes` guard survived a sabotage prefix. This is F32's routing rule
174
+ applied to a new stop cause, not a new one — a stop arriving AFTER gate-audited worker
175
+ writes is non-terminal by construction, and a denial is its most benign member. **Nothing is
176
+ widened**: the same actions are denied and the same audit rows written; a genuine scope
177
+ escape now costs one bounded attempt instead of the whole run. The category stays in
178
+ `EXCLUDED_ESCALATIONS` and in `ralph`'s passthrough decision table — that set is EXECUTABLE,
179
+ so dropping the name would re-file any future emission as a counted capability gap rather
180
+ than delete it. F98.
181
+ - **A malformed money ceiling is an ERROR at the library seam, never a silent UNBOUNDED.**
182
+ `capStop` read `'2.50'`, `NaN`, `Infinity`, `true` and `{}` as *no ceiling* while
183
+ `makeCostBook` advertised the same value straight back — the advertised and the enforced
184
+ budget were different numbers, the one thing a budget may never be — and the only guard
185
+ (`parseCeiling`) was CLI-side, so every library caller ran unprotected. A non-finite non-null
186
+ now throws **before the first paid call**: it costs $0 and propagates uncaught, so no catch
187
+ can launder it into an ABSENT survey. `null`/`undefined` stay the stated operator choice for
188
+ unbounded; `0` and negatives stay finite and cap-halt immediately, pinned as they were.
189
+ - **`budgetStop` latches only when the ceiling actually ended the ladder.** The recovery-seam
190
+ latch fired for EVERY final cause: a 1-turn transport death and even a healthy PRESENT survey
191
+ that spent past the ceiling both came back `budgetStop:'cap-halt'`, on a field whose own
192
+ docstring says *"the ceiling, when it is what ended the ladder"* (the second instance was not
193
+ in the finding). The recovery predicate is settled first now, and `capStop` consulted only
194
+ when a recovery call was actually PENDING. The genuine cap-plus-transport-death case keeps its
195
+ `cap-halt`, but the refusal detail **concedes the concurrent cause instead of contradicting
196
+ the `ENETUNREACH` it quotes** — and concedes it for every INCOMPLETE cause, not only transport
197
+ death: a SHORT/EMPTY/UNPARSEABLE survey whose repair round the ceiling refused was still
198
+ emitting *"the survey stopped on the authoring ceiling, not on anything it read"* directly
199
+ above the reply it was quoting. `CALL_FAILED` keeps the transport wording; `NOT_FUNDED` — no
200
+ call ever made — keeps the ceiling-only wording, the one case where it is true. No budget
201
+ semantics moved: the PRD already said only the former is why the loop ended, and the code now
202
+ honours its own sentence.
203
+ - **The authoring spine event rides its own stop.** `scripts/run-author.mjs` hardcoded
204
+ `type:'cap-halt'` on the governance readout even when the stop was `pricing-red` — the only
205
+ site in the repo where a spine event's type contradicted its own category, and a `pricing-red`
206
+ event carrying *"not under cap"* sends the operator off to raise a number when the repair is a
207
+ priced provider. The old spelling could also arm `ledger.js`'s capability-gap fuse on a run
208
+ whose wallet was never empty (latent — no script feeds author spines to `updateLedger` today,
209
+ but type-keyed slicing is exactly how F45's misread happened). The emit now spends
210
+ `authored.stop` for both fields, so meaning derives from the stop once.
211
+
212
+ ## [0.9.0] — 2026-08-09
213
+
214
+ The **close-authoring rung**. The close stops being a hand-written script and becomes a
215
+ DECLARATION over kinds bareloop owns: a user answers a six-question interview, an LLM
216
+ composes the declaration by calling a schema-derived tool, three mechanical gates run it
217
+ against the real repository, and a human signs the hash. Nothing here judges a close —
218
+ no LLM validates another LLM's close, at any point (D9). Design record (D1–D13, FROZEN):
219
+ `docs/plans/2026-08-07-close-authoring-design.md`; PRD v1.51–v1.58.
220
+
221
+ ### Added
222
+
223
+ - **M1 — `src/kinds.js`, the kind executor: four kinds under three inherited runtime
224
+ contracts.** The four kinds (`command-exit`, `count-not-worse`, `pattern-absent-in-diff`,
225
+ `files-changed`) are the small part; the CONTRACTS every kind inherits are the
226
+ load-bearing part, and they live in the executor where no declaration can weaken them.
227
+ **Instrument-stop** (exit 97, `judged` withheld — a broken instrument is a CASUALTY, never
228
+ a red; and `first`-over-nothing is *unknown* where `sum`-over-nothing is a counted 0).
229
+ **The gap** (`gapKeep` on every line, trims announced and counted). **The changed set**
230
+ (diff plus untracked, minus `.litectx/` by PREFIX and `gate-audit.jsonl` by EXACT path —
231
+ so a worker-authored `src/gate-audit.jsonl` still counts). Graduated from the gate-2 POC
232
+ executor as a REWRITE: async spawn (`spawnSync` freezes the host loop the stall fuse lives
233
+ in, F68), caller-scoped seed worktrees under the OS temp dir rather than a module-global
234
+ cache inside the package, and the arbiter's own `CLOSE_ENV_DENY` imported rather than
235
+ re-spelled. **D12 rides on top: the close stores the COUNTING RULE and never a number** —
236
+ every run measures its own seed, with the route that measured it recorded. 49 integration
237
+ tests over real temp git repos and real child processes.
238
+ - **M2 — the catalogue, the TYPES genre, and the declaration validator: a path is SELECTED
239
+ from the tree, never derived from prose.** One module, because the catalogue, the genre
240
+ and the validator are one contract read from three sides, and a second spelling of any of
241
+ them is the drift class the parser normaliser already exists to prevent.
242
+ - **`KIND_CATALOGUE` IS the whole vocabulary.** The four live kinds are keyed to the
243
+ executor's own `LIVE_KINDS` and asserted equal in BOTH directions. `judged-floor` and
244
+ `human-confirms` are LOCKED menu entries — a distinct `locked-kind` red at validation,
245
+ before any tokens, carrying `verb` + `lib: 'bareloop'` so the demand is counted on
246
+ structured fields and never filed against a bare-suite package (the BA-2 class).
247
+ `harness-loop` (TESTGEN) is **ABSENT** — no entry at all, so declaring it is an
248
+ unknown-kind typo, which is the honest reading of a kind that is out of v1.
249
+ - **The genre template is FROZEN TEXT.** The suite reads addendum 3 out of the prereg and
250
+ compares BYTES, so a paraphrase in source fails the build rather than quietly changing
251
+ what every future close is authored against. The JS 7-pattern and Python 5-pattern
252
+ suppression batteries come from the hand-written closes' own `SUPPRESSIONS` tables —
253
+ operator-owned genre knowledge now. Two residues land as genre DATA, ours to inject and
254
+ never model-filled: guard **SCOPE** (`no-suppressions` ships with NO scope and nothing
255
+ left to fill — the POC's arm A narrowed it to the two target files, which reads stricter
256
+ and is not, because a suppression in a new helper walks straight through; narrowing an
257
+ injected guard is a red, not a taste) and genre **ENV** (`MYPYPATH` is expressed here and
258
+ applied by the flow — it moves no seed number, so no seed-verdict read can ever catch its
259
+ absence, and a declaration that authors it itself reds `genre-owned-env`).
260
+ - **The validator**: schema, kinds, params, F49's static nested-quantifier reject over
261
+ every model-authored regex (the detector itself untouched — monotonic-only), the parser
262
+ through M1's normaliser verbatim, and the **F84 one-population law in both halves** (a
263
+ count over a scoped job must NAME its population; the same cmd+parser+scope declared
264
+ twice is one number wearing two stage names — with the in-scope/outside split, the thing
265
+ the law exists to protect, asserted to stay legal). Plus **hamr's listing rule**: every
266
+ path-like param must SELECT from the seed listing. The POC's arm A derived
267
+ `src/alertEmail.js` from the user's prose and silently reclassified 15 real errors into
268
+ the wrong population; no READING of a declaration catches that, and a prefix match
269
+ against `git ls-tree` does. The red hands back what DOES exist beside the invented path —
270
+ named items, counted, capped (F38/F39), never a paragraph.
271
+ - Three preconditions fail CLOSED, all guarding one failure — *a validator handed nothing
272
+ to check against reports a declaration it never examined*: `listing` and `guards` absent
273
+ are reds, and `envOwned` splits ABSENT from EMPTY (`[]` is a genre that owns no
274
+ variables; `undefined` is a caller that never asked — F59's distinction in the shape it
275
+ bites here). One disagreement the tests caught and the code won: a bare `**` is **NOT a
276
+ root** — `globToPrefix` collapses only the `/**` SUFFIX, so admitting it would put the
277
+ validator and the grader on two spellings of one scope (the F9 red class): green at the
278
+ gate, and a containment guard that reds every file at runtime. It reds. 46 tests TDD,
279
+ watched failing first; 54-mutant battery, ZERO survivors (six holes closed on the first
280
+ pass).
281
+ - **M3 — the declaration is CALLED, not written** (`src/authorscout.js` LOOKS,
282
+ `src/authorflow.js` WRITES; neither ACTS — D11's split). The load-bearing change against
283
+ the gate-2 POC is hamr's schema-forcing ruling: the model answers by calling
284
+ **`declare_close`**, whose input schema IS the declaration shape and is DERIVED from the
285
+ catalogue — kind list, locked-ness, parameter names and every enum read from M1/M2, with
286
+ `schemaCoverage` failing the build loudly if the catalogue ever grows a parameter the
287
+ schema cannot express. A malformed emission is an artifact-red on its own bounded axis; the
288
+ tolerant text parse survives only as a marked fallback for a provider with no tool mode.
289
+ - Two consequences named rather than smoothed: **a locked kind now has no schema branch, so
290
+ declaring one is INEXPRESSIBLE** — which means M2's `locked-kind` demand counter is
291
+ reachable only from the interview layer or the text fallback, and the header says so
292
+ instead of pretending the channel is intact. And the tool is an OUTPUT CHANNEL that takes
293
+ no action, so D3's "no tools at all" is untouched: **hamr's law is about actions, not
294
+ syntax.**
295
+ - **Measured, both arms against the real `Loop`:** stopping the loop inside the tool costs
296
+ 1 provider round and $0.001; unwired costs 2 and $0.002 — tool mode does not cost double
297
+ what text mode did.
298
+ - Three defects found by the tests before a run found them: a FLAT directory beat every
299
+ listing tier (200 names render as 200 names at "counts-only"), so the prompt ingredient
300
+ was unbounded — names now give way, counts never do, with a backstop bounding the block
301
+ that beats every tier; an unreadable listing returned no `files` key at all, which
302
+ destructures to `undefined` and reads as falsy-empty (the exact absent-renders-as-empty
303
+ shape this module exists to refuse); and the genre-env injection rewrote stages it had no
304
+ reason to touch, making the signed form differ from what the model wrote for no stated
305
+ reason. **F59 is enforced five ways and refuses at $0** — an ABSENT survey, an EMPTY facts
306
+ object, an unreadable listing, an empty tree and an unbuildable genre variable each stop
307
+ the flow before a token is spent. 1222/1222; 40 of 41 mutants killed.
308
+ - **M4 — the close is a SIGNED FIELD, not a compiled script** (`closeDecl`,
309
+ `src/declaredclose.js`, `src/authorjob.js`). **The integration decision is made in the open
310
+ and named in the source:** a declared close is carried by a new spec field and executed by
311
+ M1's own kind executor. The cheaper alternative — render each declared stage as a shell
312
+ one-liner and let the existing `close[]` path run it — is REJECTED, and the rejection is the
313
+ point: it would turn owned kinds back into authored shell strings, which is exactly what D3
314
+ makes inexpressible. A declaration whose meaning is a generated `sh -c` is a script with
315
+ extra steps, and the arbiter would be judging text nobody validated.
316
+ - **Two executors, ONE contract.** Every behaviour the command path already had is pinned
317
+ by a test on the declared path: first-red-wins with the deciding stage naming itself; the
318
+ forbidden zone routed by a TYPED fault (`STOP_FAULTS`, stamped in M1 at the site that
319
+ OBSERVED it) so a timeout still offers *"raise the close timeout"* and a spawn failure
320
+ still offers *"fix the close"*; the gap header from ONE template (`stageGap`, now exported
321
+ — `src/trend.js` parses that exact line); the scrub at the emission boundary; and the
322
+ check menu still derived from stage names, one hop, one direction.
323
+ - **`close` and `closeDecl` are ALTERNATIVES, refused rather than merged**
324
+ (`close-duplicated`): a spec with two closes has two arbiters, and *picking one silently
325
+ is how a signed artefact stops meaning what the signer read*. The declared close is
326
+ HARD-class in the SAME table the close types use (`CLASS_BY_CLOSE.declared`), so a locked
327
+ verdict on one reds twice — as counted demand AND as `close-hierarchy`.
328
+ - **The gate is SPLIT honestly, not skipped.** A job spec is validated with no repository
329
+ in hand, so the listing rule and the scoped-job derivation that arms the F84 law are
330
+ DEFERRED there — `deferListing: true`, spelled literally, with `grounded: false` on the
331
+ result — and the RUNNER re-runs them against the real seed before any stage and before any
332
+ token. Proven both ways: the invented path `src/alertEmail.js` passes the spec gate and
333
+ dies at the runner, with **zero provider calls**.
334
+ - **D8/D12 ride together.** The seed is HEAD at run start, READ and recorded on a
335
+ `close-decl` spine event, never typed and structurally absent from the signed spec —
336
+ `seedRef` is an unknown-field red on `closeDecl`, because a baseline frozen at signing
337
+ judges run 5 against run 1's tree. `baseline: "seed"` is measured at each run's own start,
338
+ every run.
339
+ - **Two things the bridge adds**, because the declared executor knows more than an exit code
340
+ does: a `gapKeep` derived from one prefix constant, so Layer R's red-set has an instrument
341
+ on this path too; and a TREND VALUE donated **only** for `lower-is-better` — the omission
342
+ is the load-bearing half, since `trend.js` reads improvement as `value < best`, so donating
343
+ a higher-is-better test-count floor would read a DROPPING count as convergence and
344
+ recommend a top-up on a dying run. Null is the honest answer (F6); the stage position still
345
+ travels.
346
+ - **F90, two findings, neither found by a failure.** The design record's one named defect
347
+ (D13's ledger request-red misattribution) was ALREADY FIXED on `main` four days before the
348
+ record was written, so what M4 owed was the PROOF the new emit site routes through it — a
349
+ regression test walking refusal → `refusalEvents` → `classifyIncidents` → `suggestedAsk`,
350
+ asserting the ask reads "bareloop:" and names no bare-suite package, with both halves
351
+ killed as separate mutants (*a frozen document is frozen against rewriting, not against the
352
+ world moving underneath it* — F63 in its cheapest form). And a SECOND gap renderer silently
353
+ blinded Layer R's trim detector: it refuses to compare a red-set from a trimmed window by
354
+ looking for ralph's own marker, and the kind executor announces its trims in different
355
+ words — the direction that MANUFACTURES a fixation reading out of a blind instrument. Fixed
356
+ by exporting the executor's marker and reading both. *The shared-marker guard held
357
+ perfectly and was still defeated, not by drift but by a producer the reader had never heard
358
+ of.*
359
+ - 46 new tests over real temp git patients, real child processes and real `runPlan` (the
360
+ runner seam is the highest-risk part of the module and nothing there is mocked). 1268/1268;
361
+ mutation battery 22/22 after one honest survivor — a test asserting `reds.every(…)` over an
362
+ array the mutant left EMPTY, the conditional-over-a-never-true-fixture trap in its
363
+ `.every([])` form.
364
+ - **The interview re-keys from GENRE to VERDICT CLASS, and the pick is a PROMISE** (PRD v1.57
365
+ §1–§2 — **this SUPERSEDES D4**). `verdictType` is a USER-facing RADIO (`green` | `soft-green`
366
+ | `hitl` — the spelling already shipped in `job.js`; the design brief's `softgreen` would have
367
+ flipped every signed hash). **v1 admits only `green`**; a locked pick returns the counted
368
+ `request-red` refusal BEFORE its questions run.
369
+ - `QUESTION_SETS` replaces `TYPES_QUESTIONS`. The green set is TYPES' **six** questions
370
+ byte-for-byte — they were already genre-neutral, so generalising meant DELETING the D13
371
+ genre-confirm slot, not rewording. Locked sets carry `questions: null`, never `{}` (absent
372
+ is never empty, F59), and `questionsFor` THROWS on them: reaching questions means admission
373
+ failed to refuse. Genre understanding moves into `CLASS_STATEMENTS` fed to the composer, and
374
+ the genre refusal moves to the composer on the same counted path.
375
+ - **The guard battery re-homes to the class** (hamr: *"battery ties with job type"*).
376
+ `CLASS_BATTERIES` keys attachment — which guards, and their un-removability — off the class;
377
+ the tool-specific contents fill at COMPOSITION from the language the declaration names. **An
378
+ empty battery is IMPOSSIBLE, enforced not asserted**: unknown class, locked class, unknown
379
+ language, empty fill and zero guards all THROW.
380
+ - **Class-vs-ceiling, ruled *"yes to both, go"*:** every catalogue kind carries its
381
+ `verdictClass`, and a declaration whose ceiling EXCEEDS the pick is an honest
382
+ `class-ceiling` red naming the kind — never a silent upgrade or downgrade. Inert in v1 by
383
+ construction, and proven FIRING against an injected soft-green kind.
384
+ - Driver: **`--verdict` is REQUIRED** — a defaulted radio would answer the user's question for
385
+ them; `answers.json` drops key 7. Live smoke: `--verdict hitl` refuses at $0
386
+ decision-ready, `--verdict green` runs to the provider boundary. 14/14 mutants killed; the
387
+ saved PREPARED python spec still validates post-rework. 1354/1354.
388
+ - **The scout gets 3 typed attempts, and every raw lands in the books** (PRD v1.58). hamr's
389
+ rulings in the register, verbatim: *"retry on scout too, tighten-only same as revisions"*;
390
+ *"depending on causes, we can hardcode 3 attempts, but if reasons are beyond malformed then we
391
+ may verge into self-healing … i don't think we should"*; *"for audit, it should come on top of
392
+ existing audit trails … scrub for common things in general"*.
393
+ - `SCOUT_ATTEMPTS = 3`, hardcoded; the `attempts` parameter is TIGHTEN-ONLY and CLAMPS rather
394
+ than throws (floor **1** — a scout that never ran is an ABSENT nobody can act on). The gate
395
+ reads TYPED causes stamped at the detection site and **only the malformed class retries**
396
+ (`empty` | `unparseable`). What is excluded is excluded for a reason: `call-failed` covers
397
+ transport and `truncated:max_tokens` (provider-red, no redraft), `short` is F59's cut-off
398
+ population and already has its own instrument INSIDE the attempt, and `non-object` /
399
+ `empty-object` are valid JSON with vacuous content — a SEMANTIC failure, which F38/F39
400
+ measured as the same distribution sampled twice. **That is the self-healing line, and it is
401
+ not crossed.** The re-ask is TOOLLESS over the survey's own conversation, naming the
402
+ mechanical parse error and position and nothing else — the repository was already read, only
403
+ the emission was unreadable. **There is no JSON repair behind it and there never will be**: a
404
+ repairer decides what the model MEANT and writes it down as though the model had said it, in
405
+ the one artefact whose whole job is to be honest about what a repository contains.
406
+ - **`raws` — what every model call actually SAID**, the scout's attempts absorbed alongside the
407
+ declaration's, in the cost book's own order and under the same labels it meters. `scrubRaw` /
408
+ `RAW_PERSIST_MAX` (8000) / `RAW_TRIM_MARKER` join `src/text.js` as the third one-spelling
409
+ model-output helper — a NEW trim spelling deliberately, because `GAP_TRIM_MARKER` has a
410
+ reader with a different meaning (F90). One recorder writes the cost entry and the raw
411
+ together (labels pinned pairwise by test), the cut is walked back off a continuation byte so
412
+ a multi-byte character is never split, and **raws are present on the `$0` preflight refusal
413
+ paths too** — the path that spends nothing more is exactly the one whose evidence used to die
414
+ with the process. `iterations` records what a turn MEANT; `raws` records what it SAID, and a
415
+ malformation is only ever visible in the second. Attribution fix found mid-build: the
416
+ classified blob is tracked BY INDEX, not `raws.at(-1)` — an F59 recovery discard had
417
+ mis-stamped the verdict. E2E replay of `mslhn707`'s real failure shape: 3 labelled calls,
418
+ raws carrying causes and attempt indexes, reason *"— after 3 attempts"*, and a planted
419
+ key-shaped secret scrubbed to 0 across the serialized artifact. 1405/1405.
420
+ - **The WORK BRANCH is a hard rule, enforced STRUCTURALLY** (PRD v1.57 §3; `src/workbranch.js`).
421
+ hamr's ruling, built as ruled: *"The agent creates a NEW BRANCH before it touches any code — a
422
+ HARD RULE, no exceptions… Named with a meaningful slug."*
423
+ - **Structure over ordering:** `mkWorker` — the single seam that grants write-class verbs —
424
+ THROWS a categorised interpreter-red if asked for a writable worker with no branch prepared.
425
+ That covers both write sites and any future third, which an ordering rule cannot. The seam
426
+ sits at planrun step 0c: after the $0 precheck/preflight (an already-green tree leaves no
427
+ branch behind), before the scout (every branch fault is an instrument stop costing zero
428
+ tokens).
429
+ - The slug is `bareloop-` + the SIGNED spec's `job` field, kebab-validated and INSIDE the spec
430
+ hash, so the name cannot drift from what was signed (goal prose was rejected as a source, and
431
+ the rejection is documented). `WORK_BRANCH_RE`'s alphabet makes `..`, `@{`, `.lock`, a leading
432
+ `-`, and **`main`/`master` inexpressible**; `prepareWorkBranch` re-validates even a
433
+ hand-passed name. Collisions take `-2`/`-3` suffixes (one blast radius per try). **Resume
434
+ returns to ITS OWN recorded branch** (a `work-branch` spine record; `readResume`'s
435
+ `restart.branch`), and a recorded branch that is GONE is a STOP, never a fresh start — the
436
+ surviving mutant taught the assertion to demand the arbiter's own wording. **`branch-red` is
437
+ a distinct terminal, excluded from ledger classification** (an interpreter-red would aim an
438
+ upstream ask at a library for a patient nobody prepared). Adjacent fix: `relay` now passes
439
+ the category through instead of filing wiring faults as transport casualties.
440
+ - **Consequences recorded in `bareloop.context.md`, hamr-agreed:** the patient must be a git
441
+ repository with at least one commit, AND is always a separate COPY of the treated repo, never
442
+ the original — *"keep patients copies separate from original, always"*; the copy is the blast
443
+ radius. A detached HEAD attaches (`from: null`); a subdirectory workdir branches in the
444
+ enclosing repo (pre-existing semantics, now on the record). 18 new tests against real tmp git
445
+ repos plus guard/resume coverage across `planrun`/`run`/`reuse`, watched failing first; 2
446
+ sabotages and a mutation pass. 1387/1387.
447
+ - **`scripts/run-author.mjs` — the authoring driver, now tracked**, and the paid proofs it
448
+ bought. **pulselog (JS) reached SIGNING PREPARED twice** ($0.25 / $0.22), with the trial gate at
449
+ 32s and a **byte-identical spec hash across cold runs**. **aurora (python) refused honestly
450
+ twice**, and both refusals were the product working: run 1 a real uncrafted `mypy` fatal
451
+ (symlink `MYPYPATH`), run 2 the scope filter dropping error lines reported through a symlink
452
+ spelling — where the crash-stop correctly refused a 0-count instead of minting a fake green on a
453
+ tree carrying 16 strict errors. The fix then converted that refusal **for $0 on the same saved
454
+ declaration**: SIGNING PREPARED, all three gates green, `typecheck-spawner` honest RED at seed
455
+ (16 vs 0), `specHash` byte-identical to the failed run's own hash. Second sitting, $0.59 (F93):
456
+ `mslsnnzk` $0.345 diagnosing, `mslwbkz7` $0.245 validating the fix live — same composed shape,
457
+ GREEN `value=0` with the dropped-count note, three gates PASS, SIGNING PREPARED on the reworked
458
+ code. F91 / F92 / F93.
459
+
460
+ ### Changed
461
+
462
+ - **`maxRevisions` joins every other cap as TIGHTEN-ONLY.** The bound predated the law:
463
+ `maxRevisions: 50` bought fifty revise rounds, and unlimited revising is exactly the
464
+ thrash-as-adaptation the prereg forbids. The clamp lives at the ONE seam inside `authorClose`,
465
+ so it is not a rule any caller can be trusted to keep — the loop reads a DERIVED cap and the
466
+ parameter never reaches it. The floor is **0** (author once, no revise round — already a legal
467
+ loop shape) rather than the scout's floor of 1, and a non-finite or negative value lands on the
468
+ floor instead of a bound the loop never enters.
469
+ - **`structureRetries` becomes tighten-only** under the constant that already claimed to be its
470
+ ceiling, and **`effectiveTimeoutMs` with it** — the same direction every cap in this system runs
471
+ in.
472
+ - **Absolute paths join the command deny-floor, rejection-only.** `checkKind` gained a command
473
+ deny-floor plus an absolute-path/traversal refusal during the review rounds; drive-absolute
474
+ spellings (`C:\…`) were the gap left in it. Monotonic: the floor only ever adds rejections.
475
+ - **The ten hand-written close scripts import `JUDGED_MARKER` from `src/kinds.js`** instead of
476
+ respelling `judged=1` as a literal (hamr-approved wiring). Output proven byte-identical by
477
+ EXECUTION on all ten; no signed spec hash flips (`jobSpecHash` covers the resolved spec object,
478
+ not script bytes). **Known new coupling, recorded not hidden:** close scripts now import the
479
+ `src/` graph, which matters if they ever ship standalone.
480
+ - **A GREEN declared close now reports what its stages announced** (`notes`). `translate()`
481
+ computed a stage's gap text and threw it away on GREEN and on INSTRUMENT-STOP — but M1's kinds
482
+ announce some things whichever way a stage lands (a declared env var that was dropped, the scope
483
+ filter's own arithmetic), so the bridge was dropping the **check-both-greens** audit trail on
484
+ the floor. Surfaced as `notes`, absent when empty, per stage as well as on the summary — and
485
+ deliberately NOT `.gap`, because every consumer downstream guards with `if (gap)`, so a green
486
+ carrying one would read as revision-worthy and a stop carrying one would look like a verdict it
487
+ never rendered. Verified inert: every spine reader keys on `verdict`/`gap`/`stage`, nothing
488
+ routes or bounds on this field, and **no verdict moves.**
489
+ - **`assembleSpec` REFUSES a draft that already carries the authored half** instead of clobbering
490
+ it. It used to drop a draft's `close` on the floor and stamp its own `verdictType` over the
491
+ operator's, silently — the case `src/job.js` already rules on in its own words (*"picking one
492
+ silently is how the signed artefact stops meaning what the signer read"*), one step earlier. Now
493
+ a named refusal over `AUTHORED_SPEC_FIELDS` (`close`, `closeDecl`, `verdictType` — the fields
494
+ the fold WRITES, named as data so the refusal and the fold can never disagree about which half
495
+ of the spec is which), and `run-author` asks the same question at **$0 BEFORE the scout**, so
496
+ the answer never arrives after the model has been paid.
497
+ - **`closeStagesOf(job)` is the ONE staging every close consumer reads**, widened to both fields
498
+ — a consumer reading `close` alone sees a declared job as CLOSELESS and offers the drafter an
499
+ empty check menu. `scripts/consolidate-bridges.mjs` was still calling `stageClose(spec.close)`
500
+ and refusing declared-close specs for the one reason untrue of them; migrated (a spec with
501
+ neither field still dies honestly).
502
+
503
+ ### Fixed
504
+
505
+ - **F93 — the broken-ruler guard read match counts AFTER the scope filter, so a LIVE tool over an
506
+ empty population read as a DEAD one** (`src/kinds.js`). `count-not-worse`'s crash guard summed
507
+ `breakdown[].matches`, which `parseValue` tallies after the scope filter has already dropped the
508
+ out-of-population lines. **Two entirely different worlds collapsed into that one number**: a
509
+ tool that crashed before it looked at the tree, and a tool that RAN and printed parseable
510
+ findings which all fell outside this stage's scope. Run `mslsnnzk` paid for it: `npm run
511
+ typecheck -- --strict` exited 2 and printed **67 real `error TS\d+:` lines**, every one under
512
+ `src/`, against a stage scoped `excludePrefixes: ["src/"]` — post-scope the count was 0, the
513
+ stage stopped as a crashed instrument, and **a good close was refused at gate 2**.
514
+ - The same two-faults-read-as-one class the `first` aggregate's own stop had already SPLIT in
515
+ its own comment; the `sum` path carried the unfixed twin. `parseValue` now reports
516
+ `preScopeMatches`, tallied where LIVENESS lives — before the filter has an opinion, kept +
517
+ unattributable + dropped alike — and the guard consults that. A genuinely silent non-zero exit
518
+ still matched nothing pre-scope and still stops: **the fail-safe is untouched in the direction
519
+ it was chosen for**, and the accepted limit for `grep -c` / pytest-5 stands unchanged. One
520
+ `parseValue` call site (`measure`) serves both the current tree and the seed worktree, so the
521
+ baseline-side measurement gets the same reading by CONSTRUCTION; `command-exit` and
522
+ `pattern-absent-in-diff` never touch it.
523
+ - The stop lane also stops throwing away its own diagnostics: `stopped()` takes the notes a
524
+ measurement had already computed and renders them under the stop, so a `first` term whose every
525
+ match was scope-excluded sends the reader to the SCOPE instead of hunting a parser bug.
526
+ - **Validated live, not asserted:** `mslwbkz7` composed the same shape on the post-fix code and
527
+ walked to SIGNING PREPARED — GREEN `value=0` with the dropped-count note, three gates PASS.
528
+ F92's shape-lottery framing is **refuted in part**: the model composed the same correct
529
+ inside/outside split 3/3 (systematic, not a roll); what varied was the AGGREGATE, and only one
530
+ of the two resulting stops was a defect. The attribution was corrected the same night off the
531
+ artifacts — the crash guard fired **once**, on `mslsnnzk`; `mslhpw2v`'s stop was the
532
+ `first`-path fail-safe working as designed. **The parked re-compose-on-refusal lever survives
533
+ as a build and loses this run as its justification:** a retry loop cannot heal a mislabeled
534
+ instrument.
535
+ - **The scope filter and the FENCE compare FILES, not spellings.** The live python rerun refused at
536
+ the seed gates because `mypy` under `MYPYPATH=src` reports every error through aurora's tracked
537
+ symlink spelling (`src/aurora_spawner/…`) while the declared scope held the physical spelling
538
+ (`packages/spawner/src/…`) — the lexical filter dropped all 16 real error lines, and 0 matches +
539
+ exit 1 tripped the crash-stop. **The fail-safe did its job on first live contact:** without it, a
540
+ COUNTED ZERO against baseline 0 would have minted a fake green on a tree carrying 16 strict
541
+ errors. Fixed by resolving BOTH sides to physical identity via `realpathSync` **on a lexical miss
542
+ only**, per-call cache; unresolvable paths fall back to lexical (fail-open killed by sabotage AND
543
+ by a pre-existing shipped test), and a symlink resolving OUTSIDE every prefix stays excluded.
544
+ - Then hamr: *"fence fix approved, wire both."* The two remaining lexical sites get the same
545
+ mechanism through one shared `underPrefixEither` (lexical first, physical only on a miss; a
546
+ null physical resolution is the old fence byte for byte): `runFilesChanged`'s `allowPrefixes`
547
+ containment and `runPatternAbsentInDiff`'s scope. **The gap was REAL, not hygiene** — a
548
+ watched-failing test proved the `no-suppressions` guard (the F87 antidote) scanned NOTHING and
549
+ read GREEN over a real `@ts-ignore` when the scope was declared through a symlink spelling. A
550
+ fake-green lane, now shut; the widening direction (spelled inside, physically OUTSIDE) stays
551
+ shut, killed by sabotage plus three shipped tests. `globToPrefix`, the `..`-prohibition and the
552
+ crash-stop are untouched. **Accepted limit documented in-code:** a DELETED file inside a
553
+ symlinked package has no realpath and falls back to lexical — a possible false red on
554
+ aurora-style deletions, which is the fail-safe direction, left until a live run trips it.
555
+ - **On a RESUME the work branch is prepared BEFORE the $0 instruments** (`src/planrun.js`). Steps
556
+ 0a (close precheck) and 0b (check preflight) are readings OF A TREE, and on a resume the tree
557
+ that matters is the recorded work branch — it holds the work already paid for and IS the run being
558
+ continued — but the seam that puts the run on it sat at 0c, below both. Standing on the handed
559
+ ref, the precheck could read `already-green` off a tree that is not this run's, and every
560
+ `baseline: "seed"` stage would baseline against it: **an instrument reading the wrong subject,
561
+ reading honestly and saying nothing true.** One ordering split, one seam — `prepareBranch` is
562
+ defined ONCE (the branch-red refusal is written once, so two call sites cannot drift into two
563
+ messages) and called before 0* on a resume, at 0c otherwise. The cold ruling is untouched: an
564
+ already-green COLD tree still returns above 0c and leaves no branch behind, because that clause
565
+ is about MINTING and the resume arm creates nothing (`created: false`). A resume whose branch is
566
+ gone or foreign now stops `branch-red` before the arbiter grades anything at all — earlier, free,
567
+ deterministic. (Reported by `/code-review` and deliberately NOT fixed there: the ordering is ruled
568
+ territory, parked for hamr's word and landed on it.)
569
+ - **The persisted-copy scrub class closes its 3rd, 4th and 5th instances** — a string reaching a
570
+ record that OUTLIVES the run by a path the single redaction boundary does not cover.
571
+ - **3rd, both declaration gates:** validator reds were persisted VERBATIM — and gate 1b quotes
572
+ the SEED LISTING via the did-you-mean helper, so a secret-shaped FILENAME in the patient tree
573
+ reaches the persisted red through a channel the spec sweep structurally cannot see (measured:
574
+ spec clean, red leaks). Fixed at both gates with one shared `scrubRed` over every string field
575
+ — a detail-only fix was sabotage-proven INSUFFICIENT, because the structured `cmd` field ships
576
+ verbatim — plus symmetry wraps on the provider-derived channels.
577
+ - **4th, `src/authorscout.js`:** `record()` scrubs a raw at CREATION, when the verdict is not yet
578
+ known, and the verdict was ASSIGNED onto the stored object afterwards. `reason` is the worst
579
+ field to do that to — `classifySurvey`'s `CALL_FAILED` route quotes the transport's own error
580
+ prose verbatim, a string nobody here wrote and nobody bounds — and the raws land in
581
+ `authored.json` and on the spine. **The scrub had become a property of WHEN you knew the
582
+ verdict.** Fixed by making the stamp a FUNCTION rather than an assignment (`stampRaw` in
583
+ `src/text.js`, which `scrubRaw` renders its own two fields through): one place decides how a
584
+ diagnosis becomes persistable, exactly as one place already decided it for the text. `cause` is
585
+ enumerated and passes through untouched — masking a closed set would only hide a bug in the set.
586
+ - **5th, `src/authorjob.js`:** `notes` is the one FREE-TEXT field a model writes straight into
587
+ the SIGNED `closeDecl`, and it was the only string on that boundary not riding `redactSecrets`
588
+ while every sibling does. No live leak today (inputs are pre-scrubbed upstream and
589
+ `sweepSecretLiterals` reds a secret-shaped note on the way to signing), so this is the
590
+ defense-in-depth half of the class. Hashes are untouched: `redactSecrets` returns a
591
+ non-matching string byte-identical, and no fixture or signed spec carries a secret-shaped note.
592
+ - Every regression watched failing first with the RAW TOKEN in the actual, then watched passing,
593
+ then each fix sabotaged in turn and its test watched red again; assertions demand
594
+ `scanSecrets(JSON.stringify(…)) === []` **plus the mask and the surviving prose around it** —
595
+ masked, never deleted.
596
+ - **The whole-branch review's fifteen, in three shrinking rounds** (F91). Round 1: the genre-env
597
+ envelope survives re-validation; the command deny-floor lands in `checkKind`; `cleanup()` never
598
+ throws; the crashed counter (nonzero exit + 0 matched lines) routes to an instrument stop with
599
+ seed baselines included; `effectiveTimeoutMs` becomes tighten-only; plus a `renderSeedReadBlock`
600
+ scrub (a two-headed leak) and a tautological Layer-R trim test rewritten to be ABLE to fail.
601
+ Round 2, a fix-diff review OF round 1: `renderRejectBlock`'s scrub (a second leak channel of the
602
+ same class, found by reviewing the first fix's diff), genre-env checked at the GROUNDED gates,
603
+ `envCapableKind` consolidated so injector and validator cannot drift, and the crash-stop's
604
+ `grep -c` / pytest exit-5 blind spot documented as an accepted fail-safe limit. Round 3, a
605
+ validation sweep: guard-name-keyed `scopeOfJob` + `AT_MOST_ONCE_KINDS`, `resolveSourcePrefixes`
606
+ realpath-dedupe (measured live on aurora), the notes divergence closed, `parseValue`'s first
607
+ aggregate keeping its notes, scout recovery preferring `s2.error`, and `addedLines`' stat-throw
608
+ announcing a note. 19 new tests sabotage-proven; mutation micro-batch 5/5 killed.
609
+ - **`/code-review medium`'s five.** Validation reds are scrubbed WHERE THEY ENTER THE RECORDS — the
610
+ model-facing copy was masked and the persisted copy was not. The diff scanner's header skip is
611
+ scoped to the PREAMBLE, so an added `++` line can no longer hide a suppression from the guard.
612
+ Worktree cleanup READS git's result instead of assuming it. Plus the two caps above. Each fix
613
+ carries a regression test proven failing against the pre-fix code; the suite was independently
614
+ re-run rather than taken from the reviewer's own gate.
615
+ - **`/diff-review`'s ten land as nine fixed and one DROPPED with its reason.** Beyond the four in
616
+ *Changed* above: the dead `d.code !== 0` clause (git's `ok` IS `code === 0`; a spawn fault carries
617
+ `code: null`); one test running the same declaration through BOTH first-red-wins loops, since the
618
+ deliberate duplication had nothing pinning it; the collision walk's check-then-act NAMED rather
619
+ than closed, because `git checkout -b` already refuses an existing branch (measured, exit 128) so
620
+ the race ends in the stop below it; `scoutReaskTurn`'s rendered text; and `buildSeedListing`
621
+ rendering the `full` tier TWICE on every run that never needed to degrade. One finding **REFUTED
622
+ as a bug and kept as a fail-safe**: the retry ladder's re-survey arm is right — a toolless re-ask
623
+ rests entirely on *"the model already read the tree"*, and an empty transcript is that premise
624
+ gone — the HEADER was the thing that lied, so the header tells the truth and the branch gets the
625
+ test its always-non-empty helper had prevented. **DROPPED (S3):** every `process.exit(2)` in
626
+ `run-author` fires in the argument-parsing preamble, before the script's first stdout write, so
627
+ there is no queued output for F71 to discard — and `die` is used in expression position
628
+ (`return die(…)`), where `exitCode`+return would let a fatal unreadable-JSON CONTINUE into the
629
+ run; the post-output paths already use `process.exitCode`.
630
+
631
+ Suite **1422/1422** on a single clean run; `typecheck` and `build:types` exit 0.
632
+
8
633
  ## [0.8.0] — 2026-08-07
9
634
 
10
635
  ### Changed