bareloop 0.10.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,357 @@ 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
+
8
359
  ## [0.10.0] — 2026-08-13
9
360
 
10
361
  ### Added
package/README.md CHANGED
@@ -32,20 +32,30 @@ bareloop takes that principle and makes it the product. You describe a repeated,
32
32
  job and what *done* means for it. The agent handles the rest, under verification it never
33
33
  gets to touch.
34
34
 
35
- ## Three kinds of done
35
+ ## Two kinds of done
36
36
 
37
- Jobs differ by who or what decides the result is good. That difference is the product's
38
- spine, so you choose it when you create the job.
37
+ Jobs differ by what decides the result is good. That difference is the product's spine, so you
38
+ choose it when you create the job.
39
39
 
40
40
  **green** — a predicate decides. Tests pass, the build exits clean, the type checker is
41
41
  quiet.
42
42
  > *"Fix the failing suite."* · *"Migrate this package to strict types."*
43
43
 
44
- **softgreen** — an assessment decides against a rubric.
45
- > *"Reserve a ticket from SF to LA under $400."*
44
+ **softgreen** — an assessment decides, against a rubric you wrote and signed. You say what
45
+ separates a pass from a fail, and give one example of each; the judge only quotes back what it
46
+ finds in the real work, and fixed rules — not the judge — render the verdict.
47
+ > *"Reserve a ticket from SF to LA under $400."* · *"Improve my resume."*
46
48
 
47
- **hitl** — a human makes the final call.
48
- > *"Improve my resume."*
49
+ ## You have the last word
50
+
51
+ Every run ends at a **review door**: here is what changed, here is what every check said, here
52
+ is what I'm asking you. **Accept** it, **rerun** it — what you type is the feedback the next
53
+ attempt works from, funded fresh — or **pause** it and decide another day. A pause costs
54
+ nothing, keeps everything, and picks up from the start of the last step whenever you come back.
55
+ Never come back, and it quietly expires on its own.
56
+
57
+ The door never rewrites the run's own verdict. A green stays green in the record whatever you
58
+ decide about it; your answer says what happens to the work, not what the work scored.
49
59
 
50
60
  ## Why the agent authors the workflow
51
61