bareloop 0.7.0 → 0.8.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,289 @@ 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.8.0] — 2026-08-07
9
+
10
+ ### Changed
11
+ - **The variance meter REPORTS progress — and still decides nothing on it** (F85, A+B;
12
+ `src/planrun.js`). The meter stopped a step that was CONVERGING and then told the redrafting
13
+ planner that step's exits were *"unmoved"*. Two instruments over one run, and only one of them
14
+ right. Live on `u-msh70zla`: the close gaps went 30 → 24 → 15 → 14 with the ladder recording
15
+ `distinctGaps 3` / `strikes 0`, while the replan brief said nothing had moved and the materials
16
+ said *"0 step(s) completed"* — so the replanner, told it had achieved nothing, rebuilt a file
17
+ that was already at zero errors, wrote nothing twice, and the run died with $1.09 and 7 minutes
18
+ unspent. hamr's ruling, verbatim: *"meter is right but missing a piece … it should give heads up
19
+ on money/time + progress for llm to judge."*
20
+ - **The firing condition is byte-identical** — `moneyShare`/`timeShare >= 0.5`, both axes, the
21
+ same threshold, the same throw. A progress term in the TRIGGER would make a governance
22
+ instrument over an operator-owned allowance judge capability, so a converging step that is
23
+ eating the run is still stopped. What was missing was never a term in the condition; it was
24
+ the READING.
25
+ - **The reading is read off `src/trend.js` — the SAME instance the money halt reads.** Nothing
26
+ new is computed at the meter: no second reader, because two instruments answering one question
27
+ is the exact defect being fixed. Per-stage, stage names and numbers only, and `unknown` stays
28
+ `unknown` — a close that reports no number donates nothing (F6).
29
+ - **The hardcoded `with its exits unmoved` string is DELETED.** It printed on every step-variance
30
+ stop whatever had happened, and on `u-msh70zla` it was simply false. The escalation detail —
31
+ which a human reads and which the replan brief quotes — now states the meter's fact (*a share of
32
+ the run was consumed*) and then the trend instrument's measured reading, as two separate claims
33
+ rather than one fused verdict on the work. The materials `progress` line keeps its structural
34
+ sentence (*"step N of M did not finish; K step(s) completed before it"* — a plan's shape is what
35
+ the planner re-allocates) and gains the close trend beside it.
36
+ - **Spine change:** the `variance` record gains `trend` / `motion` / `reading` / `series` — the
37
+ same four names and the same shapes the money halt already emits, so one reader parses a
38
+ progress reading wherever it appears. ADDED fields, never repurposed ones (the spine is
39
+ append-only forever). Measured post-fix on `u-mshcpdg4`, iteration 3 on `finish-strict-typecheck`:
40
+ `moneyShare 0.497`, `timeShare 0.769`, `axis time`, `trend "converging"`,
41
+ `reading "still progressing — typecheck 30 → 15 → 15 → 1"`,
42
+ `series [{stage:"typecheck",values:[30,15,15,1]}]` — against `u-msh70zla`'s pre-fix record, which
43
+ carried the shares and nothing else.
44
+ - **`runTrend` was blind to everything a STEP achieves** (F85, the prerequisite found en route). The
45
+ run's trend reader was fed only the close PRECHECK and the outer fix loop, so a meter firing
46
+ mid-step had nothing to report about a step that had just gone 24 → 15 → 14. Two folds corrected:
47
+ a step's failing check is an arbiter-rendered close grade (same `runStages`, same chain, same stage
48
+ names) and is now recorded like one; and it is recorded on the RAW gap, because `evalExits` wraps
49
+ stage output as `check "<name>" red: …` and that wrapper line carries the word "red" with no number
50
+ on it — so every step grade used to donate a null. Measured on `u-msh70zla`'s own archived gaps:
51
+ raw reads `typecheck 24 → 15 → 14`, wrapped reads nothing at all. Plus a **preflight seed** so the
52
+ counter has a baseline: the precheck reds at the close's FIRST failing stage (first-red-wins) and
53
+ every shipped close opens with `changed-from-seed`, so the numeric stages had no baseline at all and
54
+ a run would read `unknown` on exactly the stop the readout exists for. Seeded ONCE PER STAGE, off the
55
+ reader's own books — the precheck and the preflight menu grade the same unchanged tree back to back,
56
+ and a repeated baseline in a series reads as an attempt that achieved nothing, which is the phantom
57
+ flat step this change exists to remove.
58
+ - **`tests/resume-u.test.js` is decoupled from the live job spec.** It read
59
+ `jobs/bareagent-u-types.json` while hardcoding $8 / 45min, so an operator budget edit on a live spec
60
+ turned the suite red. Every number now derives from the spec it reads, proven by passing at a third
61
+ budget value. Suite: **1023 tests** (1027 minus the 4 deleted with the `never wrote` feature below);
62
+ typecheck and `build:types` exit 0.
63
+ - **The per-step model tier menu narrows to `['sonnet']`** (`STEP_MODELS`, `src/plan.js` — a PUBLIC
64
+ surface: a plan declaring `model: "haiku"` is now REDDED by `validatePlan` with `invalid-value` at
65
+ `steps.N.model`, carrying the menu). hamr's order, verbatim: *"haiku should be dropped to see if it
66
+ passes, if it's agent poor work or harness, like always."* **This is a reversible ATTRIBUTION PROBE,
67
+ not a finding that haiku is incapable** — what the $0 archive read behind it shows is a CONFOUND, in
68
+ both directions. Over 186 spines / 71 accepted plans / 255 steps, only **20 steps ever declared a tier
69
+ at all**: haiku **12** — 9 of them on a REPLANNED plan, 6 the LAST step of their plan, round bounds
70
+ 3–12, **2 ever green** — against explicitly-declared sonnet **8**, 2 on a replan, 3 last, bounds 14–32,
71
+ **5 green**. The planner reaches for haiku on steps it judges mechanical, and a replanned step sits on
72
+ a run already in trouble, so neither column is a controlled read; and all three bareagent-u runs of
73
+ 2026-08-06 tiered their replanned step down (haiku/6, haiku/8, haiku/12) and all three failed. That
74
+ confound sat directly on top of the programme's most recent capability read (F86's tail). Removing the
75
+ tier is how it gets attributed: with one tier expressible, a still-failing replan is agent or harness,
76
+ never the tier.
77
+ - **The `model` field and its validator branch STAY.** A one-entry menu means a plan — or a stored
78
+ bridge — declaring `model: "sonnet"` still validates, and restoring haiku is a one-token edit. No
79
+ stored bridge declares haiku (verified).
80
+ - **The drafter prompt's `model` line is DELETED** rather than reduced to a one-item menu: it
81
+ advertised *"a cheaper tier for mechanical steps"* and there is no cheaper tier now, and reciting a
82
+ single legal value spends tokens to invite a no-op field (`attempts` is the existing
83
+ legal-but-not-offered precedent). Verified by RENDERING the real prompt — 0 lines match
84
+ `/model|tier|haiku|sonnet|cheaper/i` in either the fresh or the replan form.
85
+ - **Only what the AGENT may express narrows**, which is the safe direction. `--model haiku` remains
86
+ the OPERATOR's probe knob (PRD v1.36), untouched in `scripts/run-u.mjs`; two comments there called
87
+ the runner's tier map *"the same closed menu the planner uses"* and now say plainly that it is
88
+ deliberately wider.
89
+ - **No spec hash moved.** `jobSpecHash` covers the resolved spec, not `STEP_MODELS`; all 10 job specs
90
+ were re-hashed and none moved — bareagent-u and bareguard-u are still the hashes hamr signed.
91
+ - Measured on the first sonnet-only run against the last haiku-era one, same patient: **10.9 → 17.5
92
+ seconds per round** (1.6× slower), 120 → 87 rounds, and the failure CLASS changed — struck out at 2
93
+ remaining errors with money and clock left, versus still converging at 1 error when the wall
94
+ expired. Better rounds are slower rounds, and the binding constraint moved from the strike ladder to
95
+ the clock. That sonnet-only run was also the first to reach the outer close, where it was redded for
96
+ **silencing the type checker instead of typing the code** (F87's suppression genre) — the close
97
+ refused it, and the eventual green removed the suppressions.
98
+ - **`bareloop.context.md` named `sonnet | haiku` in two places and is corrected** — the shipped adopter
99
+ contract carried the old menu in the `steps[].model` schema row and in the `providerFor` (P) section,
100
+ so an integrating agent reading it would have drafted a plan that reds. Both now name the one-entry
101
+ menu and say why it is one entry.
102
+
103
+ ### Fixed
104
+ - **A locked-VERDICT `request-red` no longer files as a bare-agent upstream ask** (`src/job.js`,
105
+ `src/ledger.js`). `classifyIncidents` hardcoded `lib: 'bare-agent'` for every `request-red`, but the
106
+ code covers two territories: a locked TOOL verb (bare-agent, the original case) and a locked VERDICT
107
+ type (`soft-green`/`hitl`) — demand against bareloop's OWN `VERDICT_TYPES` menu, live-misfiled for
108
+ every `LOCKED_VERDICTS` declaration (the BA-2 misattribution class). Per the typed-lib rule the
109
+ territory is now stamped at the EMIT site (`Red` gains `lib?`; `verdictType` → `bareloop`, `tools` →
110
+ `bare-agent`) and the ledger reads it, keeping the hardcode only as the pre-field legacy-spine
111
+ fallback (zero archived rows carry the code, verified). The `suggestedAsk` template renders the
112
+ stamped target, so a bareloop-catalogue refusal can never seed a bare-agent ask; D13's genre refusals
113
+ (close-authoring design record) route by stamping their own lib with no ledger change. 5 new
114
+ failing-first tests; `capability-gap` still hardcodes bare-agent — flagged, structurally unreachable
115
+ today (a validation red returns before any provider call; the module header says so).
116
+ - **…and `capability-gap` no longer hardcodes it either — the flag above is CLOSED** (`src/ledger.js`).
117
+ `capability-gap` is `request-red`'s cap-halted form and inherits both of its territories, but the fix
118
+ landed at one of the two sites and not the other: the synthesis loop still wrote `'bare-agent'`
119
+ literally, and so did its `suggestedAsk` template — the one field a human actually files from, where
120
+ fixing only the occurrence's `lib` would have left the misattribution fully intact. The territory now
121
+ comes from the OCCURRENCE (the `request-red` rows already carry the lib stamped at their emit site),
122
+ and the **dedup key becomes `(verb, lib)` rather than the verb alone**: the ledger's own occurrence
123
+ key carries `lib`, so a `Set` over verbs would merge two territories into one row and attribute it to
124
+ whichever the set happened to hold first — an arbitrary target, which is the defect itself. One gap
125
+ per (territory, verb) costs nothing and each carries an honest one. **Reachability is unchanged and
126
+ still DORMANT** — `validateJob` returns not-ok on any red and `run.js` returns on a validation red, so
127
+ a `request-red` and a `cap-halt` cannot share one spine today; this is a latent-consistency fix at a
128
+ seam the module header documents as dormant, and it claims no live bug. 3 new failing-first tests,
129
+ each labelled in-file as a FUNCTION-CONTRACT fixture (`classifyIncidents` is pure) so a later reader
130
+ cannot mistake the synthetic stream for something a run can produce. Closed now rather than left
131
+ flagged because a fix that lands at one of two identical sites is the class this repo has already
132
+ paid for once — the ripgrep install that went into `ci.yml` and not `publish.yml`, which then failed
133
+ on the identical cause.
134
+ - **The replan ceiling reset on EVERY resume leg — a kill refilled a bound the operator signed**
135
+ (`src/planrun.js`, `src/run.js`, `src/reuse.js`, `scripts/run-u.mjs`). `replanned`/`varianceGrantUsed`
136
+ were locals in `runPlan`, and a resume is another `runPlan` call. Confirmed by execution before any
137
+ remedy: leg 1 replanned and stopped; leg 2, driven with a `resumeSeed`, replanned again — a run total
138
+ of 2 with every record showing 1. Two became four by being killed once, which is precisely the creep
139
+ the latch exists to make impossible (*"unlimited replanning launders thrash as adaptation"*, PRD
140
+ v1.12).
141
+ - **Fixed on the MONEY-FOLD precedent, not the grade-seed one, and the choice is the fix.**
142
+ `readGradeSeed`'s documented known limit is that it reads ONE spine, so a resume of a resume
143
+ inherits leg 2's grades and not leg 1's — the chain shortens by a leg per kill. That is fail-safe
144
+ for a READOUT (a short chain can only under-claim a direction) and it is the DANGEROUS direction
145
+ for a CEILING, where an under-claimed ledger is a refilled allowance. So each leg DECLARES the
146
+ ledger it inherited on its own spine (`priorSpentUsd`'s mechanism) and the next reader adds only
147
+ its own window's `replan` records. Leg 3 inherits the whole chain, not leg 2's slice.
148
+ - **The counter-argument was answered, not stepped over.** `src/trend.js` deliberately refuses to
149
+ seed the fix loop's ITERATIONS from history, and that stays right: an attempt allowance is a LEG
150
+ bound, and a restarted leg buys its own attempts with its own money. A replan ceiling bounds how
151
+ many times the WORKFLOW may be redrawn, which is the run's span — the same span the halt readout
152
+ already covers. The two seeds disagree on purpose and both now say so in-source.
153
+ - **SPINE FIELDS — `job-start` gains `priorReplans` / `priorReplanGrantUsed`.** Emitted only when
154
+ there is a fold (a decorative `0` is indistinguishable from a run nobody folded), on the same
155
+ declaration `priorSpentUsd`/`priorWallMs` already ride. The grant latch travels BESIDE the count
156
+ rather than being derived from it: a leg can inherit a spent ordinary ceiling with the arbiter's
157
+ extra still unearned (`1`, `false`), and deriving one from the other would collapse two real states
158
+ into one. `readResume`'s `direct` mode reads the fold off these very fields, so a U-path run
159
+ resumed twice inherits leg 1.
160
+ - **SPINE MEANING CHANGE — `plan-executed.replans` counts the CHAIN, not the leg** (and `replanned`
161
+ with it). **An adopter parsing spines must re-read this field.** The change was made rather than
162
+ shadowed by a second field precisely because `replans` shipped only on this unreleased branch, so
163
+ no archived spine carries the leg reading for a reader to be confused by. The leg's own number is
164
+ not lost: a leg emits one `replan` record per replan it drafts, so `chain − records-in-this-window`
165
+ is the fold it inherited — the same arithmetic `readResume` runs — and a second field stating a
166
+ derivable number would be a second reader of one question.
167
+ - **New option: `runJob(spec, { resumeReplans })`** (and `runPlan`'s identically-named option),
168
+ `{count, grantUsed} | null`, defaulting to `null` = the cold path, byte-identical to a run nobody
169
+ resumed. Belted like every declared fold entering the arbiter's arithmetic: a garbage, negative or
170
+ non-finite seed reads as `0` rather than poisoning later comparisons as `NaN`, and a negative one
171
+ can never cancel a real replan and widen the ceiling. `scripts/run-u.mjs --resume` and `runReuse`
172
+ both pass it. **The disarm control holds** — a resumed leg with an UNSPENT ceiling still replans, so
173
+ the bound is bounded and not switched off.
174
+ - **The replan brief's `closeGapBlock` re-bounded an ALREADY-bounded gap and deleted the red names the
175
+ first envelope had rescued** (`src/planrun.js`, `src/exits.js`). `src/exits.js` states the rule for
176
+ this same artifact one seam earlier: the check gap arrives already enveloped by `runClose`'s
177
+ `boundGap` under the stage's own `gapKeep`, which deliberately rescues the `not ok`/`FAILED` names
178
+ out of the elided middle — the mechanical gap F46's conversion mechanism feeds the worker. A second
179
+ 400/1500 envelope deleted exactly those a second time, which is F28 reintroduced; measured on a
180
+ 120-red-line close output, the first bound rescued **78** names and the second dropped **12** of
181
+ them. `CHECK_GAP_MAX` (12,000) is now **exported** from `src/exits.js` and imported here rather than
182
+ respelled, so the two seams cannot drift on how big a check gap may be, and it is applied as a
183
+ **backstop, not an envelope**: under it the gap passes through VERBATIM. The headroom is measured,
184
+ which is why the ceiling is not smaller — `runClose`'s own envelope tops out at ~10.1KB (400 head +
185
+ 8192 keep + 1500 tail + labels), and joined with a `tree-changed` detail that is ~10.2KB, leaving
186
+ ~1.8KB to spare, so the shipped path is untouched and the backstop fires only where `src/exits.js`
187
+ says it should: a seam that returned something never bounded at all. Length is measured AFTER the
188
+ secret scrub, because a mask is wider than most literals it replaces and bounding the raw string
189
+ would let a gap cross the ceiling on its way through the one transformation that always runs.
190
+ - **The replan brief's block was LABELLED as the close's own output but could lead with ours**
191
+ (`src/planrun.js`). `res.gap` is `lastGap` — the join of EVERY failing exit's detail in exit order —
192
+ and with `MAX_EXITS_PER_STEP` at 2 plus the mandatory `tree-changed` pairing on a write step, a step
193
+ that wrote nothing opens the block with the EVALUATOR's own prose (`0 files changed under src/** …`)
194
+ under a label reading *"the verification's own output"*. Captured verbatim from a real replan prompt.
195
+ The **label** is corrected to *"What this step's exits reported on its last attempt (their own
196
+ output, verbatim)"*; the **payload is deliberately kept** — "the step wrote nothing" is exactly what
197
+ a replanner should be told, and stripping it to make an old label true would trade a real fact for a
198
+ tidy sentence. F86's thesis (the artifact travels as TEXT, never parsed) is restated, not weakened.
199
+ - **F49's ReDoS reject now covers the OPERATOR's two regex fields, not just the agent's**
200
+ (`src/job.js`, `src/validate.js`, `src/plan.js`). `hasNestedQuantifier` shipped wired at exactly ONE
201
+ site — the agent-authored `artifact-written.pattern`. Two operator-authored patterns reach an
202
+ equally untimed evaluator and were uncovered: `judged.pattern`, exec'd by `runClose` against the
203
+ close's whole stdout+stderr, and `gapKeep`, compiled by `boundGap` and run **once per line** of
204
+ close output. Both now red `invalid-value` at the validation gate, before any tokens burn.
205
+ - **Measured, not argued:** `boundGap(stream, '(a+)+$')` against a 40-character line does not finish
206
+ in 20 seconds (`timeout` exit 124).
207
+ - **Why this is worse than an operator self-DoS, and F67 is the reason.** On the agent side F49's
208
+ own scope note holds — the agent authors both the pattern and the artifact, so a hang burns only
209
+ its own wall-clock and compromises no arbiter. These two run in **bareloop's own process**, so the
210
+ hang blocks the MAIN EVENT LOOP — and the in-process stall fuse is a timer in that same loop, so
211
+ it cannot fire (*a guard living inside the process it guards shares that process's fate*). The run
212
+ then dies to the OUTSIDE watchdog and reads as a model stall: **a bad regex in a SIGNED spec
213
+ presents as a provider problem.** The cost is a misdiagnosis, not just a stop.
214
+ - **The detector MOVED to `src/validate.js` — one inventory, never a second copy.** `plan.js`
215
+ imports `job.js`, so `job.js` cannot import `plan.js`; housing the scan with the other shared
216
+ primitives is what lets both documents reject through the same function. `plan.js` re-exports the
217
+ name (the `WRITE_VERBS` precedent), so the F49 site is byte-identical in behaviour. A second copy
218
+ would be precisely the drift class `SECRET_PATTERNS` exists to prevent — a shape one validator
219
+ rejects and the other admits. `hasNestedQuantifier` is not exported from `src/index.js`, so the
220
+ move is internal and breaks no adopter.
221
+ - **Admissibility swept before the change and re-run after:** all **92** operator regexes across all
222
+ **10** signed specs in `jobs/` — **zero** newly red. No spec breaks, no spec-hash churn (no spec
223
+ file is touched). The shipped shapes are pinned in the suite as the control that can produce the
224
+ negative: if the reject ever reds one of them, it has broken the live fleet.
225
+ - **Known limit, PARKED not chased:** the scan misses the alternation-overlap class (`(a|aa)+$`,
226
+ `(x|xy)*$` — no inner quantifier exists to find; the blowup comes from overlapping branches under
227
+ a repeat). Widening it changes which SIGNED specs are admissible, which is arbiter-adjacent.
228
+ Docked to the close-authoring rung, where hand-authored operator regexes may cease to be a surface
229
+ at all — see **PRD v1.55**. Recorded there too, so it is not re-chased: `([^])*$` is NOT caught and
230
+ that is CORRECT (`[^]` is the any-char idiom, so the pattern is linear); the genuinely nested
231
+ `([^]*)*$` IS caught.
232
+
233
+ ### Added
234
+ - **A SECOND variance stop can earn ONE more replan — granted by the ARBITER, on a mechanical
235
+ reading** (F85, C). A second `step-variance` after the one-replan ceiling was spent used to be a
236
+ hard stop, and on a run that was still converging that stop threw away work already paid for. Every
237
+ clause of the grant is a constraint: **ONE**, bounded by a LATCH rather than a comparison so the
238
+ ceiling cannot creep by arithmetic (unlimited replanning launders thrash as adaptation, v1.12) — a
239
+ third variance stop is the stop, however well it is going; **the arbiter's**, read mechanically off
240
+ the same `runTrend` instance the meter and the money halt read, with the agent given no channel to
241
+ it — it never asks, is never offered it, and cannot influence it (no self-adjusted budgets, ever);
242
+ **`converging` only**, the trend instrument's own category with no fresh number invented here
243
+ (threshold-setting is the operator's), so `flat` and `unknown` stop exactly as before and an unknown
244
+ is never rounded up into a reason to spend (F6); and **`step-variance` only** — an exhaustion or a
245
+ stall after the ceiling is unchanged. `remainingUsd() > MONEY_MIN` still gates it, as it gates the
246
+ ordinary replan. **Spine change:** `replan` gains `replan` (which replan this is) and — only when the
247
+ arbiter granted one past the ordinary ceiling — `granted` with the reading it was granted on, so a
248
+ reader can never mistake the ceiling for a grant; `plan-executed` gains a `replans` count riding
249
+ BESIDE the shipped `replanned` boolean, never repurposing it. Live on `u-mshcpdg4`: three
250
+ `plan-accepted` records — the initial draft and two replans, the second granted on `converging`.
251
+ - **`closeGapBlock(gap)` — the replan brief carries the close's OWN output** (F86; `src/planrun.js`).
252
+ `u-mshcpdg4` reached ONE remaining strict error and still died. The close gap named the work exactly
253
+ — `src/recurse.js(978,115)` — and the worker read it every attempt; the REPLANNER, which is the
254
+ component that chooses which files the next plan targets, saw only
255
+ *"still progressing — typecheck 30 → 15 → 15 → 1"*: a number with no address. It aimed the last plan
256
+ at `src/loop.js`, already at zero errors, wrote nothing twice, and took two strikes — with $1.10 and
257
+ 82 seconds still on the clock at the moment that replan was granted (the run itself ended $0.8174 and
258
+ ~6 seconds short of its caps). F85 gave the brief the trajectory; this gives it the artifact the trajectory
259
+ is a summary OF. Handed over as **TEXT**, with both boundaries reused rather than respelled: scrubbed
260
+ through the one secret inventory (`redactSecrets`/`SECRET_PATTERNS`) because a prompt is an egress
261
+ point and the next caller will not remember the upstream scrub, and bounded by ralph's own `boundGap`
262
+ — now **exported** from `src/ralph.js` for it, since a private slice would be a second truncation
263
+ scheme that drifts on exactly the property that matters (whether the red lines survive, and whether
264
+ the trim announces itself, F28); the close path still calls it with the stage's own `gapKeep`,
265
+ byte-identically. The red-set is `REPLAN_GAP_KEEP = '\S'` — every non-blank line — because a shipped
266
+ `gapKeep` is `^`-anchored (`^BAREAGENT `, `^red`, `^FAILED`) and the exit evaluator wraps stage output
267
+ in `check "x" red: …`, so the anchor no longer sits where the pattern expects it; a gap that reaches
268
+ here is ALREADY a red-set (`judge` builds it out of the failing exits only), so keeping every line is
269
+ the correct instrument, not a widened one. **No spine field** — `src/trend.js`'s rule is that no
270
+ record carries a close byte. An empty gap yields the empty string, so the brief renders
271
+ byte-identically to the pre-F86 one (a labelled empty section would invite the planner to explain an
272
+ absence the run never observed). Verified by execution, not by reading: sabotaging the wiring
273
+ (`gapBlock = ''`) turns the `u-mshcpdg4` regression test red (`not ok 121`); restored byte-clean.
274
+
275
+ ### Removed
276
+ - **`gapFilesNeverWritten()` and the `never wrote` replan advisory** (F86) — an EXPORT of
277
+ `src/planrun.js` is gone (it was never re-exported through the package entry, whose `exports` map
278
+ admits only `.`, so no adopter import breaks). hamr's order, verbatim: *"delete it and rerun
279
+ bareagent"*. The measured reason, on `u-mshcpdg4`'s own gap: the advisory resolved to
280
+ `src/loop.js` — the file already at zero errors — and said so as a DIRECTIVE, beside the artifact
281
+ that said the opposite; this worker follows directive prose (positive-scope confinement). It also had
282
+ no evidence of ever converting anything: it fired by construction on `u-msdsmkid`, and that run still
283
+ step-redded. The general rule it broke is why `closeGapBlock` hands over text instead: **a parsed
284
+ file list cannot tell a summary line from a detail line**. Line 1 of that very gap reads
285
+ `reports 1 error(s) in src/recurse.js, src/loop.js` — every file in SCOPE — and only line 2 names the
286
+ culprit; and the shapes differ per close anyway (tsc `file:line`, pytest test ids, a count close that
287
+ names no file at all). A model reading the artifact can make that distinction; a regex reproduces the
288
+ bug. Two readers of one question with the parsed one wrong is the same failure the variance meter just
289
+ had. Its 4 tests went with it, and its sole caller was the brief — no speculative code left behind.
290
+
8
291
  ## [0.7.0] — 2026-08-05
9
292
 
10
293
  ### Changed
package/README.md CHANGED
@@ -49,8 +49,10 @@ it and never disappears.
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
51
51
  > public API — but shipping is not acceptance: one execution green proves reuse safe and
52
- > legal, never *beneficial*. The lift claim waits on the ON-vs-forced-cold contrast, which
53
- > has not fired. Until it does, treat reuse as available and unproven.
52
+ > legal, never *beneficial*. The contrast that was to prove the lift is now retired (F88): a
53
+ > validation rule hands every cold run the plan shape a bridge was carrying, so the gap it
54
+ > would have measured is gone. Treat reuse as available and unproven — what a bridge should
55
+ > carry instead is open.
54
56
 
55
57
  ## Quick start
56
58
 
@@ -96,7 +96,7 @@ the step plan at run time (gated by `validatePlan`); the human signs only:
96
96
  | field | shape | notes |
97
97
  |---|---|---|
98
98
  | `goal` | non-empty text | what the agent plans against |
99
- | `verdictType` | `green` \| `soft-green` \| `hitl` | declared radio, never inferred (`VERDICT_TYPES`, frozen). v1 ADMITS only `green`; declaring `soft-green`/`hitl` reds `request-red` with the type as a structured `verb` field (declared-but-locked — the tool-menu pattern) |
99
+ | `verdictType` | `green` \| `soft-green` \| `hitl` | declared radio, never inferred (`VERDICT_TYPES`, frozen). v1 ADMITS only `green`; declaring `soft-green`/`hitl` reds `request-red` with the type as a structured `verb` field (declared-but-locked — the tool-menu pattern). Every `request-red` also carries `lib` — the territory the demand lands against, stamped at the emit site (`verdictType` → `bareloop`, a locked tool verb → `bare-agent`): the ledger keys and its `suggestedAsk` seed on it, so a bareloop-catalogue refusal never files as an upstream ask |
100
100
  | `close` | **an ORDERED LIST of named stages** `[{ name, cmd, expect, judged?, gapKeep?, offer?, needs? }, ...]` (PRD v1.28), or a close object (table below) **only** for the declared-but-locked verdict classes | the destination, the only thing hand-authored; the check menu DERIVES from it (below). The plan flow executes a staged close directly and adapts a bare `predicate` object into a one-stage list; a `gold`/`rubric`/`hitl` object close validates (the declared-but-locked verdict classes still parse) but the plan flow refuses it at runtime as `close-unsupported` — it names no command to run |
101
101
  | `checks` | **RETIRED** (PRD v1.28/v1.32) | hand-authored checks are gone, not merely discouraged: declaring `checks` reds `checks-derived` by name. The check menu is DERIVED from the close's own stages instead — see **Staged close** below. The hazard this removes is measured, not theoretical: job #5's three hand-written checks were re-implementations of three stages the close already ran, and a hand-carved copy can drift LENIENT (the worker passes the operator's ruler and fails the real inspection) |
102
102
  | `tools` | optional unique subset of `TOOL_MENU` (14 verbs, below) | the CEILING every plan step's grant must fit inside (omitting it means the full menu — and the hash is taken over that RESOLVED form, so a `TOOL_MENU` widening flips an omitted-`tools` spec's hash and forces a re-sign; see `jobSpecHash`, MED-1); `run` is `LOCKED_TOOLS` and reds `request-red` — locked-but-listed, and the red IS the admission evidence the ledger tallies (a typo stays `invalid-value`). A ceiling of write-class and store-class verbs ONLY reds `invalid-value`: the scout surveys read-only, so it would be handed an empty menu and survey blind |
@@ -174,6 +174,20 @@ reader for every close, deliberately (the F49 precedent). **Splitting a stage mo
174
174
  hash** — stage names are in the signed spec — so it is a spec edit the operator re-signs, and
175
175
  it changes what stored bridges match (below).
176
176
 
177
+ **The law for spec authors: your `goal` must state everything your close will judge.**
178
+ Nothing derives the goal from the close or the close from the goal, and nothing checks the
179
+ two against each other — the ONE derivation in the library is close stages → the agent's
180
+ `check-passes` menu, one hop, one direction. That separation is the arbiter rule (the agent
181
+ references the operator's ruler and never authors it) and it is not negotiable, so the drift
182
+ is yours to close: the planner and the worker read the `goal` text, not your stage list, and
183
+ they will spend their single check slot (above) on the constraint the goal sentence names.
184
+ A stage your goal never mentions is a **cost trap, not a safety hole** — the close still
185
+ refuses the tree and the run still cannot mint a green — but the run discovers that stage at
186
+ the tail, after paying for work aimed elsewhere, and may not have the money or wall left to
187
+ redo it. Write the goal from the close's own stage list, in the close's own words; keep it to
188
+ a few explicit requirements rather than prose. Preflight already names every stage on the
189
+ `check-menu` spine event — read it back and check your goal against it before you sign.
190
+
177
191
  **Close types and the hierarchy — the OBJECT form, which survives only for the
178
192
  declared-but-locked verdict classes** (a close is data, never code; verdict-class laundering
179
193
  is a named red `close-hierarchy`). The go-forward shape is the staged list above; a
@@ -233,6 +247,19 @@ first) so a pathological close cannot rebuild the very bloat the bound exists to
233
247
  when the cap trims matches it **says so** with an explicit marker (silent truncation is the
234
248
  disease this cures, never a cure that truncates silently). Validated like `judged.pattern`:
235
249
  a non-empty string that must **compile as a RegExp**, else a spec red before any tokens.
250
+
251
+ **Both operator patterns must also survive the ReDoS shape check** (`invalid-value`, F49 —
252
+ the same reject the agent's `artifact-written.pattern` gets): an unbounded quantifier over a
253
+ group that itself repeats unboundedly (`(a+)+`, `(\d*)*`, even wrapped: `((a+))+`) is
254
+ rejected at the gate. **This is stricter than an "it compiles" contract, and deliberately
255
+ so.** `judged.pattern` is exec'd by `runClose` over the close's whole stdout+stderr and
256
+ `gapKeep` is run by `boundGap` **once per line** — both untimed, both in bareloop's own
257
+ process. Measured: `boundGap(stream, "(a+)+$")` on a 40-character line does not finish in
258
+ 20 seconds. Because the hang blocks the main event loop, the in-process stall fuse cannot
259
+ fire, the run dies to the outside watchdog, and **a bad regex in a signed spec presents as a
260
+ provider problem** — so this reds at signing time rather than being diagnosed later. All 92
261
+ operator regexes across the 10 in-repo specs pass unchanged. Known limit, parked: the scan
262
+ does not catch the alternation-overlap class (`(a|aa)+$`) — see PRD v1.55.
236
263
  Omitting it is exactly today's bound. Like `judged`, it is **arbiter territory** — the
237
264
  drafted workflow config cannot express it (unknown-field red). The gap path also combines
238
265
  **both** streams (stdout + stderr), so a failure printed to stdout survives stderr noise
@@ -249,11 +276,23 @@ middle writes; `validatePlan` gates it against the SIGNED job spec before tokens
249
276
  | `steps[].tools` | non-empty unique subset of the SPEC ceiling | a verb beyond the ceiling reds `verb-escape` with the verb as structured data (overreach, distinct from the operator-side `request-red`); `run` is `verb-escape` at every layer |
250
277
  | `steps[].rounds` | int 1..shell cap (default 40) | the step's per-attempt tool-round bound (the Gate's `maxTurns` natively) |
251
278
  | `steps[].target` | path inside the fence | v1.18 deliverable; REQUIRED on write-granted steps |
252
- | `steps[].model` | optional; `sonnet` \| `haiku` (`STEP_MODELS`) | the step's model TIER — a menu, never a model string the agent spells (the tier→model mapping and any per-model effort params are the runner's). `opus` is deliberately absent: reserved for work the human assigns, never plan-selectable. Off-menu reds `invalid-value`. A step naming a tier when the caller supplied no `providerFor` factory is an `interpreter-red` STOP, never a silent run on the default tier |
279
+ | `steps[].model` | optional; `sonnet` (`STEP_MODELS`) | the step's model TIER — a menu, never a model string the agent spells (the tier→model mapping and any per-model effort params are the runner's). `opus` is deliberately absent: reserved for work the human assigns, never plan-selectable; `haiku` was removed 2026-08-06 as a reversible ATTRIBUTION PROBE (F87, see `STEP_MODELS`), so the menu is one entry deep and the field is no longer offered in the drafting prompt. The field and its validator branch STAY: `model: "sonnet"` still validates, a stored bridge declaring it still loads, and restoring haiku is a one-token edit — this is not a finding that haiku is incapable. **The narrowing binds only what the AGENT may express.** Which real model a tier maps to, and what the caller runs as its own default worker model, stay the OPERATOR's: `providerFor` is yours, and the in-repo runner's `--model haiku` probe knob is untouched (PRD v1.36) — the runner's tier map is deliberately WIDER than the plan menu. Off-menu reds `invalid-value`. A step naming a tier when the caller supplied no `providerFor` factory is an `interpreter-red` STOP, never a silent run on the default tier |
253
280
  | `steps[].attempts` | optional; int `>= 1` | **RETIRED and INERT (F79) — it bounds nothing.** A step's iterations are governed by the STRIKE LADDER (below), which is shell-owned and inexpressible from the plan. The field is no longer offered in the drafting prompt and the runner ignores it; it is deliberately still ACCEPTED, because stored bridge plans carry it and a plan minted under one runner's number must not red under another's. The shape is still checked (a known field holding garbage stays a `bounds` red); the old `<= capRuns` ceiling is gone with the cap it named. It was previously TIGHTEN-only — which meant a step that needed MORE iterations could not ask, and drafters measurably tightened it instead (F77) |
254
281
  | `steps[].scope` | optional; one value from the SAME menu `tree-changed` uses | narrows this step's live WRITE fence to a subset of the signed `writeScope` — chosen from the offered scope menu (below), never authored. Off-menu reds `invalid-value` carrying the menu |
255
282
  | `steps[].exit` | 1..2 items (`MAX_EXITS_PER_STEP`), ALL must pass (AND-only, no OR/NOT) | closed menu (`EXIT_TYPES`): `artifact-written(path, pattern?)` · `tree-changed(scope)` · `json-valid(path)` · `check-passes(name)`. **`tree-changed.scope` is a MENU, not a glob the agent authors** — the runner enumerates the signed fence entries plus the real directories beneath them (shallowest-first, capped at `MAX_SCOPE_MENU`=24; `legalScopes` in `src/plan.js`), `planPrompt` lists them verbatim, and `validatePlan` accepts membership only — the SAME array on both sides, so what was offered is what is accepted; omitted, it derives from `writeScope` alone — never a free-text fallback. An off-menu value that escapes the fence still reds `scope-escape` (the ledger's attribution class), an in-fence unoffered value reds `invalid-value` carrying the menu. `runPlan` emits `scope-menu {offered, truncated, offerableCount?, cap?}` so a capped menu is never silently complete. `check-passes` must name a stage on the close's DERIVED menu (`check-unknown` red names the offered menu — no operator authors this list, it comes straight from `checkMenu(close)`); on a write-granted step it must be paired with `tree-changed` (`exit-illegal` — the seed tree is green, a lone check would pass untouched, F17/F46). **`check-passes` also requires a write-class verb (`write`/`edit`) on the SAME step** (`exit-illegal` — BREAKING for plan authors: a read-only "verify" step carrying a check used to validate and no longer does). A failing check's gap is re-delivered to that step's OWN worker, so a step holding a check must be able to act on it; a read-only one is a mailbox with no hands and stalls to cap on a byte-identical gap (measured on 4 of 4 drafted plans). The check belongs on the step that fixes; the run's final verification is the operator's close, which the agent never authors. The rule is stated identically in the drafter prompt and the validator, so the two can never disagree — and it is suppressed when `tools` failed to parse (the step's hands are unknowable then, and the real defect already redded: one defect, one red). `artifact-written.pattern` must compile AND survive a ReDoS shape check — an unbounded quantifier over a group that itself repeats unboundedly (`(a+)+`, `(\d*)*`, even wrapped: `((a+))+`) is an `invalid-value` red (F49, catastrophic-backtracking footgun; rewrite without a repeated group inside a repeat). Exits verify FORM, not truth — progress gates; the operator's close stays the one arbiter |
256
283
 
284
+ **A step has exactly ONE check slot — plan for it.** The three exit rules above compose into
285
+ a ceiling worth stating on its own, because it decides what a plan can enforce *during* a
286
+ run: `MAX_EXITS_PER_STEP` is **2**, the two are ANDed (no OR/NOT, `exit-illegal` past the
287
+ bound), and a `check-passes` on a write-granted step MUST be paired with a `tree-changed`
288
+ exit. Both slots are therefore spent the moment a step carries one check, so **a step cannot
289
+ carry two checks** — `check-passes(typecheck)` and `check-passes(no-suppressions)` together
290
+ on one step is inexpressible today, not merely discouraged. The consequence for an adopter:
291
+ if your close judges N stages, at most ONE of them can be enforced inside a step; the other
292
+ N−1 are enforced only at the close, at the end of the run. That is a COST shape, not a safety
293
+ hole — the close still refuses a tree that fails any stage, and nothing green is minted mid-run
294
+ — but a stage first tested at the tail is a stage the run pays full price to discover.
295
+
257
296
  Red vocabulary (both validators): `parse-error`, `unknown-field`, `missing-required`,
258
297
  `invalid-value`, `bounds`, `duplicate-id`, `close-type`, `close-hierarchy`,
259
298
  `secret-literal`, `scope-escape`,
@@ -431,6 +470,17 @@ ONE inventory) at the emission boundary, before anything rides the append-only s
431
470
  module itself does not scrub: a direct `evalExits` caller wiring results into its own log
432
471
  must apply its own redaction.
433
472
 
473
+ A `check-passes` detail is sliced at `CHECK_GAP_MAX` (12,000 chars) — a defensive backstop
474
+ above the close path's own `boundGap` envelope, not a second trim of it: a normally-bounded
475
+ gap passes through intact and only a seam that returned something unbounded is ever cut. The
476
+ constant is now `export`ed from `src/exits.js` so the replan brief's own backstop imports it
477
+ rather than respelling it (two spellings would drift on the one property that matters —
478
+ whether the `not ok`/`FAILED` names survive). **It is NOT part of the adopter surface:** the
479
+ package `exports` map admits only `"."`, and `src/index.js` re-exports `snapshotScope` and
480
+ `evalExits` from this module and nothing else, so `CHECK_GAP_MAX` and `boundGap` are internal
481
+ by construction and a deep import cannot reach them. They are named here to explain the
482
+ behaviour you WILL see in a detail string, not as API to call.
483
+
434
484
  ### `jobSpecHash(job)` / `checkApproval(job, approvals)` — `src/job.js`
435
485
 
436
486
  The pure half of **human-signs-always**: an agent may draft a job spec, but no job runs
@@ -454,10 +504,12 @@ Reserved spine vocabulary (V7, machinery-free until job #1 surfaces one):
454
504
  `coordination-red` — a failure between units (scope contention, step order, store
455
505
  races), never to be folded into worker/interpreter reds.
456
506
 
457
- ### `runJob(spec, { approvals, workdir, provider, nativeProvider?, providerFor?, emit, capRuns?, strikeLimit?, shellCapUsd?, closeTimeoutMs?, layerRoot?, bridge?, priorSpentUsd?, priorSpendComplete?, priorWallMs?, resumeSeed?, resumeGrades? })` → outcome — `src/run.js`
507
+ ### `runJob(spec, { approvals, workdir, provider, nativeProvider?, providerFor?, emit, capRuns?, strikeLimit?, shellCapUsd?, closeTimeoutMs?, layerRoot?, bridge?, priorSpentUsd?, priorSpendComplete?, priorWallMs?, resumeSeed?, resumeGrades?, resumeReplans? })` → outcome — `src/run.js`
458
508
 
459
- The last four are the RESUME fold and are documented under *Resuming a killed run* below;
460
- they default to `0` / `true` / `0` / `null`, so a fresh run passes none of them.
509
+ The last six are the RESUME fold and are documented under *Resuming a killed run* below; they
510
+ default to `0` / `true` / `0` / `null` / `[]` / `null`, so a fresh run passes none of them.
511
+ Three of them are folds of a bound the operator SIGNED (money, wall, replans) and one is a
512
+ readout seed (grades) — the distinction matters and is spelled out there.
461
513
 
462
514
  The runner — the shell's top layer, and the ONE entry. It composes everything below it and
463
515
  interprets nothing itself. Sequence: **approval gate** (human-signs-always — refuses an
@@ -524,8 +576,9 @@ fed back, then `plan-red`) → **EXECUTE** (strictly sequential micro-loops: `ra
524
576
  the exit-evaluator judge, each step under its own STRIKE LADDER — below; tree snapshots at
525
577
  step start; the gap names every failing
526
578
  wall — mechanical genre, F46's measured mechanism; artifacts feed forward labeled by
527
- step id) → **ONE replan**, triggered by exhaustion OR variance (an instrument stop never
528
- replans) → **the operator's close**, a red feeding ONE bounded fix loop judged by the
579
+ step id) → **ONE replan**, triggered by exhaustion OR variance OR a stall (an instrument stop
580
+ never replans) — plus at most ONE arbiter-granted extra on a second variance stop that reads
581
+ `converging`, below → **the operator's close**, a red feeding ONE bounded fix loop judged by the
529
582
  REAL close and governed by the close TREND rule below (`capRuns`, default 3, survives only as
530
583
  its blind fallback). `plan-executed` (the plan-as-executed record, design law #2) lands on the
531
584
  spine on every path that executed steps. Additional outcomes: `already-green |
@@ -557,8 +610,26 @@ escalation and wall-stop records carry `strikes`/`strikeLimit`/`distinctGaps` be
557
610
  iteration count rather than a cap the step no longer has. **The replan brief
558
611
  names the mechanism**: which shape ended the step (converging-cut / stalled-no-write /
559
612
  repeated-gap, with the iteration numbers as evidence), the gap trajectory, and how much money
560
- and wall the stop left unspent (time reported UNBOUNDED when no wall was set, never `0`). It
561
- names no culprit file — F28 applies to the replan channel too.
613
+ and wall the stop left unspent (time reported UNBOUNDED when no wall was set, never `0`).
614
+ **It also carries that step's own last EXIT output, verbatim** (F86) — masked through the repo's
615
+ one secret inventory, and labelled as *the exits'* words rather than the close's, because the
616
+ payload is `lastGap`: the join of EVERY failing exit's detail in exit order. The close stage's
617
+ output is one of those details, never reliably all of them, so a step that wrote nothing opens
618
+ the block with the exit evaluator's own `0 files changed under …` prose. That line is kept (it is
619
+ exactly what a replanner should be told); only the label was wrong. The bound over it is a
620
+ **backstop, not a second envelope** — what arrives has already been bounded by `runClose` under
621
+ the stage's own `gapKeep`, which deliberately rescues the `not ok`/`FAILED` names out of the
622
+ elided middle, and re-enveloping that deletes them a second time (F28 reintroduced; measured, a
623
+ second 400/1500 pass dropped 12 of the 78 names the first had rescued). Under `CHECK_GAP_MAX` —
624
+ `src/exits.js`'s own ceiling, imported rather than respelled so the two seams cannot drift — the
625
+ gap passes through VERBATIM, with ~1.8KB of measured headroom over the largest thing the close
626
+ envelope can produce; the backstop fires only on a gap nothing upstream ever bounded. No gap, no
627
+ block, and the brief is byte-identical to one drafted before this existed.
628
+ Handed over as TEXT: the runner makes **no parsed file claim of its own** in this channel —
629
+ a close's summary line ("N error(s) in a.js, b.js" — every file in scope) and its detail line
630
+ (the one location actually failing) are indistinguishable to a regex, and the shapes differ per
631
+ close anyway. What the planner is told about WHERE the work is comes from the close's own
632
+ bytes, never from the runner's reading of them.
562
633
 
563
634
  **The close TREND rule — how the CLOSE-FIX LOOP ends (PRD v1.46 §4).** `capRuns` retired as
564
635
  that loop's governor once its own $0 replay over every archived fix loop came back clean (0
@@ -608,7 +679,9 @@ on money (the step loop, the close-fix loop, and the scout/draft relay), exactly
608
679
  close rendered no number (F6). **The library only ever REPORTS**: `budgetUsd` is in the spec
609
680
  hash, so a top-up is a spec edit a human signs — nothing in a run may widen its own budget.
610
681
 
611
- **ONE progress instrument, read by both halts.** The wall halt used to run its own: byte
682
+ **ONE progress instrument, read by both halts — and REPORTED by the variance meter.** The
683
+ `variance` record and its escalation carry the same four fields off the same instance (below);
684
+ the meter is the one consumer that never decides on the reading. The wall halt used to run its own: byte
612
685
  equality of the last two close gaps, reading `stalled` / `moving` / `unknown` beside the money
613
686
  halt's `flat` / `converging` / `unknown` on the same run. Both now read `runTrend`, the run's
614
687
  own `src/trend.js` reader. Where a stage reported a comparable NUMBER it decides; where none
@@ -671,14 +744,57 @@ The planner (never the worker) receives a **materials** block at draft and at re
671
744
  spread on verification-gap duration, so a per-round constant describes almost no real round
672
745
  and a rate framing creates a rush-or-fake incentive. The replan trigger gains a second axis
673
746
  (`step-variance`): a step consuming ≥ `VARIANCE_THRESHOLD` (0.5) of the run's REMAINING money
674
- or time with its exits unmoved is pre-empted at the head of its next attempt so the planner
747
+ or time — either axis — is pre-empted at the head of its next attempt so the planner
675
748
  re-allocates instead. **Known and recorded (F63): that trigger fired 0 times across 18
676
749
  archived spines / 54 steps** (near misses 0.35–0.45) — the threshold is arbiter territory,
677
- deliberately not fitted to those points. The ONE-replan ceiling is unchanged.
750
+ deliberately not fitted to those points. It is no longer inert: it fires on real runs (F85).
751
+
752
+ **The meter REPORTS progress; it never decides on it (F85).** The firing condition above is
753
+ the whole trigger — a progress term inside it would let a governance instrument over an
754
+ operator-owned allowance judge capability, and a converging step that is eating the run is
755
+ still stopped. What the meter says about the work is a MEASURED reading, not an assertion: the
756
+ `variance` spine record gains `trend` (`converging | flat | unknown`), `motion`, `reading` and
757
+ `series` beside its unchanged `step`/`iteration`/`threshold`/`moneyShare`/`timeShare`/`axis`,
758
+ and the escalation `detail` (which the replan brief quotes) ends with that same `reading` as a
759
+ second, separate claim — "it was stopped for eating the run" and "here is what it achieved" are
760
+ different statements. All of it is read off the SAME `src/trend.js` instance the money and wall
761
+ halts read — one reader, so the meter and the halts can never disagree, and `unknown` stays
762
+ `unknown` when the close reported no number (F6). This
763
+ replaces a fixed sentence ("with its exits unmoved") that used to print on every variance stop
764
+ whatever had happened; a consumer parsing that string will not find it.
765
+
766
+ **The replan ceiling is no longer flatly ONE.** A SECOND `step-variance` stop earns exactly one
767
+ more replan when the run's own reading is mechanically `converging`; it is granted by the
768
+ ARBITER off that same reader, bounded by a latch rather than a counter compared against a
769
+ limit, so it cannot creep — a third variance stop is the stop however well the run is going.
770
+ `flat` and `unknown` stop exactly as before, and an exhaustion or stall stop past the ceiling
771
+ is unchanged. The agent has no channel to it: it never asks, is never offered it, and cannot
772
+ influence it (no self-adjusted budgets, ever). Observable on the spine as ADDED fields, never
773
+ repurposed ones: `replan` records carry `replan: <n>` and — only on a granted one — `granted:
774
+ "converging"`, and `plan-executed` carries `replans` beside the unchanged `replanned` boolean.
775
+
776
+ **Both of those count the RUN, not the leg — the ceiling spans a resume chain.** `replanned`
777
+ and `replans` were locals in `runPlan`, and a resume is another `runPlan` call, so a kill used
778
+ to hand the next leg a fresh allowance of an allowance that is the RUN's by doctrine (measured:
779
+ leg 1 replanned and stopped, leg 2 replanned twice more — a run total of 2 with every record
780
+ showing 1). **`plan-executed.replans` therefore now means the CHAIN total**; if you parse spines,
781
+ re-read that field. Each leg DECLARES what it inherited on its own `job-start` — new fields
782
+ `priorReplans` and `priorReplanGrantUsed`, present only when there is a fold — and the next
783
+ reader adds only its own window's `replan` records, exactly the way `priorSpentUsd` folds money.
784
+ The grant latch travels beside the count rather than being derived from it (`1` + `false` is a
785
+ real state: an ordinary ceiling spent, the arbiter's extra still unearned). The leg's own number
786
+ is not lost — a leg emits one `replan` record per replan it drafts, so `chain −
787
+ records-in-this-window` is the fold it inherited. You seed it with `resumeReplans` (below);
788
+ omitted is the cold path and byte-identical to a run nobody resumed.
678
789
 
679
790
  **Per-step model tiers — `providerFor` (P).** A plan step may declare a `model` TIER
680
- (`sonnet` \| `haiku`, `STEP_MODELS`). The tier menu is signed into the schema; mapping a tier
681
- onto a real model (and any per-model provider params) is the RUNNER's territory, so both
791
+ (`sonnet`, `STEP_MODELS` — `haiku` removed 2026-08-06 as a reversible attribution probe,
792
+ F87; the field stays legal and one-token reversible, and the narrowing binds only what the
793
+ AGENT may express, never what the operator may run).
794
+ The tier menu is signed into the schema; mapping a tier
795
+ onto a real model (and any per-model provider params) is the RUNNER's territory — nothing
796
+ forces the `sonnet` tier onto any particular model, and running haiku as your default worker
797
+ model stays an operator choice the plan menu never governed — so both
682
798
  `runJob` and `runPlan` take `providerFor?: (tier: string) => provider`. It is called ONLY for
683
799
  a step that declared a tier, once per such step — the scout, the plan drafter and every
684
800
  untiered step stay on `opts.provider`, so a caller that never plans tiers can omit it
@@ -792,7 +908,7 @@ their next green, not repaired.
792
908
  door**: before the clock, before the close precheck, before any token. On a pass, the
793
909
  **newest** version's plan rides into the FIRST drafting prompt as a starting draft
794
910
  (`bridge-loaded {name, versions, runid}` on the spine) and everything after it is the
795
- ordinary shipped path — same validator, same redraft-on-reds, same ONE-replan ceiling, and a
911
+ ordinary shipped path — same validator, same redraft-on-reds, the same replan ceiling, and a
796
912
  **replan never re-injects the bridge** (it drafts from the run's own state). On a fail the
797
913
  run returns the distinct terminal **`recipe-stale`** with `bridge-gate {outcome, name, reds}`
798
914
  and a decision-ready escalation, having spent nothing. **Falling back to cold is the
@@ -950,7 +1066,8 @@ semantics are byte-unchanged (PRD v1.46 §3):
950
1066
  implicit try opened at `job-start`, through the same window machinery rather than a second
951
1067
  reader. Without it such a spine is honestly refused (`started: false`), which is what a
952
1068
  reuse runner needs. `job-start` carries the DECLARED fold (`priorSpentUsd` with its
953
- `priorSpendComplete`, and `priorWallMs`, each present only when there is one) exactly as
1069
+ `priorSpendComplete`, `priorWallMs`, and `priorReplans` with its `priorReplanGrantUsed`, each
1070
+ present only when there is one) exactly as
954
1071
  `try-start` does, so a chain of resumes adds only each attempt's own new rounds instead of
955
1072
  re-deriving and double-billing. **An inherited FLOOR is one** even when the money it qualifies
956
1073
  is `$0`: a leg whose every round came back unpriced folds nothing countable and is still not
@@ -1018,6 +1135,23 @@ and is re-signed.
1018
1135
  that re-grades an unchanged tree is flat on its own evidence while the RUN — what the
1019
1136
  top-up decision is actually about — may have been converging when the money ran out. See
1020
1137
  the trend section above for what the seed does and, deliberately, does not touch.
1138
+ - **Its REPLAN LEDGER comes with it too — and this one is a BOUND, not a readout.**
1139
+ `restart.replans` / `restart.replanGrantUsed` ride into `runJob`/`runPlan` as **`resumeReplans`**
1140
+ (`{count, grantUsed} | null`), seeding the replan ceiling instead of leaving it reborn per call.
1141
+ Without it a kill refilled an allowance PRD v1.12 makes the RUN's — measured, a leg that spent
1142
+ both replans and step-redded handed its checkpoint to a second leg that replanned twice more.
1143
+ **It deliberately does NOT use `resumeGrades`' mechanism.** The grade seed reads ONE spine, so a
1144
+ resume of a resume inherits the previous leg's grades and not the whole chain; that shortening is
1145
+ fail-safe for a READOUT (it can only under-claim a direction) and is the DANGEROUS direction for
1146
+ a ceiling, where an under-claimed ledger is a refilled allowance and every kill buys one. So this
1147
+ seed follows the MONEY fold: each leg declares `priorReplans`/`priorReplanGrantUsed` on its own
1148
+ spine and the next reader adds only its own window, so leg 3 inherits the whole chain. It is also
1149
+ not the same class as the trend's ITERATIONS, which `src/trend.js` refuses to seed on purpose: an
1150
+ attempt allowance is a LEG bound bought with the leg's own money, while a replan ceiling bounds
1151
+ how many times the WORKFLOW may be redrawn, which is the run's span. Belted like every declared
1152
+ fold — a garbage, non-finite or negative `count` reads as `0` rather than widening the ceiling.
1153
+ Null/omitted is the cold path. The disarm control is pinned: a resumed leg with an UNSPENT
1154
+ ceiling still replans, so the bound is bounded, not switched off.
1021
1155
  - **Under the REMAINDER, never a fresh allotment.** The killed attempt's spend and wall
1022
1156
  FOLD IN (`runJob`'s `priorSpentUsd`/`priorSpendComplete`/`priorWallMs`, `createClock`'s
1023
1157
  `priorElapsedMs`), so
@@ -1080,9 +1214,13 @@ the same corpus), and `seq` continues monotonically across appends. Keys are
1080
1214
  across runs dedupes and distinct bugs in one verb don't merge. Classes, worst-first
1081
1215
  (`LEDGER_CLASSES`, frozen): `silent-degradation` (a failed `primitive-smoke` — the class
1082
1216
  failures can't derive), `runtime-red`, `provider-red`, `pricing-red` (F6), possibly-dormant
1083
- `capability-gap` (cap-halt + request-red in one spine), `broken-close` (consumer-attributed),
1217
+ `capability-gap` (cap-halt + request-red in one spine — one row per `(verb, lib)`, never per
1218
+ verb, so two territories asking for one verb string stay two rows rather than collapsing onto
1219
+ whichever territory was seen first), `broken-close` (consumer-attributed),
1084
1220
  `request-red` (admission demand for a locked verb — keyed on the red's structured
1085
- `verb` field, prose-quoted verb as legacy fallback), `retention-red`, `config-red`
1221
+ `verb` field, prose-quoted verb as legacy fallback; both classes take their `lib` from the
1222
+ red's own stamp, so neither seeds an upstream ask for a bareloop-catalogue refusal),
1223
+ `retention-red`, `config-red`
1086
1224
  (drafting friction — attributed to bareloop's own schema/prompt). Deliberate exclusions
1087
1225
  (`EXCLUDED_ESCALATIONS`, a runtime set — anything outside classified ∪ excluded is
1088
1226
  counted as unmapped, never dropped): `cap-halt`/`wall-halt` (budget stories, money and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bareloop",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Workflows that earn their own design, with receipts — agent-authored workflows for repeated, long, verifiable jobs that improve across runs under an un-gameable gate.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",