bareloop 0.14.0 → 0.16.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.
@@ -1,31 +1,37 @@
1
1
  # bareloop — Integration Guide
2
2
 
3
- > **Current through Layer 2 (ACCEPTED, F47)**: the plan-v1 flow — a four-field job shape,
4
- > agent-authored validated plans, in-run check exits — validated end-to-end on the
5
- > real-model acceptance battery (3/3 conversion, 3/3 over the owned bar, the agent
6
- > composing its own check exits). **The plan shape is now the ONLY shape**: the legacy
3
+ > **Current: v0.16.0.** The verdict classes are **`green` + `soft-green`**. `soft-green` has
4
+ > a real judged floor (`judged-floor`, `src/judged.js`): a pinned haiku-4.5 judge LOCATEs
5
+ > facts with quotes from the real artifact, an arbiter-owned `decide()` renders pass/fail from
6
+ > a signed rubric card, and *unsure* is a red, never a pass — bought by a signed calibration
7
+ > gate (ten graded truths, five resisted lies, floor 10/10, itemized reds). A soft-green pass
8
+ > mints its bridge row QUARANTINED: no learning credit until an `accept` releases it, forward
9
+ > only; a rerun records a disposition and its outcome is a NEW run's own row, born held
10
+ > again. Every run ends at the signer's **review door** — `accept` / `rerun` / `pause` — which
11
+ > records a disposition and **never changes the loop's own verdict** (`runPlan` still returns
12
+ > `green`/`already-green` exactly as before). The door
13
+ > is open **always for `soft-green`**, and **opt-in for `green`** via `reviewDoor: true` /
14
+ > `--review-door` (the flag wins in both directions, never inferred); `accept` re-runs the
15
+ > close's MECHANICAL stages only — never a judged floor, never a person. Shipped in v0.12.0
16
+ > (2026-08-23); see CHANGELOG `## [0.12.0]` onward for the softgreen rung and the fixes/rungs
17
+ > that followed through v0.15.0. **The plan shape is the ONLY shape**: the legacy
7
18
  > operator-authored `steps[]` path, config-v1 and the draft-PR hitl step were deleted
8
19
  > 2026-07-26 (PRD v1.32), so `interpret`, `validateConfig` and `extractRules` are gone from
9
- > the public surface — a breaking change. `hitl` and `soft-green` return as a Layer 3
10
- > decision, rebuilt in the plan shape rather than ported. Runs on two worker surfaces —
11
- > the API `Loop` and, for
12
- > `clipipe-subscription`, the CLI's native tool channel (module 4d). API sections fill in
13
- > as build-ladder rungs land (PRD §10). What is settled — the boundary, the architecture,
14
- > the refusals, the constraints — is settled for good. Per LIBRARY_CONVENTIONS §3 this
15
- > file ships with the package and is the complete adopter contract; the README is only
16
- > the pitch.
17
-
18
- > **DIRECTION CHANGE — 2026-08-17 (design decided, NOT built; nothing below has moved yet).**
19
- > **`hitl` is RETIRED as a verdict class.** The verdict classes become **`green` +
20
- > `soft-green`**, and the hitl pause machinery is **RE-HOMED, not deleted**: it becomes the
21
- > **review door** at the end of EVERY run (green and soft-green alike), where the person
22
- > `accept`s, `rerun`s (their text is the gap, funded as a FRESH engagement with its own money
23
- > and time) or `pause`s — and **the door never changes the loop's own verdict**. `soft-green`
24
- > gets a `judged-floor` stage behind a signed rubric card and a signed calibration set.
25
- > **Everything documented in this file describes the code as it actually is on this branch**,
26
- > where `hitl` IS admitted, `human-confirms` IS live, and the three `hitl-*` terminals exist —
27
- > integrate against what is written here, and expect the class-level rename when the next rung
28
- > lands. Records: PRD Addendum v1.71 and
20
+ > the public surface — a breaking change. Runs on two worker surfaces — the API `Loop` and,
21
+ > for `clipipe-subscription`, the CLI's native tool channel (module 4d). API sections fill in
22
+ > as build-ladder rungs land (PRD §10). What is settled — the boundary, the architecture, the
23
+ > refusals, the constraints — is settled for good. Per LIBRARY_CONVENTIONS §3 this file ships
24
+ > with the package and is the complete adopter contract; the README is only the pitch.
25
+
26
+ > **LEGACY — `hitl` (retired by design, PRD Addendum v1.71, 2026-08-17).** `hitl` remains
27
+ > ADMITTED in code today — `VERDICT_TYPES` still lists it, the `human-confirms` stage kind is
28
+ > still live (never offered, at-most-once, must be last), and the two `hitl-*` terminals
29
+ > (`hitl-pause`, `hitl-decision-red`) still exist and still work exactly as documented
30
+ > wherever this file describes them below. It is retired as a *design direction*, not deleted
31
+ > as *code*: `LOCKED_CLASSES` is empty, so nothing refuses a `hitl` job today, but new
32
+ > integrations should target `green` / `soft-green` plus the review door instead — that pair
33
+ > is the forward path for anything needing a person's judgement, and `hitl`'s removal is a
34
+ > future breaking change, not yet scheduled. Records: PRD Addendum v1.71,
29
35
  > `docs/product/2026-08-17-softgreen-review-door-design.md` (repo-only, not shipped);
30
36
  > evidence F102–F105.
31
37
 
@@ -110,8 +116,8 @@ the step plan at run time (gated by `validatePlan`); the human signs only:
110
116
  | field | shape | notes |
111
117
  |---|---|---|
112
118
  | `goal` | non-empty text | what the agent plans against |
113
- | `verdictType` | `green` \| `soft-green` \| `hitl` | declared radio, never inferred (`VERDICT_TYPES`, frozen). **`green` and `hitl` are ADMITTED** (N4 slice 1) — *and `hitl` is RETIRED as a class by the 2026-08-17 design (see the direction note at the top): it still works exactly as documented here on this branch, and the forward path for a job needing judgement is `soft-green` plus the end-of-run review door*; `soft-green` is still declared-but-locked and reds `request-red` with the type as a structured `verb` field (the tool-menu pattern) until its judged floor exists. 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 |
114
- | `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 |
119
+ | `verdictType` | `green` \| `soft-green` \| `hitl` | declared radio, never inferred (`VERDICT_TYPES`, frozen). **All three are ADMITTED today** — `LOCKED_VERDICTS` is empty, so nothing reds `request-red` on the class itself. `soft-green` has its judged floor (`judged-floor`, signed rubric card + calibration gate) and is the forward path for a job needing judgement, paired with the end-of-run review door; `hitl` is LEGACY — retired as a design direction (see the direction note at the top) but still admitted and still works exactly as documented here. Every `request-red` (e.g. for a locked TOOL verb, not a verdict class) 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 |
120
+ | `close` | **an ORDERED LIST of named stages** `[{ name, cmd, expect, judged?, gapKeep?, offer?, needs? }, ...]` (PRD v1.28), or a close object (table below) | 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 still VALIDATES (`CLOSE_TYPES`) but the plan flow refuses all three at runtime as `close-unsupported` — an object close names no command to run. The modern route to a judged or human-decided close is a staged `closeDecl` (`judged-floor` / `human-confirms` stage kinds), not this object form |
115
121
  | `closeDecl` | **the AUTHORED close** `{ genre: "TYPES", lang: "js"\|"python", stages: [{ name, kind, params }], notes? }` | the ALTERNATIVE to `close`, and the point of the close-authoring rung: the user answers an interview and an LLM composes a DECLARATION over kinds whose implementations we own — never a script, never a shell fragment, never a new kind. **`close` and `closeDecl` are alternatives**: declaring both reds `close-duplicated` (two closes are two arbiters). It is HARD-class by construction (`CLASS_BY_CLOSE.declared`), so a locked verdict on one reds `close-hierarchy` as well as `request-red`. The declaration is validated by `validateCloseDecl`; the TREE-GROUNDED half of that gate (the path rule, and the scoped-job derivation that arms the F84 one-population law) is DEFERRED at spec-validation time — a job spec is validated with no repository in hand — and the runner re-runs it GROUNDED against the real seed before any stage and before any token. It stores the counting RULE and never a number (D12): there is no seed field, and `baseline: "seed"` is measured at each run's own HEAD |
116
122
  | `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) |
117
123
  | `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 |
@@ -203,14 +209,15 @@ redo it. Write the goal from the close's own stage list, in the close's own word
203
209
  a few explicit requirements rather than prose. Preflight already names every stage on the
204
210
  `check-menu` spine event — read it back and check your goal against it before you sign.
205
211
 
206
- **Close types and the hierarchy — the OBJECT form, which survives only for the
207
- declared-but-locked verdict classes** (a close is data, never code; verdict-class laundering
208
- is a named red `close-hierarchy`). The go-forward shape is the staged list above; a
209
- `predicate` object is legal shorthand for a one-stage list and the plan flow adapts it, but
210
- `gold`/`rubric`/`hitl` validate and then refuse at runtime (`close-unsupported`) since v1
211
- admits `verdictType: green` and `hitl` only — and N4's hitl is a STAGE
212
- (`human-confirms` inside a `closeDecl`), never a close TYPE, so the `hitl` OBJECT form below
213
- stays refused at runtime exactly as it was:
212
+ **Close types and the hierarchy — the OBJECT form, which the plan flow only partially
213
+ supports** (a close is data, never code; verdict-class laundering is a named red
214
+ `close-hierarchy`). The go-forward shape is the staged list above; a `predicate` object is
215
+ legal shorthand for a one-stage list and the plan flow adapts it, but `gold`/`rubric`/`hitl`
216
+ validate and then refuse at runtime (`close-unsupported`) regardless of verdict class — the
217
+ plan flow has never executed an object close beyond `predicate`. `hitl` (LEGACY) is a STAGE
218
+ (`human-confirms` inside a `closeDecl`) and `soft-green`'s judged floor is likewise a STAGE
219
+ (`judged-floor` inside a `closeDecl`) — never close TYPES — so both OBJECT forms below stay
220
+ refused at runtime exactly as they were:
214
221
 
215
222
  | type | fields (exact — extras red) | legal class |
216
223
  |---|---|---|
@@ -484,17 +491,19 @@ signed `writeScope` alone, never a free-text fallback. Menus exported: `EXIT_TYP
484
491
  The one layer where a human still wrote code. A close used to be a hand-written script
485
492
  per patient; now the user PICKS A VERDICT CLASS, answers that class's questions, and an LLM
486
493
  composes a DECLARATION over kinds whose implementations bareloop owns. **v1 admits exactly
487
- ONE genre** (`TYPES`: a type checker stops complaining without breaking the tests) and
488
- exactly one verdict class (`green`) — everything else refuses honestly, and every refusal
489
- is COUNTED.
494
+ ONE genre** (`TYPES`: a type checker stops complaining without breaking the tests) —
495
+ everything else refuses honestly, and every refusal is COUNTED.
490
496
 
491
497
  **The verdict class is the USER's answer (PRD v1.57 §1), and it DRIVES the authoring.**
492
498
  `verdictType` is a declared radio the preflight validates, never inferred:
493
- `VERDICT_CLASSES` = `green` | `soft-green` | `hitl`, with `LOCKED_CLASSES` =
494
- `soft-green` alone (N4 slice 1 admitted `hitl`; `soft-green` waits for its judged floor —
495
- **which is now the forward path: `hitl` is retired as a class by the 2026-08-17 design and its
496
- pause becomes the end-of-run review door, unbuilt as of this branch**). Picking a locked one returns a counted
497
- `request-red` refusal at ADMISSION — before that class's questions are ever asked. The
499
+ `VERDICT_CLASSES` = `green` | `soft-green` | `hitl`, with `LOCKED_CLASSES` = **empty** —
500
+ `soft-green` shipped its judged floor in v0.12.0 (2026-08-23) and is fully admitted
501
+ alongside `green`; `hitl` is LEGACY (retired as a class by the 2026-08-17 design, PRD
502
+ Addendum v1.71) but still admitted too, since removal is a future breaking change. The
503
+ review door (`accept`/`rerun`/`pause`) that re-homes hitl's pause machinery is shipped and
504
+ live — always on for `soft-green`, opt-in for `green` via `reviewDoor: true` /
505
+ `--review-door`. A future LOCKED_CLASSES entry would still return a counted `request-red`
506
+ refusal at ADMISSION — before that class's questions are ever asked. The
498
507
  pick is also a PROMISE: every catalogue kind carries the `verdictClass` it can honestly
499
508
  render, `closeCeiling(declaration)` reports the highest class a declaration demands, and a
500
509
  declaration ABOVE the pick is a `class-ceiling` red naming the kind that raised it (inert
@@ -505,7 +514,7 @@ a step, or test it without a provider.
505
514
 
506
515
  | step | call | what it is |
507
516
  |---|---|---|
508
- | interview | `runInterview({ verdictType, answers, repoPath })` → `{ ok, answers, verdictType, refusal, reds }` | PURE — no model, no repo, no clock. THREE frozen question sets keyed by verdict class (`QUESTION_SETS`; `questionsFor(cls)` / `requiredAnswersFor(cls)`), and the interview asks NOTHING about a genre and NOTHING about the repository. **Ask the library for the set and its numbers — never hardcode a count or a slot number**: the green set has lost two slots since it was frozen (D13's genre confirm, then the repo question, dropped 2026-08-15 because `repoPath` is mandatory structured input and asking for it again invites a second, drifting answer for a fact the machine already holds), and it renumbers CONTIGUOUSLY from 1 each time rather than leaving a gap. Today the green set is five questions, answers keyed by number; the HITL set is those five byte for byte plus one more (what the signer is deciding when they look at the result — `human-confirms`' `ask`), which is the spread's next number and moves with a green-side deletion; the soft-green set is named but LOCKED (`questions: null`, absent rather than empty) and its selection refuses before it runs. `verdictType` and `repoPath` are STRUCTURED input, never parsed out of prose. Answers are scrubbed at INGEST |
517
+ | interview | `runInterview({ verdictType, answers, repoPath })` → `{ ok, answers, verdictType, refusal, reds }` | PURE — no model, no repo, no clock. THREE frozen question sets keyed by verdict class (`QUESTION_SETS`; `questionsFor(cls)` / `requiredAnswersFor(cls)`), all three LIVE as of softgreen module 3 — nothing is locked today (`LOCKED_CLASSES` is empty) — and the interview asks NOTHING about a genre and NOTHING about the repository. **Ask the library for the set and its numbers — never hardcode a count or a slot number**: the green set has lost two slots since it was frozen (D13's genre confirm, then the repo question, dropped 2026-08-15 because `repoPath` is mandatory structured input and asking for it again invites a second, drifting answer for a fact the machine already holds), and it renumbers CONTIGUOUSLY from 1 each time rather than leaving a gap. Today the green set is five questions, answers keyed by number; the LEGACY `hitl` set is those five byte for byte plus one more (what the signer is deciding when they look at the result — `human-confirms`' `ask`), which is the spread's next number and moves with a green-side deletion; the `soft-green` set is likewise those five plus two more — Q6 the signed rubric card (what separates a pass from a fail) and Q7 the frozen calibration set (one example to pass, one to fail) — the judged floor's two required inputs. `verdictType` and `repoPath` are STRUCTURED input, never parsed out of prose. Answers are scrubbed at INGEST |
509
518
  | survey | `runAuthorScout({ workdir, provider, attempts?, ceilingUsd? })` → `{ state, facts, reason, meta, raw, raws, calls, budgetStop }` | a bounded READ-ONLY LLM survey. Read-only by MENU CONSTRUCTION (`AUTHOR_SCOUT_VERBS` = the full menu minus write-class and store-class verbs), 8 rounds, F59's reserved toolless final round. **`state: 'ABSENT'` means the scout did not complete — never "no special facts are needed"**, and a parsed `{}` is one of its five ABSENT routes. Up to `SCOUT_ATTEMPTS` (**3**, hardcoded — PRD v1.58) attempts: `attempts` is TIGHTEN-ONLY and CLAMPS rather than throws (floor **1**, because a scout that never ran is an ABSENT nobody can act on), the direction every cap in this system runs in. **A retry fires on the typed MALFORMED class alone** (`SCOUT_RETRY_CAUSES` = `empty` \| `unparseable`); what is excluded is excluded for a reason — `call-failed` covers transport and `truncated:max_tokens`, both provider-red with NO redraft; `short` is F59's cut-off population and already has its own instrument INSIDE the attempt; `non-object` and `empty-object` are valid JSON with wrong or vacuous content, which is a SEMANTIC failure, and F38/F39 measured what re-asking one buys (the same distribution, sampled twice) — that is the self-healing line, and it is not crossed here. The re-ask is TOOLLESS over the conversation the survey already produced (the repository was already read; only the emission was unreadable) and names the mechanical parse error and nothing else. **There is no JSON repair behind it and there never will be** — a repairer decides what the model MEANT and writes it down as though the model had said it, in the one artefact whose whole job is to be honest about what a repository contains. `meta.attempts` / `meta.attemptsAllowed` record what was paid for; every attempt is metered under its own label (`author-scout`, `author-scout#2`, …) and leaves its raw behind. **`ceilingUsd` is the operator's money ceiling** (PRD v1.62) — checked BETWEEN attempts and before F59's reserved round, so no paid call escapes it; `null`/absent is UNBOUNDED, and a non-null non-finite value THROWS (the shared `capStop` seam — see `authorClose` below). When it ends the ladder, `budgetStop` names which stop (`cap-halt` \| `pricing-red`) and a survey that was never asked for carries the typed cause `not-funded` — deliberately OUTSIDE `SCOUT_RETRY_CAUSES`, because a retry is precisely what the cap forbade |
510
519
  | listing | `buildSeedListing({ workdir, seedRef, sourcePaths, testPaths })` | mechanical, `$0`, no model. `files` is the WHOLE tree (what the validator judges paths against); `block` is scoped to the survey's own paths and capped in ANNOUNCED tiers (what the prompt carries). Handing the validator the scoped half would make a job scoped to `src/` read as whole-tree and silently disarm the one-population law |
511
520
  | authoring | `authorClose({ workdir, seedRef, lang, verdictType, answers, scout, listing, generate, ceilingUsd? })` | the grounded loop: author → validate → run EVERY stage at the seed → feed the MEASURED results back → revise, bounded at `MAX_REVISIONS` (2) — a passed `maxRevisions` is TIGHTEN-ONLY, so a caller may LOWER that ceiling and never buy more revise rounds than the constant (floor **0**: one authoring call with no revise round is a legal ask, and unlimited revising would launder thrash as adaptation) — early-stop on an unchanged declaration. The declaration is emitted through a SCHEMA-FORCED TOOL CALL (`declare_close`), never parsed out of prose; the feedback is EXECUTION OUTPUT only — no model ever reviews another model's close. The return carries **`raws`**: what every model call actually SAID, the scout's attempts absorbed together with the declaration's, in the cost book's own order and under the same labels it meters. Each is `{ label, attempt, bytes, trimmed, text, cause, reason }` through the ONE persist helper (`scrubRaw` — redacted over the ONE `SECRET_PATTERNS` inventory, since a raw is the record most likely in this system to carry a live credential; bounded at `RAW_PERSIST_MAX` (8000) with the trim ANNOUNCING its own full size, and the cut walked back off a continuation byte so a multi-byte character is never split). `raws` is present on EVERY path **including the `$0` preflight refusals** — the path that spends nothing more is exactly the one whose evidence used to vanish with the process. `iterations` records what each turn MEANT (declaration, validation, seed read); `raws` records what it SAID, and a malformation is only ever visible in the second. **`ceilingUsd` is the same operator ceiling the survey took** — the scout's spend is ABSORBED into this loop's cost book before its first call is weighed, so spend already incurred folds in and re-entering cannot silently widen it. It is checked immediately before EVERY paid call (the author call, each revise, and each malformed-emission retry). **A MALFORMED ceiling is an ERROR, not a silent unbounded run** (v1.64 §3): the seam every ceiling read goes through (`capStop`, `src/text.js`) THROWS on a non-null non-finite value — `'2.50'`, `NaN`, `Infinity`, `true`, `{}` — before the first paid call, and the throw propagates uncaught rather than being caught into an ABSENT survey, because a caught one would hand you the very silent-unbounded run it exists to prevent. `null`/omitted is the STATED operator choice for unbounded; `0` and negatives are finite, legal, and cap-halt immediately. The advertised ceiling and the enforced ceiling can therefore never be different numbers. A `cap-halt` here can arrive with `ok:true` — a close that was already validated and measured survives the money stop, exactly as a late provider casualty does |
@@ -1640,13 +1649,17 @@ in the ledger whatever the person then does with it.
1640
1649
  write}` — the terminal registry write for a runner that drives `runJob` directly** (rather
1641
1650
  than through `runReuse`, which writes its own rows). It is what gives that runner's review
1642
1651
  door a row to release: without one, `accept` reaches `recordDoor` and reds `no-row-for-run`.
1643
- STORAGE ONLY — nothing here selects, promotes or reuses a bridge. Four refusals, each with a
1644
- named `reason`: no `registryDir` (`no-registry` — the registry is operator-supplied, never
1652
+ STORAGE ONLY — nothing here selects, promotes or reuses a bridge. Three refusals, each with
1653
+ a named `reason`: no `registryDir` (`no-registry` — the registry is operator-supplied, never
1645
1654
  conjured), `outcome !== 'green'` (`green-predates-run` for already-green — accept confirms a
1646
- verdict, it never mints one — else `not-green`), no executed `plan` (`no-plan-executed`, R1),
1647
- and a verdict class whose credit is not held (`credit-not-held` — `green`-class runs mint
1648
- nothing here, `quarantinesCredit` is the one predicate). `name` falls back to `job.job`; the
1649
- row is born HELD because the record carries the spec's own `verdictType`, never a flag. A
1655
+ verdict, it never mints one — else `not-green`), and no executed `plan` (`no-plan-executed`,
1656
+ R1). Every green class mints (hamr's ruling 2026-08-30): a `soft-green` row is born HELD —
1657
+ its learning credit is quarantined until a signer accepts at the door — and a `green`-class
1658
+ row is born RELEASED, because a machine's own close proved it and there was never a credit
1659
+ to quarantine; `quarantinesCredit(verdictType)` in `greenParts` is the one predicate, read
1660
+ off the record's own `verdictType`, never a flag. `door-accept` on a released row records
1661
+ the disposition and releases nothing — not an error; a run that earned no row at all still
1662
+ refuses `no-row-for-run`. `name` falls back to `job.job`. A
1650
1663
  refused underlying write returns `{minted: false, reason: 'write-refused', write}` with the
1651
1664
  reds on `write` — never a throw. Minting goes through the same cold-leg green write `runReuse`
1652
1665
  uses (shape-fork rule and collision refusals included), so the two runners can never spell
@@ -2142,6 +2155,318 @@ is not a spend record and no spend-slicing instrument needs to account for it.
2142
2155
  (`no record` if the run ended before the summary could fire); `scripts/behaviour-readout.mjs`
2143
2156
  prints the same line when handed the run's spine file as its optional third positional.
2144
2157
 
2158
+
2159
+ ### `replayRun(spineEvents, auditEvents?, { runId? })` / `formatReplay(summary)` — `src/replay.js`
2160
+
2161
+ PRD build-list item 6: today no failed run can be reconstructed in under five minutes — every
2162
+ byte is on disk (the spine JSONL + its gate-audit sidecar) but only hand-slicing JSONL reads it.
2163
+ `replayRun` is a report-only reader over BOTH files: **reads only, mints no verdict, writes
2164
+ NOTHING to the spine**, same posture as `runBehaviour` above, which it reuses rather than
2165
+ reimplements for the tool-call breakdown. It returns one plain object:
2166
+
2167
+ ```
2168
+ { runId, job, goal, budgetUsd, specHash, branch, verdictType, model, code,
2169
+ outcome, stopReason, spentUsd, spendComplete, wallMs, chainClock,
2170
+ resumed, resumeSeed, thisFileSpend, spendMismatch,
2171
+ timelineKind: 'steps'|'iterations', replans, close,
2172
+ steps: [{ id, occurrence, outcome, rounds, toolCalls, checks: {passed, failed},
2173
+ treeChanged, wallMs, spentUsd, unpricedRounds, tripped }],
2174
+ iterations: [{ iteration, verdict, boundary, closeStage, rounds, toolCalls,
2175
+ wallMs, spentUsd, unpricedRounds, tripped }],
2176
+ ending: { record, before /* the 3 records before it */, escalation },
2177
+ lastEscalation, behaviour, memoryCache, skipped }
2178
+ ```
2179
+
2180
+ **`verdictType`/`model`** (F117, PRD TODO #20 — hamr blessed the whole item verbatim, landed
2181
+ 2026-08-25): `verdictType` reads `job-start.verdictType` (`src/run.js:303`) — a REQUIRED spec
2182
+ field (`src/job.js:552` reds `missing-required` before job-start ever fires), so `null` here
2183
+ means only "this spine predates the field" (measured: `u-msdsmkid`, an Aug-3 archived run),
2184
+ never "unset". `model` reads `job-start.model`, present only when the shell-owned provider
2185
+ binding itself carried a `.model` string at job-start time — `src/planrun.js:76`'s own comment
2186
+ documents bare-agent's Loop reading `baseProvider.model` directly off that same object, so it is
2187
+ a real, citable, synchronously-available property, not a guess; a native/clipipe call path (or
2188
+ any provider double with no such field — measured on `tests/helpers.js`'s `scriptedProvider`)
2189
+ legitimately has neither, and this reader reports that honestly rather than defaulting one in.
2190
+ `formatReplay` prints both right after `shape:` — `class: softgreen` / `class: not recorded
2191
+ (pre-F117 spine)`, `model: claude-sonnet-5` / `model: not recorded`
2192
+ (the two "not recorded" wordings are deliberately different: a missing `verdictType` is always
2193
+ an archive-age gap, a missing `model` can happen on any spine, old or new). `--all` gains a
2194
+ `class` column right after `shape` and a `model` column right after `class` (the verdict class /
2195
+ the model string, or `-`) — `model` is kept FULL, never truncated, unlike the compact `reason`
2196
+ column: a wrong or uncertain model reading is exactly the thing a directory-wide scan needs in
2197
+ full, not cut.
2198
+
2199
+ **`code`** (F118, the run→code direction — the commit→run direction is the prompt-commit
2200
+ check's `Failure:` run-reference rule; the two are companion halves of one loop, built the
2201
+ same day). `codeVersion()` (`src/codeversion.js`) is a pure, $0, no-shell reader: `version`
2202
+ is bareloop's own `package.json` version (resolved off `import.meta.url`, never a hardcoded
2203
+ path — works whether this module is imported from `src/` or from an installed
2204
+ `node_modules/bareloop`); `sha` is read straight off `.git/HEAD` (following a symbolic ref
2205
+ to its loose ref file, falling back to `packed-refs` when the branch has been packed) **only
2206
+ when `.git` is a real directory at the package root** — a dev checkout has one, an
2207
+ npm-installed copy does not, and that absence reads as `sha: null`, honestly, never a guess.
2208
+ `dirty` is always `null`: whether the tree has uncommitted changes cannot be known without a
2209
+ shell, and this module NEVER shells out — `run` is the one locked verb (hamr's law targets
2210
+ what the agent can DO, not how it's written), and a report-only field is not a license for
2211
+ the library to grow a shell seam. `null` is reported here rather than a defaulted `false`,
2212
+ because a faked "clean" reading is a worse lie than an honest "don't know."
2213
+
2214
+ `job-start` (`src/run.js:303`) carries `code: {version, sha}` alongside F117's
2215
+ `verdictType`/`model` (`dirty` is intentionally omitted from the spine — it is always
2216
+ `null`, and a spine field that can only ever hold one value is not worth a byte).
2217
+ `replayRun` reads it as `{version, sha}` or `null` (absent field, an archive older than this
2218
+ landing); `formatReplay` prints a `code:` line right after `model:` — `code: bareloop
2219
+ 0.14.0 @ 43f2812` on a spine with a real sha, `code: bareloop 0.14.0 @ sha unknown` when
2220
+ `sha` is null (an npm-installed run), `code: not recorded (pre-F118 spine)` when the
2221
+ whole field is absent. `--all` gains no new column for this — the row is already at hamr's
2222
+ named limit.
2223
+
2224
+ **Unknown money/time is always `null`/`unknown` — never `0`, and a partial sum is never stamped
2225
+ exact** (F6's rule, carried through every level this module reports at): `spentUsd`/`wallMs` are
2226
+ `null` when the run's own `job-end` is absent or carries no figure; `spendComplete` reads `false`
2227
+ whenever `spentUsd` is `null`, regardless of what the record's own flag says. The SAME rule
2228
+ governs one occurrence/iteration's own `spentUsd`: it sums `worker-round.costUsd`
2229
+ (`src/planrun.js:2288-2297`) over records strictly inside that occurrence's own `seq` window —
2230
+ a single `null` `costUsd` anywhere in the window makes the WHOLE occurrence's `spentUsd` `null`
2231
+ (`unpricedRounds` names how many), never a partial sum. Zero rounds in the window is a real `$0`,
2232
+ not an unknown one.
2233
+
2234
+ `stopReason` is `null` on a green/already-green outcome; otherwise `category — decision —
2235
+ detail` from the last `escalation` record, every part present that record actually carries
2236
+ (`category` dropped when it duplicates the outcome already shown beside it; `detail` trimmed at
2237
+ 400 chars with an explicit `…[+N chars]` marker, never silent) — `decision` alone is the generic
2238
+ one-line prose every category shares ("the provider path failed mid-run…" fires for every
2239
+ transport casualty); **`detail` is what actually killed THIS run** (a TLS "bad record mac", a
2240
+ specific mypy error) and is the reason a stop is explained, never the decision prose alone.
2241
+ Falls back to the bare outcome string for a pre-escalation red (`plan-red`/`job-red`/
2242
+ `smoke-red`). `ending.escalation` carries that same last escalation record whenever it falls
2243
+ outside the fixed 3-before window (common on a run that keeps emitting bookkeeping records after
2244
+ its real failure) — never silently dropped. `lastEscalation` is the same raw record exposed
2245
+ unconditionally (not gated on the window check) — `summarizeForAllLine`'s `reason` column reads
2246
+ it directly rather than re-deriving from `ending`.
2247
+
2248
+ **A step id can recur** (a replan re-executes the same step): each `step-start`..`step-end` pair
2249
+ is its own **occurrence** (`occurrence: 1, 2, …`), and every field on it — `rounds`, `toolCalls`,
2250
+ `checks`, `treeChanged`, `wallMs`, `spentUsd` — is scoped to records strictly between THAT pair's
2251
+ own `seq` (the spine's monotonic per-record counter, never `ts` — every record in one spine
2252
+ shares the same seq counter, so it is an exact ordering where two same-millisecond `ts` stamps
2253
+ are not), never pooled across every occurrence sharing the id. `rounds` counts real
2254
+ `worker-round` records (`phase === 'step:<id>'`), never the declared cap; `toolCalls` is the one
2255
+ thing still windowed by `ts` (the gate-audit carries no `step` or shared `seq`). `tripped` is the
2256
+ last `escalation` whose `seq` falls inside the occurrence's window — `{category, detail}`, with
2257
+ `detail` falling back to `decision` when the escalation site carries no `detail` at all (a
2258
+ ladder-exhaustion `cap-halt`, `src/ralph.js`, is the measured example — without the fallback the
2259
+ tripped line would print a bare category and hide the real reason).
2260
+
2261
+ **A run whose spine carries no `step-start` at all** — the plan's only step was
2262
+ already-green-skipped on a resumed leg, or a genuinely loop-only shape (measured: `u-msf70nei`,
2263
+ a resumed leg whose one step was already proven and only the CLOSE-FIX loop ran) — has
2264
+ `timelineKind: 'iterations'` and `iterations[]` populated from `iteration-start`
2265
+ (`src/ralph.js:583`) instead: each row runs from one `iteration-start` to the next
2266
+ `close-verdict`/`run-end`/`escalation` (`src/ralph.js:674,577/657/676/698`), whichever comes
2267
+ first by `seq`. `steps`/`iterations` are never both populated — exactly one is non-empty. Each
2268
+ iteration's own `closeStage` (`{verdict, stages}`) is set when a `close-verdict` ended it, distinct
2269
+ from the run's FINAL `close` (below).
2270
+
2271
+ **`close`** is the run's real, final close — resolved by picking the LAST (by `seq`) record
2272
+ among every `close-verdict` that carries an ARRAY `stages` field and every `outer-close`
2273
+ (`src/planrun.js:3296`). A `close-verdict` with NO `stages` came from a PLAN STEP's own
2274
+ micro-loop, judged by the exit evaluator instead of a command (`src/planrun.js:2907`) — never
2275
+ "the close". Picking by `seq` rather than preferring `outer-close`'s TYPE matters: `outer-close`
2276
+ is the PRECHECK against the real close, fired once before the fix loop even starts, so when it
2277
+ reads red the fix loop's own staged close-verdicts run strictly AFTER it — measured on
2278
+ `u-msf70nei`, whose `outer-close` (seq 14) reads `needs_revision` but whose fix loop's own final
2279
+ close-verdict (seq 97, LATER) reads `satisfied`, which is what the run actually ended on
2280
+ (`job-end.outcome:'green'`). A run that never reached ANY close (e.g. `u-msdsmkid`, stopped by a
2281
+ step-variance meter before `judgeClose()` ever fired) resolves `close: null`, printed as `none —
2282
+ the run ended before any close ran` — never omitted.
2283
+
2284
+ `replans` prefers `plan-executed.replans` (`src/planrun.js:2688`) and falls back to counting
2285
+ `materials` records stamped `phase:'replan'` (`src/planrun.js:2520`) when that field is absent —
2286
+ an older archived spine can predate the field (measured on `u-msdsmkid`, an Aug-3 run). `branch`
2287
+ reads the last `work-branch.branch` (`src/planrun.js:1589`) or `null` when absent — also measured
2288
+ missing on the same era of archive, before the work-branch rung landed.
2289
+
2290
+ `wallMs` is THIS FILE's own `job-start`→`job-end` span, labeled `(this file)` — on a RESUMED leg
2291
+ that is only the latest engagement, not the whole signed chain. `chainClock` is the other true
2292
+ number: the SIGNED wall read off the run's own clock record — `wall-halt` when present (the
2293
+ TERMINAL reading, taken at the moment the wall stopped the run; `src/planrun.js:1269`'s
2294
+ `emitWallHalt`), else the single `wall-clock` record every run emits near its start
2295
+ (`src/planrun.js:1203`) — both spread from `clock.report()` (`src/clock.js:235-247`: `bounded,
2296
+ requestedMs, closeStages, enforcedMs, elapsedMs, remainingMs`). `elapsedMs` on either is
2297
+ chain-scoped by construction (`createClock`'s `priorElapsedMs` fold, `src/clock.js:174`), so it
2298
+ can legitimately exceed this file's own `wallMs` on a resumed run. `wall-halt`'s reading is
2299
+ end-of-run-honest, so `formatReplay` states it plainly: `clock: 60m04s of 60m00s signed (chain,
2300
+ from wall-halt)`. `wall-clock`'s reading is taken at the TOP of the engagement (chain time
2301
+ already inherited, not this run's own duration) — printing that under the same template read as
2302
+ "this run took 0 seconds" on a cold run, so it gets its own honest wording instead: `clock: 30m00s
2303
+ signed · 0.0s inherited from prior legs at start (from wall-clock; no end-of-run reading in this
2304
+ file)`.
2305
+
2306
+ **Money carries the identical split** (hamr, 2026-08-25): a RESUMED run's `job-end.spentUsd` is
2307
+ the CHAIN total (prior legs folded in — `src/run.js:227`'s `chainFoldUsd`, seeded from
2308
+ `job-start.priorSpentUsd`), which can never equal this ONE file's own rounds — measured on
2309
+ `u-msf70nei`: header `spentUsd` reads `$5.3389` while its own 55 `worker-round`s sum to exactly
2310
+ `$1.2127`. `resumed` reads `job-start.priorSpentUsd > 0` — the SAME condition `src/run.js` itself
2311
+ folds on — deliberately NOT `resume-seed`'s presence: `resume-seed` (`src/planrun.js:2622`) is a
2312
+ NARROWER record, fired only when a plan is actually reloaded mid-flight, and its absence does not
2313
+ mean "not resumed" — measured directly on 4 real archived runs (`litectx-maintainer-bareloop`'s
2314
+ `msx7xoe0`/`msx87qqs`/`msxf9129`/`msxf2mwi`) that carry a real `priorSpentUsd` fold with NO
2315
+ resume-seed record at all. `resumeSeed` (when present) supplies the DETAIL —
2316
+ `{phase, completed, skipping, divergence}` — printed as `resumed: yes — phase close, 1 of 1
2317
+ completed step(s) skipped`; a resumed leg with no resume-seed record still reads `resumed: yes
2318
+ (no resume-seed detail recorded)`, honestly, rather than falling back to `no`. `thisFileSpend`
2319
+ prefers `job-end.engagementSpentUsd` (F103, `src/run.js:284` — this engagement's own fold-out
2320
+ spend) when the record carries it, falling back to summing this file's own `worker-round.costUsd`
2321
+ when it does not (measured absent on `u-msf70nei` itself); an unpriced round anywhere in THIS
2322
+ FILE makes the figure `null` regardless of source (F6, never a partial sum stamped exact) — both
2323
+ figures print on the `spent:` line, each labeled with its source, never one silently standing in
2324
+ for the other. `spendMismatch` is the same-class check for a NON-resumed run: there is no fold to
2325
+ explain a gap, so `job-end.spentUsd` disagreeing with this file's own summed rounds by more than
2326
+ $0.0001 is a genuine finding, printed as `MISMATCH vs job-end`, never hidden — measured across
2327
+ every non-resumed run in the available archive (aurora-u, bareagent-u, litectx-maintainer): zero
2328
+ trip it. `--all`'s spend column stays the chain total (what a directory scan asks first) but
2329
+ marks a resumed row with a trailing `*` on its id, with one footnote line
2330
+ (`* resumed run — spend is the chain total, see detail`) printed once, only when the listing
2331
+ holds at least one such row.
2332
+
2333
+ `formatReplay` renders the full page: a header block (`goal:`/`shape:`/`signed:`/`branch:`/
2334
+ `resumed:`/`outcome:`/`spent:`, every absent field printing the literal `none recorded` — never
2335
+ blank, never a fabricated `0`), a `TIMELINE (steps)` or `TIMELINE (iterations — loop-shape run)` section with
2336
+ its own column-header row (`# step result rounds tools checks tree wall spend`, or the
2337
+ iteration equivalent without `checks`/`tree`) and a `↳ tripped:`/`↳ close:` line under any row an
2338
+ escalation or close fell inside, a `CLOSE` section (never omitted — `none — the run ended before
2339
+ any close ran` when nothing ever closed; the failing stage, if any, prints FIRST with `✗`, the
2340
+ rest `name OK`), `ENDING` (last record + 3 before it, plus an out-of-window escalation), the
2341
+ `BEHAVIOUR` block, and a `MEMORY-CACHE` line **always printed** — the existing wording when the
2342
+ run carried a record, `not armed on this run` otherwise. An absent record is never silent.
2343
+ `RESULT` words are `GREEN`/`RED`/`UNKNOWN` — `satisfied`/`green`/`already-green` read GREEN,
2344
+ every other non-null value reads RED.
2345
+
2346
+ `summarizeForAllLine(summary)` builds one `--all` row's raw fields (`id, job, shape, outcome,
2347
+ spend, wall, steps, reason`) — `shape` is `plan`/`loop` (from `timelineKind`, never the
2348
+ always-literal-`'plan'` `job-start.shape` field); `steps` is a bare count for a plan-shape run,
2349
+ `"N it"` for a loop-shape one; `reason` is `category — <first clause of detail, cut at 60 chars,
2350
+ plain "…", no [+N] marker>` off `lastEscalation` — **`detail` only, never `decision`** for this
2351
+ column specifically (unlike the full report's `↳ tripped:` line, which does fall back to
2352
+ `decision` — this compact row has no room for both and the coordinator's own ruling keeps
2353
+ `decision`'s generic per-category prose out of it); `-` on a green row; the distinct, literal
2354
+ `no job-end (killed mid-run)` when the spine never reached a `job-end` at all (`outcome` itself
2355
+ is `null`) — not folded into the ordinary escalation case. `formatAllLines(entries)` renders every
2356
+ row as a FIXED-COLUMN, ALIGNED table with a header row (never CSV — columns are padded, not
2357
+ delimited; the header's own label lengths count toward the column widths) plus any
2358
+ `<name> not-a-spine` lines at their original position (unpadded — a filename carries no
2359
+ outcome/spend/wall of its own). No header prints when there are zero real spines in the listing.
2360
+
2361
+ `scripts/run-replay.mjs <spine.jsonl>` resolves the sibling `-gate-audit.jsonl` automatically and
2362
+ prints the full report; `--all <dir>` is **name-agnostic** — the patient corpus does not agree on
2363
+ one filename convention (`u-<id>.jsonl`, `battery-A1-<id>.jsonl`, `l2accept-L1-<id>.jsonl`,
2364
+ `reuse-<id>.jsonl`, `types-screen-C-<id>.jsonl`, …), so a spine is identified by CONTENT: any
2365
+ `.jsonl` that is not a sidecar by name (`*-gate-audit.jsonl`, `*.lag.jsonl`) and whose records
2366
+ include a `job-start` (present on every real spine checked, nested wrappers included) or
2367
+ `run-start` (present on most, absent on a run that died before ralph's first iteration). A
2368
+ `.jsonl` matching neither is printed as `<name> not-a-spine` — never silently dropped or
2369
+ misread as an empty run. Both modes mirror `scripts/behaviour-readout.mjs`'s own malformed-line
2370
+ handling (skip-and-count, never throw).
2371
+
2372
+ **F117's PRD TODO #20 is now landed in full**: both the verdict CLASS and the worker MODEL
2373
+ name are surfaced (see `verdictType`/`model` above) — hamr blessed the item verbatim
2374
+ ("1") on 2026-08-25, so the spine-WRITER change to `job-start` (`src/run.js`) that a
2375
+ report-only reader would not otherwise make on its own authority went in with his explicit
2376
+ word. `judgedCount` on a close-verdict was never a reliable stand-in for the class either way
2377
+ (it appears on plain green closes too, not only softgreen ones) — this reads the real
2378
+ declared field instead.
2379
+
2380
+ **Transport retries and floor reasons (F119, live-proven 2026-08-25)**: every
2381
+ `transport-retry` record (`src/planrun.js:126`) whose `seq` falls inside a step's or
2382
+ iteration's own window prints a `↳ transport retry ×N (recovered|not recovered|partially
2383
+ recovered) — <first 80 chars of the error>` line under that row — the same open-interval
2384
+ `seq` windowing `windowSpend` already uses for rounds. A retry whose `seq` falls outside
2385
+ every occurrence window (e.g. during the scout/plan phase) is counted instead under the
2386
+ `TIMELINE` header as `transport retries outside steps: N`, printed only when that count is
2387
+ non-zero. The header's `spent:` line, whenever `spendComplete` is `false`, appends
2388
+ `· floor because: <reason(s)>` — derived from the spine, never guessed: `transport retry
2389
+ ×N` (any `transport-retry` record), `unpriced round(s)` (a `worker-round`/`judge-round`
2390
+ with non-finite `costUsd`), `cut mid-call` (a `wall-halt` record with `cutMidCall:true`),
2391
+ `stall` (a `stall` record), `prior leg floor` (`job-start.priorSpendComplete === false`) —
2392
+ every cause that actually shows evidence prints, never just one; a floor with no derivable
2393
+ evidence on this spine (an older archive, or a resumed leg's floor inherited from a file not
2394
+ in hand) prints `· floor (reason not in spine)` instead. `--all` adds no new column: a run
2395
+ that carried any transport retry gets ` ⟲N` appended directly after its outcome word (same
2396
+ posture as the resumed `*` on `id`), with one footnote
2397
+ (`⟲N transport retry(ies), recovered inline — see the full replay for detail`) printed once
2398
+ when the listing holds at least one such row. Live-proven on `u-mt8yk53k` (F119): two
2399
+ recovered TLS `bad record mac` retries, both inside step `fix-mypy-strict`'s window —
2400
+ `floor because: transport retry ×2` and `green ⟲2` in `--all`, on a run that finished green.
2401
+
2402
+ ```js
2403
+ import { replayRun, formatReplay } from 'bareloop';
2404
+ const spine = fs.readFileSync(spineFile, 'utf8').trim().split('\n').map(JSON.parse);
2405
+ const audit = fs.readFileSync(auditFile, 'utf8').trim().split('\n').map(JSON.parse);
2406
+ console.log(formatReplay(replayRun(spine, audit, { runId: 'mszcthk1' })));
2407
+ ```
2408
+
2409
+ ### `PROMPT_REGISTERS` / `isPromptFile(path)` — `src/promptregisters.js`, and the prompt-commit rule
2410
+
2411
+ PRD build-list item 5 (TODO #8), Q9 answered (hamr, 2026-08-25): "a check". A commit that
2412
+ changes a **prompt register** — one of the model-facing system/strategy/instruction
2413
+ strings a worker or judge actually reads (`SCOUT_SYSTEM`, `AUTHOR_SYSTEM`,
2414
+ `PERSONA_TOOLS`, `READ_SHIM_STRATEGY`, and their neighbors — the full, verified list is
2415
+ `PROMPT_REGISTERS` in `src/promptregisters.js`) — must say, in its own message, three
2416
+ things: what failure caused the change, what it addresses, and what it corrects.
2417
+ Convention-only was rejected: doctrine already on record is "a frozen rule without a
2418
+ wired detector is prose." `isPromptFile(path)` is the pure predicate the check runs
2419
+ against; both are exported from `bareloop`'s root so the inventory has exactly one home.
2420
+
2421
+ hamr's addition (2026-08-25, same day): the `Failure:` line must ALSO cite the **run**
2422
+ that caused the change — `run <id>` or `run u-<id>` (case-insensitive at the `run`
2423
+ keyword, multiple refs fine), matched by `\brun\s+(u-)?[a-z0-9]{8}\b` (tightened from an
2424
+ initial `{6,12}` margin after a PR #23 review found the margin loose enough to accept
2425
+ plain prose — "run failed" is 6 lowercase letters, satisfying `{6,12}` outright — to the
2426
+ measured exact shape: every real archived id is exactly 8 characters. A per-id digit
2427
+ requirement was proposed in that same review and rejected: 23 of 130 re-sampled real ids
2428
+ carry no digit at all, `msdsmkid` among them — a real archived run cited by name
2429
+ elsewhere in this repo — so requiring a digit would reject genuine citations). The commit
2430
+ points at the run; `replayRun`/`formatReplay` (above) close the loop the other way,
2431
+ turning that run id back into the whole story. A `Failure:` line with no run reference
2432
+ fails with a distinct message: `Failure: must cite the run that caused the change (e.g.
2433
+ "Failure: run mszcthk1 — ...")`. FORMAT ONLY — the check never verifies the cited run's
2434
+ spine file actually exists (a run's patient lives outside this repo entirely, so there is
2435
+ nothing here to check it against).
2436
+
2437
+ Enforcement is **local only** — wired into `npm test` (see `package.json`'s `test`
2438
+ script), never into `.github/workflows/*` (CI already runs `npm test`, so the rule is
2439
+ enforced there without an ask-first CI edit). The check itself lives outside the
2440
+ published package, at `scripts/prompt-commit-check.mjs` (pure decision logic in
2441
+ `scripts/promptcommitlib.mjs`, `scripts/` is not in `package.json`'s `files`, so none of
2442
+ this ships):
2443
+
2444
+ ```
2445
+ node scripts/prompt-commit-check.mjs --range origin/main..HEAD # validates every commit
2446
+ # in the range that
2447
+ # touches a prompt file
2448
+ node scripts/prompt-commit-check.mjs --message-file .git/COMMIT_EDITMSG
2449
+ # commit-msg hook mode:
2450
+ # validates one message
2451
+ # against staged files
2452
+ ```
2453
+
2454
+ A compliant message:
2455
+
2456
+ ```
2457
+ fix: tighten PERSONA_TOOLS
2458
+
2459
+ Failure: run mszcthk1 — worker read the arbiter spine after being told it was denied
2460
+ Addresses: PERSONA_TOOLS did not name the spine file explicitly
2461
+ Corrects: spells the denied paths from ARBITER_BOOK_STORES
2462
+ ```
2463
+
2464
+ A commit that touches no prompt-register file is passed without inspecting its message.
2465
+ An unresolvable `--range` (no such ref — a fresh clone with no `origin`, a detached
2466
+ checkout that never fetched) is a **skip**, printed and exited 0: a missing baseline is
2467
+ not a rule violation, never a red.
2468
+
2469
+
2145
2470
  ## Architecture
2146
2471
 
2147
2472
  Three layers. An **outer shell** (dumb, permanent): per-run budget cap via bareguard,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bareloop",
3
- "version": "0.14.0",
3
+ "version": "0.16.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",
@@ -17,7 +17,7 @@
17
17
  "node": ">=20"
18
18
  },
19
19
  "scripts": {
20
- "test": "node --test",
20
+ "test": "node --test && node scripts/prompt-commit-check.mjs --range origin/main..HEAD",
21
21
  "typecheck": "tsc --noEmit",
22
22
  "build:types": "tsc",
23
23
  "prepublishOnly": "npm run build:types"
package/src/authorjob.js CHANGED
@@ -290,8 +290,8 @@ export function runInterview({ answers, verdictType = null, repoPath = null, que
290
290
  verb: 'non-green-verdict',
291
291
  path: 'repoPath',
292
292
  detail: 'This job has no code repository, so there is no seed to measure against and no changed set to read — '
293
- + 'nothing deterministic can decide whether it came back done. That needs a judged or a human close, and '
294
- + 'both are declared-but-locked in v1.',
293
+ + 'nothing deterministic can decide whether it came back done. That needs a judged (soft-green) or a '
294
+ + 'human close; this authoring flow drafts code-genre closes against a git seed only.',
295
295
  }),
296
296
  });
297
297
  }