bareloop 0.9.0 → 0.11.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,561 @@ 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.11.0] — 2026-08-18
9
+
10
+ ### Changed (2026-08-18 — the review door's third button)
11
+
12
+ - **The three doors are `accept` / `rerun` / `pause`; `cancel` is DELETED as a concept.** hamr:
13
+ *"what's the point of cancel anyways? pause can resume — that would be more honest"* and
14
+ *"rerun implies I don't like it, go again"*. A `pause` answer runs nothing, spends nothing,
15
+ buys no worker round, never opens the fix loop and does NOT consume the one-shot rerun
16
+ allowance: it mints the ordinary `hitl-pause` checkpoint with an explicit
17
+ `humanDecision: 'pause'` and leaves the run resumable from the start of its last step. An
18
+ unresumed pause simply expires under the existing 60-day `PAUSE_TTL_MS` — **that expiry is
19
+ what cancel used to be**, without forcing a permanent decision. `HUMAN_DECISIONS` is the
20
+ enumerated set the surface derives from, so `cancel` is now inexpressible rather than
21
+ rejected after the fact; `hitl-decision-red` still refuses any word that is not a door.
22
+ - **`hitl-cancel` is no longer mintable and the constant is gone** (removed from
23
+ `src/declaredclose.js` and the package exports). The ledger keeps the bare string in its
24
+ excluded-escalation set — the `gate-red` precedent — so a spine written before this change
25
+ still reads as governance rather than as a counted capability gap; no spine on disk carries
26
+ it (the door never fired live), so that entry is forward compatibility, not bookkeeping.
27
+ - **A human checkpoint row is classed `checkpoint`, not `casualty`** (`src/reuse.js`, both the
28
+ live try loop and the resume reconstruction). A casualty is a run that died; a checkpoint is
29
+ a run that is waiting on a person with nothing lost — every other reader already treated it
30
+ that way, and the row was the last place the machinery mis-described itself.
31
+ - **Runner surface (`scripts/` never ships):** `scripts/run-u.mjs --decide pause` answers the
32
+ door and launches nothing at all — it prints the decision, names the TTL and the ORIGINAL
33
+ runid, and exits having spent nothing. Deliberate: the runner writes one spine per leg, and a
34
+ leg that returns before drafting emits no `plan-accepted` and no `step-end`, so launching a
35
+ run to say "not now" would mint a runid whose own checkpoint is empty and re-pay for finished
36
+ steps on the next resume. Door rendering (`scripts/u-readout.mjs`) and the escalation
37
+ passthrough (`src/ralph.js`) carry the new third door.
38
+ - **Docs:** dated addendum on `docs/02-features/2026-08-17-softgreen-review-door-design.md`
39
+ (closed sections untouched), **PRD Addendum v1.72**, README's *"You have the last word"*,
40
+ `bareloop.context.md`'s terminal/decision surface, and `docs/01-product/LAYERS.md`.
41
+
42
+ ### Docs (2026-08-17 — decisions only; NO code changed, and no build is authorized)
43
+
44
+ - **New design record `docs/02-features/2026-08-17-softgreen-review-door-design.md`, and PRD
45
+ Addendum v1.71: `hitl` is RETIRED as a verdict class, its pause becomes the REVIEW DOOR at
46
+ the end of EVERY run, and `soft-green` becomes the forward path.** The hitl slice below FIRED
47
+ live first and worked — the first hitl pause in programme history, 64 strict errors to 0, the
48
+ authored guard catching the cheat genre twice, evidence package and three doors and a stopped
49
+ clock (F105). hamr then retired the class for what it asks of a composer and of a person:
50
+ *"checkers are subjective human experience that they should grade for"*, and *"it's hard to
51
+ apply deterministic flow on a probabilistic throughput"*. **The machinery is re-homed, not
52
+ deleted.** The door never changes the loop's own verdict (*"it's important not to change the
53
+ loop self verdict"*): a green stays green in the ledger and the door records a disposition —
54
+ `accept` (and, on soft-green, the release of quarantined learning credit), `rerun` (the text
55
+ IS the gap), `cancel` (*"the verdict stands, but I'm not taking the merchandise"*). A rerun is
56
+ a **FRESH ENGAGEMENT** with its own money and time and two side-by-side counters, and **any**
57
+ wall or halt now records full state — time, money, strikes, notes/progress and any pending
58
+ human decision. `soft-green` gets a `judged-floor` kind on the LOCATE+DECIDE pattern (the
59
+ judge extracts facts with quotes from the real artifact; an arbiter-owned `decide()` renders
60
+ pass/fail; unsure = red), behind a signed rubric card (Q6) and a signed calibration set (Q7)
61
+ the whole pipe must grade correctly before the close is signable. **Nothing is built.**
62
+ - **F102, F103, F104 and F105 in `docs/FINDINGS.md`**, each grounded in today's spines under
63
+ `bareloop-patients/litectx-maintainer-bareloop/`: a pending rerun decision does not survive a
64
+ wall-halt → resume (two byte-identical pause records — the human paid the same decision
65
+ twice); the wall folds across legs, so a decide-time rerun inherited 87 seconds; the
66
+ catalogue cannot count documentation, so a composer measured a typecheck PROXY instead —
67
+ which is what retired the class; and the positive record of everything the first live hitl
68
+ loop proved, including that rerun DELIVERY works and rerun CONVERSION is still unproven-live.
69
+ - **`docs/01-product/LAYERS.md`** gains the review door, the two-verdict table and the next
70
+ rung; the **2026-08-13 N4 build record** (closed) gains a dated addendum pointing forward;
71
+ **README** and **`bareloop.context.md`** carry the direction change, the contract still
72
+ describing what the code on this branch actually does today.
73
+
74
+ ### Added
75
+
76
+ - **N4 slice 1 — the `hitl` verdict class is ADMITTED, and the run PAUSES for a person.** A job
77
+ whose *done* needs someone to say so now runs through the same signed spec, the same
78
+ agent-authored plan and the same arbiter as a green job. The refusal was never one flag, so
79
+ every coupled site moved together: `LOCKED_VERDICTS` / `LOCKED_CLASSES` drop it,
80
+ `DECLARED_CLOSE_CLASSES` widens to `['hard','hitl']`, the class battery is the GREEN mechanical
81
+ battery inherited verbatim (hamr's OPEN-1 ruling: nothing a human stage cannot see), and the
82
+ interview gains a hitl question set (the green questions byte for byte, plus the one thing
83
+ they cannot supply — the question the signer actually answers). **`soft-green` stays
84
+ declared-but-locked**: its judged floor is slice 2, and a wobbly ruler must mint nothing.
85
+ - **`human-confirms` goes live as the one kind that measures nothing.** Its whole parameter
86
+ surface is `ask`; it cannot spawn, is never env-capable, is `offer: false` BY LAW (a
87
+ declaration that offers one is a validation red, and the arbiter's own bridge stamps the law
88
+ onto the stage the runner sees, so the agent can never compose `check-passes(<a person>)`), is
89
+ at-most-once, and must be the LAST stage — first-red-wins would otherwise silently delete every
90
+ mechanical stage behind it. It also SKIPS the seed-verdict read (ruling 8), and the skip is
91
+ recorded as a row rather than taken in silence.
92
+ - **Three new terminals, minted rather than borrowed.** `hitl-pause` is a decision-ready
93
+ CHECKPOINT carrying the evidence package (every mechanical stage's result, the close's own
94
+ question, and what the run changed) — never a bare "approve?"; `hitl-cancel` is terminal with
95
+ an explicit `gap: null`, no fix loop and no worker round; `hitl-decision-red` refuses a
96
+ decision the run cannot act on (a fourth door, a `rerun` with empty text, or a decision handed
97
+ to a close with no human stage) before anything is spent. All three are excluded from the
98
+ ledger's escalation counting for their own stated reasons, and none demotes a bridge. The
99
+ legacy `hitl-close` entry keeps its own meaning; nothing was renamed in passing.
100
+ - **The RETURN needs no new channel.** A resumed leg carrying the signer's `rerun` feeds their
101
+ words through the same seam `post.gap` uses — same bound, same scrub — and the run continues
102
+ under the folded wallet on an unbilled clock. The ruling is spent when the fix loop opens, so
103
+ the next machine-clean tree pauses for a second review instead of converting one sentence
104
+ forever, while a mechanical red in between is still converted normally.
105
+ - **New public exports**: `HUMAN_DECISIONS`, `SEED_EXEMPT_KINDS`, `normalizeHumanRuling`,
106
+ `NEVER_OFFERED_KINDS`, `HUMAN_PAUSE`, `CHECKPOINT_OUTCOMES`, `PAUSE_TTL_MS`,
107
+ `checkpointAgeGate`, `HITL_PAUSE`, `HITL_DECISION_RED`, `HUMAN_CHECKPOINTS`. The 60-day pause TTL and the
108
+ canonical checkpoint list live in the LIBRARY (hamr's OPEN-2 ruling) so the exported bundle
109
+ inherits them rather than every runner re-implementing them. The two terminal names are
110
+ spelled ONCE, in `src/declaredclose.js` beside the `HUMAN_PAUSE` close-verdict word they
111
+ translate, because four different readers key on them — the emitters (`src/planrun.js`), the
112
+ ledger's excluded-escalation set, the resume reader (`src/reuse.js`) and the runner script —
113
+ and hand-spelled copies of one terminal is how those readers come to disagree.
114
+ - **A registry guard that was only holding by accident**: an `already-green` terminal now mints
115
+ no bridge version for a NAMED reason (`green-predates-run`) rather than because no plan
116
+ happened to be on the spine — a hitl try that paused and came back with `accept` has its
117
+ predecessor's plan in the very same try window.
118
+ - **An authoring run says what it is doing while it is still doing it — three REPORTING seams,
119
+ `onPhase` / `onStage` / `onCall`.** The pipeline ran up to ~15 minutes between `author-start`
120
+ and its result with nothing on the terminal or the spine — a real survey ladder, a real
121
+ declaration ladder, and a real toolchain per close stage, all inside one await — and silence
122
+ is byte-for-byte what a hang looks like, so the operator's only lever was to kill a run that
123
+ might be working. The library REPORTS and the shell PRINTS, the same shape `runJob` → `runPlan`
124
+ already has: `onPhase` fires at the composition's boundaries in `authorCloseForJob`
125
+ (`src/authorjob.js`) and `authorClose` (`src/authorflow.js`) — `seed`, `scout`/`scout-done`,
126
+ `listing`/`listing-done`, `author`, `author-call`, `seed-read`/`seed-read-done`; `onStage`
127
+ (a new `Ctx` field in `src/kinds.js`) fires per seed-read stage, AFTER the stage lands, because
128
+ only then is there a verdict to carry; `onCall` fires from inside each of the two metered
129
+ recorders (`makeCostBook.add`, `runAuthorScout`'s `record`) rather than at the call sites, so a
130
+ paid call cannot be metered without its report going out. **Nothing here is consulted and
131
+ nothing decides**: `capStop` is still the one ceiling predicate, still asked between calls,
132
+ still reading the same entries — and every callback defaults to a no-op, so every existing
133
+ caller is byte-identical. Two deliberate omissions: `makeCostBook.absorb` does NOT fire
134
+ `onCall` (those calls were already metered and reported by whoever made them, and reporting
135
+ them twice would double the scout's spend in every reader downstream), and the `seed` phase is
136
+ announced only on the path that actually reads one, because a phase line for work nobody did is
137
+ the blind-instrument class one line wide. The renderer is `phaseLine` in
138
+ `scripts/author-readout.mjs` (a readout no test can reach is a readout nothing checks); it
139
+ prints WHAT is happening and never a fraction — a survey attempt has no progress and a suite
140
+ stage finishes when it finishes — and an unmeasured `durationMs` prints as `unknown`, never as
141
+ `0` (F6 in its time form). A phase the renderer does not know is printed rather than swallowed.
142
+
143
+ The runner half of the same slice landed beside it and is deliberately NOT in the tarball
144
+ (`scripts/` never ships): `scripts/run-u.mjs` gains `--decide accept|rerun|cancel` (+ `--text`)
145
+ on the existing `--approve <specHash>` signature, renders the pause's evidence package plus the
146
+ line-level diff, consumes `CHECKPOINT_OUTCOMES` and `checkpointAgeGate` instead of re-spelling
147
+ either, and prefers the run's own terminal over a stale watchdog record when folding the wall —
148
+ so a person's deciding time is never billed to it. What an adopter's own runner has to do is in
149
+ `bareloop.context.md`.
150
+
151
+ Beside it, `scripts/run-interview.mjs` — **the terminal interview, and the answer to hand-writing
152
+ an `answers.json`**, which is precisely the SWE tax this product exists to refuse. It asks the
153
+ LIBRARY's frozen questions ONE AT A TIME, **calls no model and costs $0**, and writes
154
+ `answers.json` (the shape `run-author.mjs` consumes) plus `specdraft.json` (the operator half — no
155
+ close and no `verdictType`, because those are what `run-author` authors). The draft is checked for
156
+ $0 by `validateJob`, the SAME validator that will judge the resolved spec, so a draft that cannot
157
+ become a spec says so before a provider is ever built. It then OFFERS to spawn the paid
158
+ `run-author.mjs` under its OWN `--budget` ceiling — **default NO** (`[y/N]`, the same lean the
159
+ pause's three doors take: the answer that costs nothing is the one you get by saying nothing), and
160
+ it prints the exact command either way. The two ceilings are kept apart out loud: `--budget` is
161
+ what the AUTHORING call may spend, `budgetUsd` is what the RUN may spend, signed into the spec.
162
+
163
+ And `scripts/run-u.mjs` gains a `litectx-maintainer` JOBS row — **N4's hitl PROVING job, dark
164
+ since the legacy `steps[]` deletion took its old spec's path with it**, back as a plan-flow job
165
+ with an AUTHORED close. `jobs/litectx-maintainer.json` **DOES NOT EXIST, and the row does not
166
+ create it**: the spec is authored live through `run-interview.mjs` → `run-author.mjs` and signed
167
+ by hamr, goal and answers included, and until it lands **the runner REFUSES BY NAME** — a row is
168
+ the runner's half of a job, never the signer's. The patient is a COPY at the convention this table
169
+ already uses, and the seed is that copy's own HEAD (litectx v0.32.0, `115213d`).
170
+
171
+ ### Changed
172
+
173
+ - **Both suite pins move up a minor — `bare-agent ^0.35.0 → ^0.36.0`, `bareguard ^0.12.0 →
174
+ ^0.13.0` — because on 0.x a caret locks the MINOR**, so the old pins admitted neither release
175
+ and the bump is the only way to consume them. **bare-agent 0.36 delivers BA-20**, the
176
+ productized decisive judge this queue reopened on: `judge` (verbatim request + one structured
177
+ artifact → `honored`/`broke` with a mechanical `where`, truncated/unparseable responses as
178
+ DISTINCT flagged outcomes excluded from graded denominators, composing **around** a provider
179
+ and never inside the Loop), the calibration harness (`calibrate`, `CALIBRATION_CASES`,
180
+ `INJECTION_BATTERY`, `constantHonored` — the negative control that lets the harness fail), and
181
+ the pure `judgeToAnnotation` mapping into bareguard's shipped fact envelope. **bareguard
182
+ 0.13.0** is the companion hardening: `gate.annotate` now REJECTS malformed facts into distinct
183
+ `annotate_malformed` rows instead of normalizing them into silently-`honored` ones, `verdict`
184
+ goes through the audit redactor (it was written RAW — a real secret-leak fix), `meta`'s
185
+ 1000-byte bound became a decoupled copy, and `__proto__` keys are dropped at every depth.
186
+ **That behaviour change is zero-cost to bareloop today**: it imports only `Gate` and `redact`
187
+ and has **no `gate.annotate` call site** at all. `npm ls` resolves `bare-agent@0.36.1` +
188
+ `bareguard@0.13.0`, deduped to one shared copy, and **full fresh gates are green against the
189
+ installed versions** — `npm test` 1569/1569 exit 0, `typecheck` clean, `build:types` clean.
190
+ **The judge is NOT consumed here**: its consumer is the N4 (soft-green) rung, so wiring it into
191
+ a `judged` close stage and executing BA-20's acceptance criteria live (running `calibrate`
192
+ against the frozen floor and naming the hash it graded) are deferred to N4's opening — a
193
+ verified delivery, not a verified acceptance.
194
+
195
+ - **The interview loses its repo question and widens its scope question — hamr drove the terminal
196
+ himself and ruled on the wording (2026-08-15).** `GREEN_QUESTIONS` drops *"Is there a code repo I
197
+ can look at? Where?"*: the repository is already MANDATORY STRUCTURED input (`runInterview`'s
198
+ `repoPath`, taken from `--patient`, without which nothing starts), so asking a person to re-type
199
+ a path the machine is holding is the SWE tax this product exists to refuse — and it invites a
200
+ second, drifting answer for one fact (hamr's own live answer was *"Yes — the patient itself."*).
201
+ Question 2 gains the other half of the same decision — *"…And which files should the work read or
202
+ draw from (they stay untouched)?"* The sets **renumber contiguously from 1**, so the green set is
203
+ five questions and hitl's signer-ask is now **6**, not 7. Nothing hardcodes a count: `run-author`,
204
+ `run-interview` and the suite all read `requiredAnswersFor(cls)`. **Adopter note: an
205
+ `answers.json` written before this change is SILENTLY MISREAD** — its key `6` was the repo answer
206
+ and now reads as hitl's signer-ask, and its key `7` is dropped as unasked. Re-run the interview
207
+ (it is $0 and calls no model) rather than hand-editing.
208
+ - **The interview's own prose stops citing finding numbers and says the mechanics out loud.** The
209
+ goal prompt no longer prints *"F87"* at a person who has never read the findings — the rule is
210
+ unchanged and the price is now named as a price. The fence prompt says what a fence IS (writable
211
+ files, everything else read-only), that patterns are repo-root-relative, and that absolute paths
212
+ are refused because the run works on a copy. The multi-line hint spells the keystroke: *press
213
+ Enter on an empty line, i.e. Enter twice*.
214
+ - **Suspended minutes stop charging a signed wall — the run clock is MONOTONIC** (hamr's ruling,
215
+ 2026-08-15: *"if computer suspended, time shouldn't count."*). `createClock`'s default source
216
+ moves from `Date.now` — CLOCK_REALTIME, which counts every minute the machine was asleep — to
217
+ `performance.now`, which reads libuv's `uv_hrtime` and so CLOCK_MONOTONIC, which per
218
+ `clock_gettime(2)` *"does not count time that the system is suspended"*. That was not
219
+ hypothetical: an s2idle suspend landing mid-run charged **45 phantom minutes against a
220
+ 45-minute wall**, and the same exposure covers an NTP step or a hand-set date. Nothing corrects
221
+ for a suspend after the fact; the reading simply never includes one. **The epoch change is safe
222
+ by construction**: every use of `now()` in `src/clock.js` is RELATIVE (`now() - startedAt`) and
223
+ no value derived from this clock is ever published as a calendar timestamp — `report()` carries
224
+ durations only, and the `at:`/`ts:` stamps a run writes come from their own `Date.now` seams in
225
+ `src/reuse.js`, which stay calendar-correct on purpose. `now` is still injectable, which is what
226
+ every clock test drives. Three runJob-level wall tests mocked `Date` because that is the only
227
+ seam `runJob` exposes, and the monotonic source made them BLIND — they redded `provider-red` /
228
+ `plan-red` instead of `wall-halt`, which is the failing-first evidence that the mock was
229
+ load-bearing; they now go through one shared helper (`tests/helpers.js`) that pins
230
+ `performance.now` to the mocked `Date` and restores it after. **KNOWN LIMITATION, recorded
231
+ rather than fixed** (PRD Addendum v1.67, hamr: *"leave it and mark it in prd as possible known
232
+ limitations"*): the OUTSIDE watchdog still measures silence in CALENDAR time, so a long suspend
233
+ can still read to it as a stall. The half that charges money is fixed; the half that reads
234
+ silence is not.
235
+ - **`scripts/run-u.mjs` PRINTS the sleep-inhibitor launch line** the way `scripts/run-reuse.mjs`
236
+ has since F72, instead of carrying it as a comment — a rule nobody reads at the moment they
237
+ launch (and like every runner change here, `scripts/` never ships in the tarball). A suspend
238
+ freezes EVERY guard: the outside watchdog is a POLLER, so it cannot observe — let alone kill — a
239
+ run whose machine is asleep, and after the monotonic clock above the run's own wall does not
240
+ count those minutes either. A suspended run is therefore simply UNWATCHED for as long as it
241
+ sleeps, and the launch banner is the last moment the operator can still do something about it.
242
+ The printed line is `systemd-inhibit --what=idle:sleep --why="bareloop u run" env …`, with `env`
243
+ before the key assignment on purpose: `systemd-inhibit` execs a COMMAND, and a bare `VAR=value`
244
+ prefix is shell syntax it would try to run as one — the key still rides an env assignment and
245
+ never argv. A pause with no ruling prints the line prefixed to NONE of its three doors, because
246
+ prefixing one would quietly recommend it and this surface's lean runs the other way.
247
+ - **The hitl terminals get one canonical spelling, and the pause screen gets one renderer**
248
+ (`/code-review medium`, findings 2 and 3). The terminal names were hand-spelled string
249
+ literals at every reader: 9 sites in `src/planrun.js`, the ledger's `EXCLUDED_ESCALATIONS`
250
+ set, `src/reuse.js`, the `src/index.js` re-export and 7 sites in `scripts/run-u.mjs`. They now
251
+ all import the constants exported from `src/declaredclose.js` (the `hitl-cancel` constant
252
+ introduced with them is gone again — see the review-door entry above). Beside it,
253
+ `printPauseEvidence()` in `scripts/run-u.mjs` becomes the SINGLE assembly point for both pause
254
+ screens — the resume preview and the fresh-pause terminal — so the fallback rule cannot drift
255
+ between them: with a pause record the package is the close's own ask plus the diff of what
256
+ changed; without one it says so plainly, rather than rendering the bare "approve?" that ruling
257
+ 2 forbids. Finding 1 — `runReuse` classing a `hitl-pause` as a casualty and buying another try
258
+ — was parked as arbiter territory when this bullet was first written, and has since been FIXED
259
+ on this same branch: `d183c95` gave the checkpoint its hard stop (`hardStop` reads
260
+ `HUMAN_CHECKPOINTS` before the retry table, so the loop hands the run back rather than re-asking
261
+ a question already put to someone who has not answered), and `4e0ab94` gave the ROW its own
262
+ `checkpoint` class, so the machinery no longer mis-describes a waiting run as one that died.
263
+
264
+ ### Fixed
265
+
266
+ - **`--lang` given with no value no longer poisons the command the interview prints.** `arg()`
267
+ returns the EMPTY STRING for a flag typed with nothing after it and `?? 'js'` passes an empty
268
+ string straight through, so the printed next-step command collapsed to `--lang --budget 2.5` —
269
+ copy-paste it and `run-author` reads the word `--budget` as its language. Both scripts now stop
270
+ LOUD on a present-but-empty `--lang` (an ABSENT flag still takes the default, the same rule the
271
+ money ceilings are parsed by).
272
+ - **A crashed authoring run left one line and no body — `scripts/run-author.mjs`'s spine now says
273
+ it died.** A real run's `author-<runid>.jsonl` held exactly ONE line, `author-start`, and
274
+ stopped: the paid pipeline threw, the error reached the operator's terminal as an unhandled
275
+ rejection, and the spine's record of the run was **byte-for-byte what a run still IN FLIGHT
276
+ looks like**. A log that cannot tell a death from a hang is not a record of either — which is
277
+ why this is a fix to the record and not a new feature, even though the mechanism is new
278
+ records. The fallible span — from just after `author-start` through the end of the main flow, a
279
+ real scout, a real model call and a real toolchain per close stage — is now ONE try/catch, and
280
+ nothing above it is inside, deliberately: the argv/config `die()` paths run before the spine
281
+ file exists, and a crash record with no spine to land in is a record nobody can read (those
282
+ still stop loud on stderr and 2). **The catch RETRIES NOTHING and SWALLOWS NOTHING**: the whole
283
+ error reaches stderr FIRST and verbatim — before the two writes that could themselves fail,
284
+ because a diagnosis that only arrives if the disk is writable has a dependency nobody asked for
285
+ — and only then is it written down as `author-crash` plus `author-end {outcome:'crashed'}`.
286
+ `crashRecord` lives in `scripts/author-readout.mjs`, where the runner's other
287
+ untestable-in-place rules live: `name`/`message`/`code` go through `redactSecrets`, and the
288
+ STACK — the evidence, and the string most likely to quote a credential out of a URL or an env
289
+ value into a file that outlives the run — goes through `scrubRaw`, the ONE persist boundary,
290
+ which redacts over the same `SECRET_PATTERNS` inventory the validator reds on and bounds it with
291
+ the bound announcing its own size (F28). One level of `cause`, no chain-walking. A non-Error
292
+ throw is recorded honestly rather than coerced into an Error shape. The detail is said ONCE, on
293
+ the crash record; `author-end` carries only the outcome, because two hand-spelled copies of one
294
+ fact are two instruments that can disagree (this file already paid for that in the
295
+ cap-halt/pricing-red type). The spine write is itself guarded — a crash handler that crashes
296
+ destroys the report it was built to make (F70) — and a failed append says so out loud while the
297
+ original error still stands. **NEW EXIT CODE 4**, distinct from 1 (a refusal or a failed gate),
298
+ 2 (operator/config) and 3 (a leak): a crash is none of those, and sharing a code with a refusal
299
+ would file a bug as a result. The leak scan still OVERRIDES a 4 deliberately — a written secret
300
+ is the harder line, and the crash keeps both of its own louder channels. **No reader changes**:
301
+ `classifyIncidents` (`src/ledger.js`) keys on event types it does not emit, so both new records
302
+ fall through every branch and are simply not counted — uncounted, never miscounted, which is
303
+ the fail-safe direction.
304
+ - **A KILLED authoring run kept losing 100% of its own money record — it now leaves a body, the
305
+ way a crash does.** The crash catch above covers a THROW and never a SIGNAL, so a `^C` on a run
306
+ that merely looked hung ended the process with the spine holding one line, `author-start`, and
307
+ with the spend existing only inside the cost book, which dies with the process (F6/F12: a
308
+ halted attempt's spend was invisible by 300×). `scripts/run-author.mjs` now handles `SIGINT`,
309
+ `SIGTERM` and `SIGHUP` by emitting `author-killed {signal, phase, …cost}` plus
310
+ `author-end {outcome:'killed'}` — `appendFileSync` inside `emit` is synchronous, so the record
311
+ is on disk before the process goes — then removing its own listener and re-raising the signal,
312
+ because the honest exit code for a signal death is 128+signo and any code set here would file a
313
+ killed run under a name this runner's vocabulary already spends on something else. **SIGKILL is
314
+ not covered and is not pretended otherwise**: it is uncatchable, and there is no handler to
315
+ write for it. The killed report and the live running total read ONE list (`metered`) through
316
+ `tallyCalls` — the same reader the library's own cost book uses — rather than a second
317
+ hand-spelled accumulator, because this file has already paid once for a pair of instruments
318
+ over one fact (the cap-halt/pricing-red type); `costUsd` rides as `null` when a call was
319
+ unpriced, so the known half is reported as a `≥` floor instead of `?? 0` laundering unknown
320
+ into $0. Both reporting seams are wrapped best-effort (`try/catch`) on purpose: progress
321
+ reporting on a PAID run must never take down the work being paid for (F70), and the run's real
322
+ records — `authored.json`, the crash catch, `author-end` — are all downstream and unaffected.
323
+ - **`scripts/run-interview.mjs` stops making an offer that can only be refused.** With no
324
+ `ANTHROPIC_API_KEY` in the shell, `run-author.mjs` exits 2 at its own door before a spine
325
+ exists, so the *"Run it now? [y/N]"* question had exactly one possible outcome and spent a
326
+ person's attention on a choice they did not have. Unkeyed, the question is not asked at all and
327
+ the closing line says which state it is in (*"Not offered — there is no key in this shell to
328
+ run it with"*) rather than reporting a refusal typed on the operator's behalf. What is printed
329
+ instead is what is actionable: the exact command, and one line saying to set the key in the
330
+ shell from their own secret store — deliberately WITHOUT a specific incantation, because which
331
+ store a person keeps a key in is theirs, and the one thing this script must never do is put a
332
+ key anywhere a command line can be read from.
333
+ - **A run that landed its own terminal now dates its own stop (N4 surface §1.6).** The resumed
334
+ leg's `deathAt` preferred the watchdog's kill record whenever one existed. That is right for a
335
+ run that was KILLED — the process was alive, silently, right up to the signal, and billing only
336
+ to its last emitted event under-reports the wall it really burnt — and wrong for a run that
337
+ ENDED ITSELF. A `hitl-pause` is the second kind: a clean terminal a person may answer days
338
+ later, and any record dated after it bills their deciding time to the run's wall. The POC
339
+ measured the shape — a report 45 days after the pause put `RESUME_WALL_MS` at 0 and doomed the
340
+ resumed leg before it opened, on a run that had barely started. So the preference order is not
341
+ *"the later record wins"* but *"the run's own record wins when there is one"*: a spine carrying
342
+ a `job-end` returns `null`, which hands `readResume` back its own documented default (the last
343
+ event's timestamp, which for a clean terminal IS the terminal). The order moved out of
344
+ `scripts/run-u.mjs` into `deathAtOf` (`scripts/u-readout.mjs`) so a test can reach it at all,
345
+ with the killed-run case kept as the control, and an unreadable watchdog stamp reads as UNKNOWN
346
+ rather than handing a `NaN` to a fold (F6).
347
+ - **A pause is a PHASE — the resume readout stops walking off the end of the plan (N4 #9).** hamr
348
+ read `at step 2 of 1 "(unknown)"` on the first paused resume preview he drove. A pause happens
349
+ AFTER the plan's steps, at the close's human stage, and the checkpoint a person actually meets
350
+ reads `phase:'steps'` with every step green (the pause site emits no `outer-close` of its own),
351
+ so the `at` line's step arithmetic ran past the plan and named an id nobody drafted. The same
352
+ walk-off was reachable one terminal over with no person involved: a kill between the last
353
+ `step-end` and the `outer-close` leaves exactly that shape and printed `step 3 of 2`. The
354
+ rendering moves to `resumeAtLines` (`scripts/u-readout.mjs`, where a test can reach it) and
355
+ holds one rule — a step count never exceeds the plan, and the phase is said in words: the human
356
+ review, the close and its fix loop, the close after an exhausted plan, or a named next step.
357
+ Rendering only; it decides nothing and bounds nothing.
358
+
359
+ ## [0.10.0] — 2026-08-13
360
+
361
+ ### Added
362
+
363
+ - **The close-AUTHORING pipeline gets a money ceiling** (`--budget`, `ceilingUsd`) — the lever
364
+ parked at v0.9.0 as arbiter territory, approved by hamr. Until now the flow **metered** spend
365
+ and nothing **bounded** it: the runner printed a total and no number anywhere could stop a
366
+ call. It has **NO DEFAULT** — omitting it runs UNBOUNDED and the runner **prints that** before
367
+ the provider is built, because a defaulted cap is a silent second ceiling (the `maxWallMs`
368
+ precedent) and an unbounded run must be a visible operator choice. A malformed value is an
369
+ error, never a silent fall back to unbounded. The ceiling **binds BETWEEN metered calls** —
370
+ the survey's attempts and its F59 recovery round, and the declaration loop's author call,
371
+ each revise, and each malformed-emission retry — because a cap that binds mid-call kills the
372
+ row before it can be graded (F45). ONE number reaches BOTH paid seams, and spend already
373
+ incurred folds in, so re-entering cannot silently widen it. **F6 keeps its own axis**: a
374
+ null/unknown cost never counts as $0, so spend that cannot be *known* stops as `pricing-red`
375
+ rather than passing silently — while known spend at or over the cap is `cap-halt` even when
376
+ the total is unknown, since that breach is certain on the priced half alone. The stop is a
377
+ governance stop, not an error: it names the cap and the spend, nothing retries, partial
378
+ artifacts stay on disk, and it is DISTINCT from `artifact-red` and `provider-red`. A survey
379
+ the ceiling stopped is named a money stop (`budgetStop`, typed cause `not-funded`, outside
380
+ `SCOUT_RETRY_CAUSES`) instead of blaming a model that never spoke. One predicate, one
381
+ spelling (`capStop`/`tallyCalls` in `src/text.js`, beside `priceOf` and for the same reason).
382
+
383
+ - **A resume may DERIVE `wall-halt` from a recorded `step-red`.** Only when that run's OWN
384
+ spine shows the wall crossed BEFORE the terminal was minted — a derivation from the primary
385
+ record, which never rewrites the recorded outcome and announces itself with a banner. The
386
+ terminal's NAME is what gates resume (`readResume`'s `resumableOutcomes`, which the operator
387
+ runner supplies as `cap-halt` / `wall-halt`), so a run mislabelled `step-red` past its own
388
+ wall silently lost its resumability. **It is a PERMANENT read-side safety net, not a
389
+ migration shim** (ruled 2026-08-13; the expiry question that was parked here is closed). The
390
+ defect that minted the shape is fixed at source, but spines are append-only forever, so the
391
+ population it protects can never shrink; it fires only on the run's own un-forgeable
392
+ `wall-bounded` record, cannot rescue a `step-red` that has no wall record in front of it, and
393
+ is a never-taken branch on a healthy spine.
394
+
395
+ ### Changed
396
+
397
+ - **The two revise-loop tighten-only clamps get a detector.** `maxRevisions` and
398
+ `structureRetries` have been clamped since `40672ae`, but nothing could *fail* if they
399
+ stopped clamping: MEASURED, three mutations survived the whole suite — including deleting
400
+ the `structureRetries` clamp outright, a straight revert of the fix that added it. The two
401
+ existing tests pinned only the ceiling and the floor, and neither can fail when the clamp
402
+ stops honouring values *between* them; the direction the rule exists to permit — the
403
+ operator **lowering** a bound — was the untested one. Seven tests now cover the middle of
404
+ both ranges, the non-numeric floor, and `Infinity` (a widening, which clamps to the ceiling,
405
+ matching `runAuthorScout`'s own reading). The three floors are pinned as *deliberately
406
+ different* (`SCOUT_ATTEMPTS` at 1, both revise axes at 0) so a later "harmonisation" has to
407
+ argue with a test. No production behaviour changed — this is the detector the rule was
408
+ missing (F45's class: a frozen rule without a wired detector is prose, not protection).
409
+
410
+ - **A stop past the wall never funds a replan draft, on ANY trigger.** W-2's *"no new step
411
+ starts"* now covers the drafting call itself (hamr: *"it's a replan that is doomed to
412
+ die"*), and the guard is trigger-agnostic rather than wired to one path. A declined grant is
413
+ not consumed. With time left on the clock every one of these paths is byte-identical to
414
+ before — pinned as a control.
415
+ - **The drafter is told its worker has no shell.** A replan wrote an action opening *"Run
416
+ `npx tsc --strict --noEmit`…"*; `run` is locked out of `TOOL_MENU` permanently, so the
417
+ worker obeyed as far as its verbs allowed — 25 reads and greps, zero edits, a whole step
418
+ spent hunting six errors by hand, on a plan that validated clean. The no-shell law now sits
419
+ in the DRAFTER prompt unconditionally, beside the exit-freedom law, with `read`/`grep`
420
+ glossed as the worker's only eyes; the scout and worker prompts already carried it, and the
421
+ one component that WRITES instructions did not. **A regex over action prose was explicitly
422
+ refused** (F86's anti-precedent): the same sentence is legitimate in an operator's signed
423
+ goal, so the instrument is a prompt register, never a matcher. F95.
424
+ - **The worker persona REGISTERS the arbiter's books instead of leaving them to be
425
+ discovered.** `PERSONA_TOOLS` now names `gate-audit.jsonl`, `.smoke`, `.litectx` and the
426
+ run's spine as always-denied — records of how the worker is judged, holding nothing about
427
+ its task — beside the absolute-path law it is the twin of, so it renders for every worker on
428
+ every grant (a `write`-only grant gets no component strategy paragraph at all and must still
429
+ be told). **The fence is unchanged**: nothing widened, no book became readable. The only way
430
+ to learn the rule was previously to spend rounds of a bounded attempt on it. F98.
431
+ - **Both signing surfaces show the GOAL and the judged stages as ONE reading.** F87's law is
432
+ that the goal must state everything the close judges while NOTHING derives one from the
433
+ other or checks them against each other — so the sole defence is the signer reading both
434
+ halves at once, and neither surface offered it: `run-author` printed the declaration and
435
+ never the goal, `run-u --approve` printed the goal and never the declaration. `run-u`'s gate
436
+ now names every close stage (name + kind) under the goal, **for BOTH spec forms** — the
437
+ pairing was first gated on `closeDecl`, so 10 of the 11 shipped specs (the command `close[]`
438
+ form) still met the signer with a goal and none of the stages that judge it, the exact
439
+ half-reading this entry claimed to end; `closeStagesOf(spec)` now renders either form, and a
440
+ command stage prints `[command]` rather than the catalogue kind name `command-exit` it does
441
+ not carry (spec hashes and exit codes byte-identical, proven live). `run-author`'s block moves to
442
+ **`scripts/author-readout.mjs`** (`declarationLines(spec)`) and gains the goal above it,
443
+ rendered from the RESOLVED spec — the bytes that get hashed. It moved because the block was
444
+ otherwise reachable only after a paid scout and a paid model call, and a readout no test can
445
+ reach is a readout nothing checks. An absent goal renders as ABSENT, never as a bare label.
446
+ Still a READING and not a validator — comparing the two is the move F87 forbids.
447
+ - **README rewritten value-first** (hamr's framing, straight to `main`). The pitch now leads
448
+ with the trust-but-verify lineage (the ralph loop, code machine MCP, and RLMs converged on
449
+ the same principle), the three kinds of done (green / softgreen / hitl, one everyday
450
+ example each), why the agent authors the workflow (forty-plus primitives — counted 51+
451
+ across the suite's real surfaces — and no one recipe picked), and "build it once, keep it"
452
+ (localhost UI, whole-workflow export with its self-healing harness). One `[WIP]` badge is
453
+ the only status marker; all mechanics, layer tables, findings numbers and rung history
454
+ moved out of the pitch — `bareloop.context.md` stays the contract, the README the pitch.
455
+ - **`step-stalled` joins the RESUMABLE halts** (hamr's go, 2026-08-13). A stall that trips with
456
+ time left and only reaches the replan gate after the wall has expired kept its NAME — `run.js`
457
+ keys the F44 spend floor on that exact outcome, and renaming it would report unknown spend as
458
+ an exact total — but the name is also what `readResume` gates on, so keeping it cost the run
459
+ its checkpoint. A `step-stalled` spine now previews as **RESUME**, folds the spend floor in as
460
+ `>=$x`, and re-enters at the recorded branch and step. A pure widening of the resumable set:
461
+ nothing else about the terminal moved, and `reuse.js`'s D4a path was checked unaffected.
462
+ - **The arbiter-book names get ONE home.** `gate-audit.jsonl`, `.smoke` and `.litectx` were
463
+ spelled independently in `isArbiterBook`, in BOTH fence deny lists, in the `PERSONA_TOOLS`
464
+ prose and in `run.js`'s smoke root — five sites, the exact drift class F98 had just paid for,
465
+ with the prose copy unguardable by any test. `kinds.js` now exports
466
+ `SMOKE_STORE`/`LITECTX_STORE`/`GATE_AUDIT_FILE`/`ARBITER_BOOK_STORES` and all five consume it.
467
+ Purely mechanical and verified as such: the deny-list joins and the persona prose are
468
+ **byte-identical** to the strings they replace, so no fence decision and no worker prompt
469
+ changed, and there is no import cycle (`kinds.js` sits below `tools.js`/`run.js`).
470
+ `tools.test.js`'s literal-spelling guard now asserts the fence consumes the constant — the
471
+ drift it guarded is impossible by construction.
472
+
473
+ ### Fixed
474
+
475
+ - **A time-stop is NAMED a time-stop — W-2 closes over the sibling terminals.** The ruling
476
+ (*"when time is up, keep the grade we already have and stop"*) was implemented on the
477
+ cap-halt path and not on the variance path: one run minted `step-red` **9.9 seconds past its
478
+ own wall**, having recorded the crossing in its own spine 5.6 seconds earlier. Now **every
479
+ variance and cap-halt terminal re-reads the clock before minting**, through ONE shared
480
+ `wallHaltTerminal` emission site — one site, one spelling —
481
+ and the ladder's cap-halt past the wall is a `wall-halt`. **`step-stalled` is pinned OUT of
482
+ the class by test:** `run.js` keys `spendComplete:false` on that exact outcome name because
483
+ an abandoned-and-reissued call may already be billed, and relabelling it would report a
484
+ floor as an exact total (F6 in a self-heal coat). **With both allowances exhausted the MONEY
485
+ cut is reported** (`cap-halt` wins over `wall-halt`) — hamr: *"understood"*; recorded so the
486
+ choice stays visible. F96 / PRD v1.59.
487
+ - **A declared `count-not-worse` red gap carries the LINES it counted, not just how many.**
488
+ `"8 match(es)"` with no file, no line and no error code is a number with nowhere to open —
489
+ F28's rule (*the close's output format is part of the contract*) recurring one rung later
490
+ inside the authored-close executor, with the `gapKeep` machinery it needed sitting in the
491
+ same file. `parseValue` now returns `matched`: the **KEPT** lines only — those that survived
492
+ the scope filter and reached aggregation, so a scope-DROPPED line is never named and the
493
+ worker is never aimed outside its own population (F84). The harvest is **per-term**, not
494
+ global: each term's kept lines nest under that term's own breakdown row — the one place its
495
+ `+n`/`-n` already prints — a `first` aggregate echoes only the one line it read, and dedup is
496
+ WITHIN a term, so a line feeding two terms plays both roles and appears under each. Harvested
497
+ globally, a negative-sign term's lines and a `first` term's never-read lines were echoed flat
498
+ as *the lines the count is made of*, handing the worker a line to go fix when that term
499
+ SUBTRACTED; labelling per term rather than dropping negatives keeps the subtracted term
500
+ visible, because under higher-is-better that is where the news is. The value is
501
+ still computed from the parsed values and never from the list. The lines ride the EXISTING
502
+ channel and invent none: the stage's `gapKeep` prefix on every line (Layer R's `redKeep` is
503
+ DERIVED from it), the existing `GAP_LINE_CAP`, and the announced trim on overflow.
504
+ **Nothing here can flip a signed spec hash** — verdict logic, baselines, counting,
505
+ `closeDecl` schema and `detail` untouched. Measured, not asserted: the identical job, same
506
+ patient, same hash, went from stalling at 8 and dying to **67 → 8 → 1 → 0** in three
507
+ iterations. F98.
508
+ - **A fence deny streak ends the ATTEMPT, never the RUN — `gate-red` is extinct as a mintable
509
+ terminal.** A converging run (close reading 46 → 10 → 8, no strikes) probed three of the
510
+ arbiter's own books, the fence denied all three CORRECTLY, bare-agent's BA-11 deny-spin
511
+ guard ended the loop, and the plan flow mapped that to a terminal `gate-red` — escalate,
512
+ never retry. The run died with **$1.77 and 21.6 wall-minutes unspent, on a fence that had
513
+ worked perfectly.** BA-11's stop now joins `max_turns` in the **bounded-attempt lane on BOTH
514
+ worker surfaces**: `attempt-bounded` carries the `reason`, the close judges the partial
515
+ work, the gap feeds forward, caps unchanged, the loop continues. **And the next attempt is
516
+ told which bound actually fired.** All three attempt bounds wrote the same bare iteration
517
+ number, so the following prompt rendered *"CUT OFF after N tool rounds"* for a denial streak —
518
+ false on cause and count, aiming the worker at its read budget instead of at the fence, while
519
+ `r.error` reached only the spine. The bound now carries `{iteration, cause, reason}`, and the
520
+ denial branch quotes the recorded `denied:<tool>` terminal verbatim — bare-agent returns the
521
+ tool name only, so no path and no streak count is invented — trimmed under `GAP_TRIM_MARKER`
522
+ and scrubbed once at capture, one scrub shared by prompt and spine (an append-only spine that
523
+ captures a key captures it forever). The round-bound sentence stays **byte-identical**, pinned
524
+ by block-equality after an `includes` guard survived a sabotage prefix. This is F32's routing rule
525
+ applied to a new stop cause, not a new one — a stop arriving AFTER gate-audited worker
526
+ writes is non-terminal by construction, and a denial is its most benign member. **Nothing is
527
+ widened**: the same actions are denied and the same audit rows written; a genuine scope
528
+ escape now costs one bounded attempt instead of the whole run. The category stays in
529
+ `EXCLUDED_ESCALATIONS` and in `ralph`'s passthrough decision table — that set is EXECUTABLE,
530
+ so dropping the name would re-file any future emission as a counted capability gap rather
531
+ than delete it. F98.
532
+ - **A malformed money ceiling is an ERROR at the library seam, never a silent UNBOUNDED.**
533
+ `capStop` read `'2.50'`, `NaN`, `Infinity`, `true` and `{}` as *no ceiling* while
534
+ `makeCostBook` advertised the same value straight back — the advertised and the enforced
535
+ budget were different numbers, the one thing a budget may never be — and the only guard
536
+ (`parseCeiling`) was CLI-side, so every library caller ran unprotected. A non-finite non-null
537
+ now throws **before the first paid call**: it costs $0 and propagates uncaught, so no catch
538
+ can launder it into an ABSENT survey. `null`/`undefined` stay the stated operator choice for
539
+ unbounded; `0` and negatives stay finite and cap-halt immediately, pinned as they were.
540
+ - **`budgetStop` latches only when the ceiling actually ended the ladder.** The recovery-seam
541
+ latch fired for EVERY final cause: a 1-turn transport death and even a healthy PRESENT survey
542
+ that spent past the ceiling both came back `budgetStop:'cap-halt'`, on a field whose own
543
+ docstring says *"the ceiling, when it is what ended the ladder"* (the second instance was not
544
+ in the finding). The recovery predicate is settled first now, and `capStop` consulted only
545
+ when a recovery call was actually PENDING. The genuine cap-plus-transport-death case keeps its
546
+ `cap-halt`, but the refusal detail **concedes the concurrent cause instead of contradicting
547
+ the `ENETUNREACH` it quotes** — and concedes it for every INCOMPLETE cause, not only transport
548
+ death: a SHORT/EMPTY/UNPARSEABLE survey whose repair round the ceiling refused was still
549
+ emitting *"the survey stopped on the authoring ceiling, not on anything it read"* directly
550
+ above the reply it was quoting. `CALL_FAILED` keeps the transport wording; `NOT_FUNDED` — no
551
+ call ever made — keeps the ceiling-only wording, the one case where it is true. No budget
552
+ semantics moved: the PRD already said only the former is why the loop ended, and the code now
553
+ honours its own sentence.
554
+ - **The authoring spine event rides its own stop.** `scripts/run-author.mjs` hardcoded
555
+ `type:'cap-halt'` on the governance readout even when the stop was `pricing-red` — the only
556
+ site in the repo where a spine event's type contradicted its own category, and a `pricing-red`
557
+ event carrying *"not under cap"* sends the operator off to raise a number when the repair is a
558
+ priced provider. The old spelling could also arm `ledger.js`'s capability-gap fuse on a run
559
+ whose wallet was never empty (latent — no script feeds author spines to `updateLedger` today,
560
+ but type-keyed slicing is exactly how F45's misread happened). The emit now spends
561
+ `authored.stop` for both fields, so meaning derives from the stop once.
562
+
8
563
  ## [0.9.0] — 2026-08-09
9
564
 
10
565
  The **close-authoring rung**. The close stops being a hand-written script and becomes a