bareloop 0.5.1 → 0.7.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,11 +1,14 @@
1
1
  # bareloop — Integration Guide
2
2
 
3
- > **Current through Layer 2 (ACCEPTED, F47)** (headless single-job loop: `runJob`, text +
4
- > tool middles, the draft-PR hitl step; the plan-v1 flow — four-field job shape, agent-
5
- > authored validated plans, in-run check exits — validated end-to-end on the real-model
6
- > acceptance battery: 3/3 conversion, 3/3 over the owned bar, the agent composing its own
7
- > check exits). The plan shape is the primary path; legacy `steps[]` co-exists but sunsets
8
- > on landing. Runs on two worker surfaces — the API `Loop` and, for
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
7
+ > operator-authored `steps[]` path, config-v1 and the draft-PR hitl step were deleted
8
+ > 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
9
12
  > `clipipe-subscription`, the CLI's native tool channel (module 4d). API sections fill in
10
13
  > as build-ladder rungs land (PRD §10). What is settled — the boundary, the architecture,
11
14
  > the refusals, the constraints — is settled for good. Per LIBRARY_CONVENTIONS §3 this
@@ -41,11 +44,13 @@ const spec = {
41
44
  provider: 'anthropic-api',
42
45
  cadence: { unit: 'day', every: 1 }, budgetUsd: 1.5,
43
46
  writeScope: ['src/**'],
44
- steps: [
45
- { id: 'fix', mode: 'tools', tools: ['read', 'grep', 'write'],
46
- close: { type: 'predicate', cmd: 'npm test', expect: 0 }, class: 'hard' },
47
- { id: 'pr', close: { type: 'hitl', prompt: 'PR opened — review and merge?' }, class: 'hitl' },
48
- ],
47
+ // the human signs the DESTINATION; the agent authors the road
48
+ goal: 'Fix any failure in src/ so the suite passes.',
49
+ verdictType: 'green',
50
+ // the close is an ORDERED LIST of named stages (PRD v1.28) — the check menu
51
+ // DERIVES from it; there is no separate `checks[]` field to author
52
+ close: [{ name: 'suite-green', cmd: 'npm test', expect: 0 }],
53
+ tools: ['read', 'grep', 'write', 'edit'],
49
54
  escalation: { mode: 'decision-ready' },
50
55
  };
51
56
 
@@ -58,17 +63,17 @@ const outcome = await runJob(spec, {
58
63
  provider: new AnthropicProvider({ model: 'claude-sonnet-5' }), // key from env
59
64
  emit: makeSpine('/path/to/checkout/run.jsonl'),
60
65
  });
61
- // 'escalated' at the hitl step is the happy path: the draft PR URL rides the
62
- // decision-ready escalation on the spine; merge stays human, forever.
66
+ // 'green' means the operator's close passed. Anything else is a named,
67
+ // decision-ready stop on the spine — never a silent partial success.
63
68
  ```
64
69
 
65
70
  ## All options
66
71
 
67
- Two documents, two validators — the schema is the option surface; config-red before
68
- tokens burn. The arbiter split is guarded from both sides by INEXPRESSIBILITY: the
69
- workflow config cannot say `close`/`provider`; the job spec cannot say
70
- `hooks`/`loop`/`memory`, any minting claim, or the shell-owned retry cap — all
71
- unknown-field reds.
72
+ Two documents, two validators — the SIGNED job spec (`validateJob`) and the AGENT-authored
73
+ plan (`validatePlan`). The schema is the option surface; every red lands before tokens
74
+ burn. The arbiter split is guarded by INEXPRESSIBILITY in both directions: the plan cannot
75
+ say `close`/`provider`/`budget`, and the job spec cannot say `hooks`/`loop`/`memory`, any
76
+ minting claim, or the shell-owned retry cap — all unknown-field reds.
72
77
 
73
78
  **Job spec (`schema: "job-v1"`, operator-owned — the arbiter's rulebook):**
74
79
 
@@ -76,34 +81,105 @@ unknown-field reds.
76
81
  |---|---|---|
77
82
  | `job` | kebab-case slug | |
78
83
  | `description` | non-empty string | |
79
- | `provider` | `anthropic-api` | menu; part of the lineage key by definition |
84
+ | `provider` | `anthropic-api` \| `clipipe-subscription` | menu (`PROVIDERS`); part of the lineage key by definition. Only `anthropic-api` is guaranteed (F48); `clipipe-subscription` drives the worker natively and needs `opts.nativeProvider` |
80
85
  | `conditions` | `{ providerPath?, closeVerbosity?, taskFraming?, scaffold? }` | declared keys only, string values — the environment label (consumed by the N3 lineage key; recorded on spines from run one) |
81
86
  | `cadence` | `{ unit: hour\|day\|week, every: 1..30 }` | validated now, consumed at N5 (Scheduler) |
82
87
  | `budgetUsd` | `0 < n <= shell cap` | ceiling chain: workflow ≤ job ≤ shell — each layer may tighten, never exceed |
83
- | `writeScope` | array of contained globs | the operator's outer fence; same containment law as the workflow layer, same code |
84
- | `steps` | array of `{ id, close, class, mode?, tools? }` | unique slug ids; every step names its close |
85
- | `steps[].mode` | `text` (default) \| `tools` | `text`: the worker returns ONE artifact written to the shell's `target`; `tools`: the worker drives Gate-governed file tools (multi-file). Illegal on `hitl` steps (they run no loop) |
86
- | `steps[].tools` | unique subset of `read\|grep\|write\|edit\|recall\|get` | the SPEC-side tool grant (`TOOL_MENU`, frozen; defaults to the full menu) — the drafted config cannot express mode or tools; `edit` (BA-13) is the anchored exact-once replace, judged by the SAME writeScope fence as `write`; requesting `run` (`LOCKED_TOOLS`) reds with the DISTINCT code `request-red` (locked-but-listed: the red IS the admission evidence the ledger tallies; a typo stays `invalid-value`) |
88
+ | `maxWallMs` | optional integer ms `>= MIN_WALL_MS` (one close timeout) | the run's wall clock. **NO DEFAULT, by ruling** — absent means time-unbounded *by explicit operator choice*, never by fallback (F45: a defaulted cap is a silent second ceiling). Enforcement is a BETWEEN-ROUND deadline, so the honest worst case is `maxWallMs + closeStages × closeTimeoutMs` — every stage of a staged close gets the FULL timeout — and all three numbers are reported (`loop.stop()` cannot cut an in-flight call — F61 measured 500ms→4,018ms). The `MIN_WALL_MS` floor is a ONE-stage number, so a spec with many stages can validate while its overshoot dwarfs its cap: the clock reports that honestly rather than the floor pretending to prevent it (stage-aware floor parked, PRD v1.39). Operator-only, tighten-only; adding or changing it changes the spec hash |
89
+ | `writeScope` | array of contained globs | the operator's outer fence; the plan's own scopes must fit inside it, same containment code |
90
+ | `steps` | RETIRED | operator-authored `steps[]` was deleted (PRD v1.32); a spec carrying it reds `shape-retired:steps` by name rather than half-running |
87
91
  | `escalation` | `{ mode: "decision-ready" }` | the pain channel is not optional |
88
92
 
89
- **The plan shape (Layer 2, design record 2026-07-21).** A job-v1 spec declares EITHER the
90
- `steps[]` chain above OR the four-field plan shape — never both (`shape-conflict` red).
91
- In the plan shape the AGENT authors the step plan at run time (gated by `validatePlan`);
92
- the human signs only:
93
+ **The plan shape — the only shape** (Layer 2, design record 2026-07-21). The AGENT authors
94
+ the step plan at run time (gated by `validatePlan`); the human signs only:
93
95
 
94
96
  | field | shape | notes |
95
97
  |---|---|---|
96
98
  | `goal` | non-empty text | what the agent plans against |
97
99
  | `verdictType` | `green` \| `soft-green` \| `hitl` | declared radio, never inferred (`VERDICT_TYPES`, frozen). v1 ADMITS only `green`; declaring `soft-green`/`hitl` reds `request-red` with the type as a structured `verb` field (declared-but-locked — the tool-menu pattern) |
98
- | `close` | a close object (table below) | ONE close, the only truth; `green` demands a hard-class close (`close-hierarchy` red on a rubric/hitl close) |
99
- | `checks` | optional array of `{ name, cmd, expect, judged?, gapKeep? }` | the operator-SIGNED named-check menu: the predicate-close body plus a kebab slug `name`, validated by the same rules, executed under the same runClose machinery. Plans reference them via the `check-passes(name)` exit; the agent can never author, edit, or compose one. **Checks decide nothing and mint nothing** — a check result is a progress gate and a gap source only |
100
- | `tools` | optional unique subset of `TOOL_MENU` | the CEILING every plan step's grant must fit inside (defaults to the full menu); `run` reds `request-red` here exactly as on a step grant. On a legacy `steps[]` spec this field is an `invalid-value` red (steps grant tools per step) |
100
+ | `close` | **an ORDERED LIST of named stages** `[{ name, cmd, expect, judged?, gapKeep?, offer?, needs? }, ...]` (PRD v1.28), or a close object (table below) **only** for the declared-but-locked verdict classes | the destination, the only thing hand-authored; the check menu DERIVES from it (below). The plan flow executes a staged close directly and adapts a bare `predicate` object into a one-stage list; a `gold`/`rubric`/`hitl` object close validates (the declared-but-locked verdict classes still parse) but the plan flow refuses it at runtime as `close-unsupported` — it names no command to run |
101
+ | `checks` | **RETIRED** (PRD v1.28/v1.32) | hand-authored checks are gone, not merely discouraged: declaring `checks` reds `checks-derived` by name. The check menu is DERIVED from the close's own stages instead — see **Staged close** below. The hazard this removes is measured, not theoretical: job #5's three hand-written checks were re-implementations of three stages the close already ran, and a hand-carved copy can drift LENIENT (the worker passes the operator's ruler and fails the real inspection) |
102
+ | `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 |
101
103
 
102
- `steps[]` is co-existing scaffolding with a staged sunset: it archives alongside
103
- config-v1 once the Layer 2 path proves itself in its battery — not before.
104
+ **`TOOL_MENU` — the worker's 14 verbs, in four components** (`src/job.js`; the verb→tool map
105
+ is `TOOL_BY_VERB` in `src/tools.js`). The menu is an INVENTORY: every verb is an existing
106
+ bare-agent or litectx primitive, none was built for it. A granted component's strategy line
107
+ ships with the grant (`COMPONENT_STRATEGIES` — capability without strategy is inert, F19).
104
108
 
105
- **Close types and the hierarchy** (a close is data, never code; verdict-class laundering
106
- is a named red `close-hierarchy`):
109
+ | component | verbs | class |
110
+ |---|---|---|
111
+ | write | `write` · `edit` | tree writes, judged by the signed `writeScope` fence — `edit` (BA-13) is the anchored exact-once replace and goes through that SAME fence, not a weaker one |
112
+ | select | `read` · `grep` · `recall` · `get` · `impact` · `related` · `recent` | read-only; every one is judged as a `read` action by that same fence, so the deny list applies through every door |
113
+ | compress | `compress` · `peek` | read-only (a signature tier; a stash's head/tail) |
114
+ | isolate | `stash` · `remember` · `forget` | STORE writes (the `.litectx` store, never the tree) — their own gate action types, so a store park can never be counted as a tree write by the F32 `workerWrites` instrument. **Not read-capable** |
115
+
116
+ `run` is locked at every layer and always will be: a worker that can run commands can run
117
+ its own close. `ctx_recall` serves BOTH axes — code pointers, plus any note written with
118
+ `ctx_remember`, returned on a line labeled `memory` with the note's BODY inline (capped at
119
+ 400 chars). A note is a conclusion, not a pointer: there is no worker verb that dereferences
120
+ a memory id, so a pointer-only reply would be inert. **`recall`, `get` and `impact` share ONE
121
+ 0-based line space** — the index's own index positions, one lower than the line an editor
122
+ shows — and every one of the three says so in its own tool contract. The numbers are
123
+ interchangeable handles: `impact`'s def range dereferences through `get` verbatim, and
124
+ `recall` prints the same number for the same chunk. Rendering any one of them 1-based was
125
+ tested and REFUTED: it would print two numbers for one chunk and turn `get`'s clean
126
+ chunk-boundary refusal into a guessing game.
127
+
128
+ **`impact` requires `ripgrep` (`rg`) on `PATH`.** litectx's caller scan shells out to `rg` and
129
+ catches a missing binary exactly as it catches "no matches", so without it the verb reports
130
+ `0 callers, risk low` — silently, in the false-isolation direction (LC-5, open upstream).
131
+ Install `rg` wherever the worker runs, or leave `impact` off the spec's `tools` ceiling.
132
+
133
+ **The store is caller-owned, and it is a LIVE cross-run channel.** `runJob`/`runPlan` root ONE
134
+ litectx store at `<workdir>/.litectx` (`new LiteCtx({ root: workdir })`, `src/planrun.js`) — an
135
+ on-disk SQLite index that OUTLIVES the run. A note written with `remember` lands there as a
136
+ durable `fact`, and `ctx_recall` reads that tier back on any later run against the same
137
+ workdir: agent notes persist across runs by default. The library ships NO reset — it never
138
+ deletes the store, because what a run inherits is the caller's decision, not the runner's. A
139
+ caller who wants a COLD baseline (an inheritance-OFF control arm, a reproducible re-run, any
140
+ contrast between two runs) must remove `<workdir>/.litectx` itself between them; the in-repo U
141
+ runner does exactly that with a one-line `rmSync`. The store is a derived, self-healing cache
142
+ by litectx's own contract, so the only cost of deleting it is the re-index. Store writes are
143
+ bounded: `stash`/`remember` payloads over **64KB** (`ISOLATE_MAX_BYTES`) come back as a
144
+ refusal RESULT naming the actual size and the limit — worker feedback, never a throw, and
145
+ nothing enters the store. The bound is per-payload (aggregate growth across distinct ids is a
146
+ future threshold call); `forget` needs no cap — its id is a bound SQL parameter, never stored.
147
+
148
+ **Staged close — the check menu DERIVES from it (PRD v1.28, `checkMenu` in `src/job.js`).**
149
+ Every stage is a `predicate` command body (the same `cmd`/`expect`/`judged?`/`gapKeep?`
150
+ contract the single close object has always used) plus:
151
+
152
+ | stage field | shape | notes |
153
+ |---|---|---|
154
+ | `name` | unique kebab-case slug | the plan references it via `check-passes(name)`; duplicates red `duplicate-id` |
155
+ | `offer` | optional boolean, default offered | `offer: false` hides the stage from the derived menu — it never reaches the agent. For a stage that cannot stand alone as a ruler: a PRECONDITION (e.g. "the seed commit exists" — passes instantly, teaches nothing) or the **final grading stage** — of the ten specs in `jobs/`, four hide a final `verdict` stage and six hide a `changed-from-seed` precondition instead (a per-spec convention, not a schema rule — nothing stops a stage named last from being offered except the spec author remembering to set the flag; preflight's `check-menu` event always names the full menu either way, so a forgotten flag is visible in the run's own record) |
156
+ | `needs` | optional non-empty array of EARLIER stage names | a stage that reads what an earlier stage built (e.g. "the public API matches what an earlier stage emitted") names its prerequisite chain; picking it via `check-passes` runs the chain first, then the stage itself. Every name must be declared before this stage (`invalid-value` otherwise); `needs` + `offer: false` together is incoherent (`invalid-value`) — a stage with a chain to run must be reachable |
157
+
158
+ The stages run in declared order as the close itself; the first red renders the verdict and
159
+ later stages never run. `checkMenu(close)` returns only the offerable stages (each with its
160
+ run chain, prerequisites first); a hidden or partial menu is an acceptable case, never a
161
+ failure — `check-menu` on the spine reports `hidden` + `meaning` whenever the derived menu is
162
+ narrower than the stage list.
163
+
164
+ **The law for close authors: ONE POPULATION PER STAGE.** The trend reader buckets one series
165
+ per stage NAME, and a stage name is not an axis — nothing in the library can stop a stage from
166
+ redding on two structurally different measurements under one name, and a run that crosses that
167
+ seam donates both genres to one series (a 29 → 4 across such a seam reads *converging* and
168
+ would recommend a top-up on work that had only swapped which wall it was behind). If a stage
169
+ can report two unlike numbers, **split it into two stages** in the close, each where its branch
170
+ already runs — the gate sequence is unchanged because the close is first-red-wins. The shipped
171
+ `u-*` closes were split exactly that way (`typecheck` / `typecheck-outside` /
172
+ `tests-kept` / `suite-green`), and the fix is never a sharpened reader: `readGrade` is one
173
+ reader for every close, deliberately (the F49 precedent). **Splitting a stage moves the spec
174
+ hash** — stage names are in the signed spec — so it is a spec edit the operator re-signs, and
175
+ it changes what stored bridges match (below).
176
+
177
+ **Close types and the hierarchy — the OBJECT form, which survives only for the
178
+ declared-but-locked verdict classes** (a close is data, never code; verdict-class laundering
179
+ is a named red `close-hierarchy`). The go-forward shape is the staged list above; a
180
+ `predicate` object is legal shorthand for a one-stage list and the plan flow adapts it, but
181
+ `gold`/`rubric`/`hitl` validate and then refuse at runtime (`close-unsupported`) since v1
182
+ admits `verdictType: green` only:
107
183
 
108
184
  | type | fields (exact — extras red) | legal class |
109
185
  |---|---|---|
@@ -127,7 +203,7 @@ code said.
127
203
 
128
204
  It is a **floor, not a zero-check**: `node --test` reports a crashed file as ONE failing
129
205
  test, so "zero executed" never fires. Declare it against the suite's real size
130
- (`{ pattern: "^ℹ tests (\\d+)$", min: 300 }` for a 391-test suite, with
206
+ (`{ pattern: "^ℹ tests (\\d+)$", min: 300 }` for a 558-test suite, with
131
207
  `--test-reporter=spec`, which prints both the counts and the failures at the end). It
132
208
  catches *"the arbiter did not run"* — wrong tree, broken argv, a failed shared import — not
133
209
  *"one test file is broken"*, which is an honest red the worker should fix.
@@ -173,23 +249,34 @@ middle writes; `validatePlan` gates it against the SIGNED job spec before tokens
173
249
  | `steps[].tools` | non-empty unique subset of the SPEC ceiling | a verb beyond the ceiling reds `verb-escape` with the verb as structured data (overreach, distinct from the operator-side `request-red`); `run` is `verb-escape` at every layer |
174
250
  | `steps[].rounds` | int 1..shell cap (default 40) | the step's per-attempt tool-round bound (the Gate's `maxTurns` natively) |
175
251
  | `steps[].target` | path inside the fence | v1.18 deliverable; REQUIRED on write-granted steps |
176
- | `steps[].exit` | 1..2 items (`MAX_EXITS_PER_STEP`), ALL must pass (AND-only, no OR/NOT) | closed menu (`EXIT_TYPES`): `artifact-written(path, pattern?)` · `tree-changed(scope)` · `json-valid(path)` · `check-passes(name)`. `check-passes` must name a SIGNED check (`check-unknown` red names the signed menu); on a write-granted step it must be paired with `tree-changed` (`exit-illegal` — the seed tree is green, a lone check would pass untouched, F17/F46). `artifact-written.pattern` must compile AND survive a ReDoS shape check — an unbounded quantifier over a group that itself repeats unboundedly (`(a+)+`, `(\d*)*`, even wrapped: `((a+))+`) is an `invalid-value` red (F49, catastrophic-backtracking footgun; rewrite without a repeated group inside a repeat). Exits verify FORM, not truth — progress gates; the operator's close stays the one arbiter |
252
+ | `steps[].model` | optional; `sonnet` \| `haiku` (`STEP_MODELS`) | the step's model TIER — a menu, never a model string the agent spells (the tier→model mapping and any per-model effort params are the runner's). `opus` is deliberately absent: reserved for work the human assigns, never plan-selectable. Off-menu reds `invalid-value`. A step naming a tier when the caller supplied no `providerFor` factory is an `interpreter-red` STOP, never a silent run on the default tier |
253
+ | `steps[].attempts` | optional; int `>= 1` | **RETIRED and INERT (F79) — it bounds nothing.** A step's iterations are governed by the STRIKE LADDER (below), which is shell-owned and inexpressible from the plan. The field is no longer offered in the drafting prompt and the runner ignores it; it is deliberately still ACCEPTED, because stored bridge plans carry it and a plan minted under one runner's number must not red under another's. The shape is still checked (a known field holding garbage stays a `bounds` red); the old `<= capRuns` ceiling is gone with the cap it named. It was previously TIGHTEN-only — which meant a step that needed MORE iterations could not ask, and drafters measurably tightened it instead (F77) |
254
+ | `steps[].scope` | optional; one value from the SAME menu `tree-changed` uses | narrows this step's live WRITE fence to a subset of the signed `writeScope` — chosen from the offered scope menu (below), never authored. Off-menu reds `invalid-value` carrying the menu |
255
+ | `steps[].exit` | 1..2 items (`MAX_EXITS_PER_STEP`), ALL must pass (AND-only, no OR/NOT) | closed menu (`EXIT_TYPES`): `artifact-written(path, pattern?)` · `tree-changed(scope)` · `json-valid(path)` · `check-passes(name)`. **`tree-changed.scope` is a MENU, not a glob the agent authors** — the runner enumerates the signed fence entries plus the real directories beneath them (shallowest-first, capped at `MAX_SCOPE_MENU`=24; `legalScopes` in `src/plan.js`), `planPrompt` lists them verbatim, and `validatePlan` accepts membership only — the SAME array on both sides, so what was offered is what is accepted; omitted, it derives from `writeScope` alone — never a free-text fallback. An off-menu value that escapes the fence still reds `scope-escape` (the ledger's attribution class), an in-fence unoffered value reds `invalid-value` carrying the menu. `runPlan` emits `scope-menu {offered, truncated, offerableCount?, cap?}` so a capped menu is never silently complete. `check-passes` must name a stage on the close's DERIVED menu (`check-unknown` red names the offered menu — no operator authors this list, it comes straight from `checkMenu(close)`); on a write-granted step it must be paired with `tree-changed` (`exit-illegal` — the seed tree is green, a lone check would pass untouched, F17/F46). **`check-passes` also requires a write-class verb (`write`/`edit`) on the SAME step** (`exit-illegal` — BREAKING for plan authors: a read-only "verify" step carrying a check used to validate and no longer does). A failing check's gap is re-delivered to that step's OWN worker, so a step holding a check must be able to act on it; a read-only one is a mailbox with no hands and stalls to cap on a byte-identical gap (measured on 4 of 4 drafted plans). The check belongs on the step that fixes; the run's final verification is the operator's close, which the agent never authors. The rule is stated identically in the drafter prompt and the validator, so the two can never disagree — and it is suppressed when `tools` failed to parse (the step's hands are unknowable then, and the real defect already redded: one defect, one red). `artifact-written.pattern` must compile AND survive a ReDoS shape check — an unbounded quantifier over a group that itself repeats unboundedly (`(a+)+`, `(\d*)*`, even wrapped: `((a+))+`) is an `invalid-value` red (F49, catastrophic-backtracking footgun; rewrite without a repeated group inside a repeat). Exits verify FORM, not truth — progress gates; the operator's close stays the one arbiter |
177
256
 
178
- Red vocabulary (all three validators): `parse-error`, `unknown-field`, `missing-required`,
257
+ Red vocabulary (both validators): `parse-error`, `unknown-field`, `missing-required`,
179
258
  `invalid-value`, `bounds`, `duplicate-id`, `close-type`, `close-hierarchy`,
180
- `secret-literal`, `scope-escape`, `fence-invalid` (a malformed `jobWriteScope` fence — attributed to `jobWriteScope`, never the workflow config), `shape-conflict` (both job shapes declared),
259
+ `secret-literal`, `scope-escape`,
181
260
  `request-red` (locked-but-listed: a locked tool or verdictType — admission demand the
182
- ledger tallies), plus the workflow-side verb reds (`verb-illegal`,
183
- `verb-placement`, `verb-params`, `slot-overflow`) and the plan-side reds (`verb-escape`,
184
- `exit-illegal`, `check-unknown`, `job-invalid` — a plan validated against a missing or
185
- non-plan-shape job fails CLOSED). The `secret-literal` sweep is
261
+ ledger tallies), plus the plan-side reds (`verb-escape`,
262
+ `exit-illegal`, `check-unknown`, `step-scope-escape` — a scoped step whose `target` falls
263
+ outside its OWN narrowed scope (in-fence but gate-denied ground: the step's gate is built
264
+ from the narrowed prefix, so the pair is rejected at validation instead of burning attempts
265
+ on refusals). It binds WRITES, not observations: on the exit side it fires only on a
266
+ `tree-changed` scope DISJOINT from the step's (a scope that CONTAINS it fires exactly when
267
+ the narrow one does), and `artifact-written`/`json-valid` paths carry no step-scope red at
268
+ all — the evaluator asks nothing about who wrote the file, so naming a prior step's artifact
269
+ is legal and satisfiable. `job-invalid` — a plan validated against a
270
+ missing or non-plan-shape job fails CLOSED). `stageClose(close)` is the ONE staging
271
+ every check-menu consumer shares (array → itself; legacy object predicate → its
272
+ one-stage list, named `close`; gold/rubric/hitl → null). The `secret-literal` sweep is
186
273
  defense-in-depth against known token shapes — env-only loading remains the law, not the
187
274
  sweep.
188
275
 
189
276
  ## Public API
190
277
 
191
- *Landed through N2 + the Layer 2 core (spine + shell + three validators + interpreter
192
- with text/tool middles + the plan executor + extractor + runJob). Still TBD: N3
278
+ *Landed through N2 + the Layer 2 core (spine + shell + the two validators + the plan
279
+ executor + extractor + runJob). Still TBD: N3
193
280
  (contrast-bit extractor live), N4 (verdict classes — gold/rubric close EXECUTION), N5
194
281
  (scheduler + budget ops + CLI), N6 (panel).*
195
282
 
@@ -199,15 +286,31 @@ Append-only JSONL event emitter bound to one file. `seq` monotonic per spine, `t
199
286
  last. Consumers are pure listeners; nothing reads the file back. Returns each event as
200
287
  written.
201
288
 
202
- ### `ralph({ middle, close?, judge?, capRuns, emit, redact?, closeTimeoutMs?, cwd?, expect?, judged?, gapKeep?, workerWrites? })` → `'green' | 'escalated'` — `src/ralph.js`
203
-
204
- The dumb outer shell: `while close-red and under-cap: run the middle`. `close` is an argv
205
- whose exit code is truth (`runClose` is also exported); the red gap text feeds the next
289
+ ### `ralph({ middle, close?, judge?, capRuns?, ladder?, emit, redact?, closeTimeoutMs?, cwd?, expect?, judged?, gapKeep?, workerWrites? })` → `'green' | 'escalated'` — `src/ralph.js`
290
+
291
+ The dumb outer shell: `while close-red and not exhausted: run the middle`. **Exhaustion has
292
+ two alternative rules and you supply exactly one** — `capRuns` (a fixed number of middle runs)
293
+ or `ladder` (a PROGRESS governor). With a governor the iteration count floats and the loop
294
+ ends on STRIKES instead; ralph still interprets nothing — it hands the governor each red
295
+ iteration's gap and reads back a boolean. Both rules exhaust through the SAME terminal:
296
+ category `cap-halt`, the same three records in the same order, so nothing downstream (the
297
+ ledger's excluded-set, the step loop's one replan trigger) has to know which rule fired.
298
+ `ladder` is an INTERFACE, not one module: `runPlan` wires `createLadder` (`src/ladder.js`,
299
+ repeats + writes) per step and — since v1.46 — the close TREND governor (`src/trend.js`,
300
+ per-stage graded numbers) on the close-fix loop. A governor may supply an optional
301
+ `terminal()` to override the exhaustion PROSE (the step ladder's copy offers a replan and
302
+ there is no planner at the close); it may never override the category or the outcome. Both
303
+ modules are INTERNAL (not on the public surface), so a direct `ralph` caller uses
304
+ `capRuns` — the strike rule reaches you through `runJob`/`runPlan`'s `strikeLimit`. `close` is an argv
305
+ whose exit code is truth (`runClose` is also exported — **async since 0.6: `runClose` and
306
+ `runStages` return Promises**; the child is awaited instead of spawnSync so a running close
307
+ no longer freezes the host event loop (F68); every close semantic — timeout signal, output
308
+ bounds, gap shape, exit bands — is byte-identical); the red gap text feeds the next
206
309
  iteration, tail-biased when bounded (400 head + 1500 tail — the assertion diff lives at
207
310
  the end). **`cwd` is where the close RUNS, and it is load-bearing (F8):** a close is a
208
311
  repository command (`npm test`, `make check`) and every one of them is cwd-relative — run
209
- it anywhere but the workdir and the arbiter judges the wrong tree. `interpret`/`runJob`
210
- always pass the workdir. **Corollary for job authors (F15/F28):** the gap bound keeps the
312
+ it anywhere but the workdir and the arbiter judges the wrong tree. `runPlan`
313
+ always passes the workdir. **Corollary for job authors (F15/F28):** the gap bound keeps the
211
314
  head and the TAIL, so a close whose failures print mid-stream (a 391-subtest TAP dump) tells
212
315
  the worker only the summary counts — pick a reporter whose failures land at the end, **or**
213
316
  set the close's `gapKeep` (job-spec, arbiter territory) to a regex like `"^not ok"` so the
@@ -215,7 +318,24 @@ matching failure lines are preserved in a capped kept-failures block regardless
215
318
  they print (F28: the first real firing delivered a gap with zero failure names). The gap
216
319
  also combines stdout+stderr, so a stdout failure survives stderr noise.
217
320
  `closeTimeoutMs` caps the close's wall clock (default 120s) — shell/operator
218
- territory, inexpressible in any config.
321
+ territory, inexpressible in any config. A faulted close is asked to leave with SIGTERM and,
322
+ if it is still there 2s later, ended with SIGKILL: SIGTERM is a request a close can decline
323
+ (its own handler, a trapping wrapper), and without a second deadline the wait never resolves.
324
+ The verdict is untouched — the FIRST fault named the outcome and the kill only enforces it.
325
+
326
+ **The close child's environment is stripped of credential-shaped variables before it runs**
327
+ (`CLOSE_ENV_DENY` in `src/ralph.js`: provider keys, `AWS_*`, any name ending
328
+ `_API_KEY`/`_SECRET`/`_TOKEN`/`_PASSWORD`/`_CREDENTIAL(S)`, plus known credential spellings
329
+ the suffix rule cannot see — `PGPASSWORD`, `MYSQL_PWD`, `SSH_PRIVATE_KEY`, bare
330
+ `TOKEN`/`SECRET`/`API_KEY`, …) — a close judges a tree and never
331
+ needs them. A COPY is stripped, never `process.env`, so the host keeps the key it spends every
332
+ round. This is exposure reduction, not a sandbox: worker-authored code run by the close still
333
+ has network and your OS user's permissions. The strip has no opt-out — no plan, config, or
334
+ spec field can widen it — so if your close genuinely needs a credential-shaped variable
335
+ (`AWS_*` regions/profiles included, e.g. a suite booting against localstack with a test DB
336
+ password, or `PGPASSWORD` for one that hits a real postgres), it will not be there and the
337
+ close reds; set such values inside the close command
338
+ itself, not the inherited environment.
219
339
 
220
340
  **The forbidden zone (PRD v1.11 / F17).** `runClose` returns a verdict ONLY when judgment
221
341
  was rendered. `expect` (the signed exit code, default 0) and `judged` define the two clean
@@ -240,7 +360,7 @@ worker has written files this run — with a clean precheck baseline (`runJob` e
240
360
  crash-at-precheck before any tokens), that crash is the worker's own broken edit, the most
241
361
  recoverable red there is. The routed verdict is the DISTINCT `worker-crash` (spine event
242
362
  `worker-crash` with the file list; never plain `crashed`, never `needs_revision`), and the
243
- gap tells the worker which files it wrote and to fix or revert. `interpret` wires the seam
363
+ gap tells the worker which files it wrote and to fix or revert. `runPlan` wires the seam
244
364
  to the gate audit's allow-decision write/edit lines (run_id-scoped); no seam or zero writes
245
365
  keeps the old behavior — an instrument crash still escalates `close-crashed`, never retried.
246
366
  Measured motivation: battery pass 1 (F31) lost 4 of 7 rows to exactly this escalation.
@@ -257,48 +377,40 @@ its close.
257
377
  Escalations are decision-ready (category, options, spend); cap-halt is its own
258
378
  category, never merged with "wrong". A thrown middle is relayed by its `category`
259
379
  property (`cap-halt`, `gate-red`, …); an unnamed throw is `interpreter-red`. Close output
260
- is scrubbed at capture (an injected `redact`, wired to bareguard by `interpret` with the
380
+ is scrubbed at capture (an injected `redact`, wired to bareguard by `runPlan` with the
261
381
  validators' full secret-shape inventory — Bearer/sk-/ghp_/github_pat_/AKIA/xox) so a
262
382
  secret a checked command echoes never enters the append-only spine or a worker prompt —
263
383
  a benign gap is byte-identical (secrets hard line; design law #7 intact).
264
384
 
265
- ### `validateConfig(input, { shellCapUsd?, jobWriteScope? })` → `{ ok, reds, config }` — `src/validate.js`
266
-
267
- Deterministic schema-v1 predicate; never throws. Every failure is a named red
268
- `{ code, path, detail? }` — reds before tokens burn. Returns the parsed config on ok
269
- (single parse; `null` on any red). Pass the job spec's `writeScope` as `jobWriteScope`
270
- and every workflow scope must fit inside the fence (path-boundary aware: `src2` is not
271
- inside `src`) or it reds `scope-escape`; pass `min(shell cap, job budgetUsd)` as
272
- `shellCapUsd` to complete the ceiling chain. Verb vocabulary bound from litectx
273
- (`LOOP_SHAPES`, `SLOTS`, `VERBS` exported). `diffPaths(a, b)` returns changed JSON paths —
274
- the one-knob mutation checker.
275
-
276
- `scanSecrets(raw)` → `string[]` is the same module's text-side scan: every known
277
- secret-shape match in a raw stream (a spine file, a close's output), `[]` when clean,
278
- never throws. Use it instead of re-deriving a scan from `SECRET_PATTERNS` — detection
279
- and redaction share ONE shape inventory, and a hand-rolled copy that misses a shape is
280
- a leak on the very output it was guarding.
281
-
282
385
  ### `validateJob(input, { shellCapUsd? })` → `{ ok, reds, job }` — `src/job.js`
283
386
 
284
- The operator-owned sibling (never an extension) of `validateConfig`: validates a
285
- `job-v1` spec — see **All options** for the full schema, close types, and hierarchy.
286
- Never throws on JSON text or plain parsed data (the ingest contract); returns the
287
- parsed spec on ok, `null` on any red. Validates BOTH job shapes (legacy `steps[]` and
288
- the Layer 2 plan shape — see the option tables above). Menus exported:
289
- `CLOSE_TYPES`, `CLASSES`, `CLASS_BY_CLOSE`, `GOLD_COMPARE`, `CADENCE_UNITS`,
290
- `PROVIDERS`, `CONDITION_KEYS`, `STEP_MODES`, `TOOL_MENU`, `LOCKED_TOOLS`,
291
- `VERDICT_TYPES`, `LOCKED_VERDICTS`.
292
-
293
- ### `validatePlan(input, { job, maxStepRounds? })` → `{ ok, reds, plan }` — `src/plan.js`
294
-
295
- The third validator: gates the AGENT-authored plan doc (`schema: "plan-v1"`) against the
296
- SIGNED job spec before tokens burn — the ceiling, the fence, and the checks menu all come
387
+ The operator-owned validator — the arbiter's rulebook, and the sibling of
388
+ `validatePlan`, never its extension: what the human signs and what the agent authors
389
+ are two documents with two validators, which is what makes the arbiter inexpressible
390
+ from the agent's side. Validates a `job-v1` spec — see **All options** for the schema,
391
+ close types, and hierarchy. Never throws on JSON text or plain parsed data (the ingest
392
+ contract); returns the parsed spec on ok, `null` on any red. A spec carrying the retired
393
+ `steps[]` reds `shape-retired` by name (PRD v1.32) rather than falling through to a
394
+ generic unknown-field. Menus exported: `CLOSE_TYPES`, `CLASS_BY_CLOSE`, `GOLD_COMPARE`,
395
+ `CADENCE_UNITS`, `PROVIDERS`, `CONDITION_KEYS`, `TOOL_MENU`, `LOCKED_TOOLS`, `STORE_VERBS`,
396
+ `VERDICT_TYPES`, `LOCKED_VERDICTS` — plus `checkMenu` itself.
397
+
398
+ ### `validatePlan(input, { job, maxStepRounds?, scopes? })` → `{ ok, reds, plan }` — `src/plan.js`
399
+
400
+ `validateJob`'s SIBLING in the two-document split, never a third validator over it (the
401
+ two-doc split's third validator never happens: plan-v1 gates the PLAN, the job spec stays
402
+ the arbiter's only home). Gates the AGENT-authored plan doc (`schema: "plan-v1"`) against
403
+ the SIGNED job spec before tokens burn — the ceiling, the fence, and the checks menu all come
297
404
  from `job` (a missing or non-plan-shape job fails CLOSED, `job-invalid`). Never throws;
298
405
  same `{ code, path, detail }` red shape as its siblings; `verb-escape` reds carry the
299
406
  escaping verb as a structured `verb` field (the ledger keys on it). `maxStepRounds`
300
- (default 40 — the shell's tool-mode per-attempt bound) ceilings every step's `rounds`.
301
- Menus exported: `EXIT_TYPES`, `MAX_EXITS_PER_STEP`, `MAX_PLAN_STEPS`, `WRITE_VERBS`.
407
+ (default 40 — the shell's tool-mode per-attempt bound) ceilings every step's `rounds`;
408
+ **the `capRuns?` option is GONE** (F79) — it existed only to ceiling `attempts`, now
409
+ retired-but-accepted, so a plan carries no iteration bound to check; `scopes` is the offered
410
+ `tree-changed` scope menu — the array of scope strings the drafter prompt listed, which
411
+ `runJob`/`runPlan` build internally from the signed fence plus the real directories beneath
412
+ it (a direct `validatePlan` caller may pass its own array); omitted, it derives from the
413
+ signed `writeScope` alone, never a free-text fallback. Menus exported: `EXIT_TYPES`, `MAX_EXITS_PER_STEP`, `MAX_PLAN_STEPS`, `WRITE_VERBS` — plus `stageClose`.
302
414
 
303
415
  ### `snapshotScope(dir, scope)` / `evalExits(exits, { dir, snapshot?, runCheck? })` — `src/exits.js`
304
416
 
@@ -311,146 +423,270 @@ the snapshot): an identical re-write is NOT a change (F43) and git status is nev
311
423
  consulted (F45). `artifact-written` rejects zero-byte files. `check-passes` delegates
312
424
  through the `runCheck` seam (the runner wires runClose); an unwired or crashed seam
313
425
  fails CLOSED with `fault` carrying a runClose verdict name — an instrument fault
314
- escalates through `CLOSE_FAULTS`, never masquerades as worker feedback. Failing details
315
- are counts and names only, never file bodies (they ride the append-only spine).
426
+ escalates through `CLOSE_FAULTS`, never masquerades as worker feedback. Failing details are
427
+ mostly counts and names — with one exception that matters: `json-valid` embeds `JSON.parse`'s
428
+ own message, and V8 quotes a window of the SOURCE inside it, so that detail can carry file
429
+ bytes the worker chose. The plan runner redacts EVERY detail (`scrub`/`SECRET_PATTERNS`, the
430
+ ONE inventory) at the emission boundary, before anything rides the append-only spine. The
431
+ module itself does not scrub: a direct `evalExits` caller wiring results into its own log
432
+ must apply its own redaction.
316
433
 
317
434
  ### `jobSpecHash(job)` / `checkApproval(job, approvals)` — `src/job.js`
318
435
 
319
436
  The pure half of **human-signs-always**: an agent may draft a job spec, but no job runs
320
437
  until a human approves that exact version. `jobSpecHash` is sha256 over canonical JSON
321
- (key-order independent) — any edit changes the hash, so an edited spec is unapproved by
322
- construction. `checkApproval(job, approvals)` is a pure predicate over
323
- `{ specHash, signer, ts }` records; the approval record lives OUTSIDE the document it
324
- signs and is shell/human territory, never agent-writable. The N2 runner enforces it.
438
+ (key-order independent) of the **RESOLVED** spec (MED-1): an omitted `tools` is filled in
439
+ with the concrete current `TOOL_MENU` before canonicalization, because that is the ceiling
440
+ `plan.js` and `planrun.js` actually read. So any SEMANTIC edit changes the hash and an
441
+ edited spec is unapproved by construction — but not every BYTE edit: writing today's full
442
+ menu into a spec that omitted `tools` is hash-NEUTRAL, since the two spell the same
443
+ ceiling. The direction that matters bites instead — a `TOOL_MENU` WIDENING (a library
444
+ upgrade) flips the hash of an omitted-`tools` spec whose bytes never moved, straight into
445
+ the refuse-until-reapproved machinery. That is the point: the signature pins WHICH menu it
446
+ covers, so an already-signed ceiling can never grow unsigned. A spec that named its own
447
+ `tools` hashes exactly as before. **BREAKING** for an approval held against an
448
+ omitted-`tools` spec: it re-signs once. `checkApproval(job, approvals)` is a pure predicate
449
+ over `{ specHash, signer, ts }` records, canonicalizing the SAME resolved form (a gate
450
+ reading a different form than the mint would unapprove every omitted-`tools` spec); the
451
+ approval record lives OUTSIDE the document it signs and is shell/human territory, never
452
+ agent-writable. The N2 runner enforces it.
325
453
  Reserved spine vocabulary (V7, machinery-free until job #1 surfaces one):
326
454
  `coordination-red` — a failure between units (scope contention, step order, store
327
455
  races), never to be folded into worker/interpreter reds.
328
456
 
329
- ### `interpret(configRaw, opts)` → `'green' | 'escalated' | 'config-red'` — `src/interpret.js`
330
-
331
- The only code that reads a config. Composes bareguard `Gate` (write scopes, USD budget),
332
- litectx (recall/compress/stash/remember hook ops), and the bareagent `Loop` under `ralph`.
333
- The provider and the close arrive from the shell, never the config. Optional `revisor`
334
- seam fires once after `STALL_REDS` consecutive close reds; the interpreter owns acceptance
335
- (arbiter-touch / cap-touch / validation reds), judged and installed on the candidate's
336
- PARSED form. Emits `config-final` — the run-as-executed config (design law #2) — on every
337
- run. Pass the job spec's fence as `jobWriteScope` (or `null`/omit for no fence): it is
338
- enforced HERE — the one choke point where a config becomes a Gate — on entry AND on every
339
- revision candidate, so a workflow scope outside the operator's fence reds before tokens.
340
- An enforcement belt resolves every scope and refuses to build a Gate that escapes the
341
- workdir, independent of validator correctness (law #1).
342
-
343
- Two middles, chosen by the step (`mode`/`tools` opts — SPEC-side territory threaded by
344
- the runner, never the config's): **text** (default) extracts ONE artifact from the
345
- response (`artifact-red` on a non-artifact: writes nothing, names its own axis on the
346
- spine, the retry is told why) and writes it to `target` behind a manual gate check —
347
- a fence counts as the artifact's wrapper only when it opens within the first 5 lines
348
- (the chatty-preamble shape); deeper fences are the artifact's OWN content and the whole
349
- reply is the artifact (fence-heavy artifacts — doc generators, markdown emitters —
350
- belong in tool mode, where nothing is parsed);
351
- **tools** offers only the granted tools (`read|grep|write|edit|recall|get`) to the
352
- worker's Loop, every call policy-checked against the SAME fence (tool-call paths resolve
353
- exactly as the tools resolve them — workers must use absolute paths; a relative spelling
354
- reds at the fence and the deny reason teaches the retry), reads pinned to the workdir, a
355
- denial streak stopping as `gate-red`. `edit` maps to bare-agent's `shell_edit` (BA-13,
356
- 0.29.0): an anchored exact-once replace judged as bareguard's `edit` action under the same
357
- writeScope — and the persona carries the strategy (prefer the edit verb; whole-file
358
- rewrites are how trees get broken, F31). In tool mode there is no artifact to extract —
359
- the close judges the tree; on-green `remember` retains the worker's change summary.
360
- Worker-caused close crashes feed back as `worker-crash` gaps via the gate audit (F32 —
361
- see `ralph` above).
362
-
363
- **Layer R — the within-run ratchet (`layerRoot`, default `false`; design record
364
- 2026-07-19).** The shell mechanically detects fixation across attempts — consecutive
365
- attempts that rewrote the same file(s) without moving the close's kept-failure set
366
- (`closeGapKeep` lines, duration stamps stripped before comparing; without a gapKeep the
367
- detector degrades to write-overlap alone and the spine event names the mode) — and
368
- injects an escalating note into the next prompt: first a capped summary naming the
369
- repeated files, then (if still stuck) the worker's OWN prior failed edit content
370
- verbatim, scrubbed and capped with trims announced. Inert unless stuck; the worker
371
- authors nothing and gains no verb. Two axes, deliberately separate (F43): the DETECTOR
372
- reads INTENT (the gate audit's allow-set — an edit whose anchor missed is still the
373
- worker reaching for that file again, and a tree-diff detector measured blind to exactly
374
- that), while the NOTE's claims about file contents read OUTCOME (settled post-execution
375
- through `Loop`'s `onToolResult`). So a repeat that never applied is told its anchor
376
- missed, and only content actually in the file is ever described as having landed. Spine event `root-injected` carries
377
- `{stage, mode, streak, paths, redSetSize}` — counts and paths only, NEVER content (the
378
- spine is append-only). State dies with the run: this is within-run scratch, not
379
- across-run memory. `layerRoot: true` is the ON/experimental arm; the default is OFF. It is
380
- wired on BOTH surfaces (F50): the legacy `steps[]` path (`interpret`) and the accepted
381
- plan-v1 flow (`runPlan`, one root per EXECUTE step plus one in the outer close-fix loop). On
382
- the plan flow the per-step red-set is the exit evaluator's OWN gap (the whole normalized
383
- complaint) rather than a single close's gapKeep, while the fix loop uses the close's own
384
- gapKeep (raw close output); the write-tee makes same-path target rewrites visible to the
385
- detector. It is NOT wired on the native/clipipe worker (no `onToolResult` seam — F48 fallback). Status (F41):
386
- the ratchet is MEASURED inert on every current job — two frozen probes + an archive sweep
387
- read 0 fixated pairs in 14 (the F21 repetition disease was a broken-loop symptom, since
388
- cured). Because ON has therefore never won its own A/B, it ships **OFF by default (decided
389
- 2026-07-21)** — armed and correct, but not default-enabled until a stuck job shows ON beats
390
- OFF. Flip the default to `true` the day that evidence lands; a `root-injected` event on your
391
- spine (when you pass `layerRoot: true`) is a signal worth reading, not noise.
392
-
393
- ### `runJob(spec, { approvals, workdir, provider, nativeProvider?, emit, target?, capRuns?, shellCapUsd?, closeTimeoutMs?, execCmd?, layerRoot? })` → outcome — `src/run.js`
394
-
395
- The N2 runner — the shell's top layer; composes everything below it and interprets
396
- nothing itself. Sequence: **approval gate** (human-signs-always — refuses an unapproved
397
- spec before ANY provider call: `unapproved-spec`) → **primitive smoke** (litectx
398
- known-answer round-trip before tokens: `smoke-red` — silent degradation throws nothing)
399
- → **sequential per-step interpret loops** under the ONE cumulative ledger (each step's
400
- ceiling is `min(job budget − spent, shell cap)`; a step that cannot green stops the job:
401
- `step-red:<id>`) → the **hitl step** opens a draft PR deterministically (branch → stage
402
- the job fence ONLY, so spines/audit logs never enter the PR → commit → push →
403
- `gh pr create --draft`) and ends `escalated` BY DESIGN, the PR URL riding the
404
- decision-ready escalation; a PR failure is `pr-red` and the escalation still fires with
405
- the error. Model tools never touch git — `execCmd` is the shell-owned process seam
406
- (defaults to real spawnSync). **The hitl step checks the fence first**: an
407
- affirmatively-clean `git status --porcelain -- <fence>` means nothing changed, so there
408
- is nothing for a human to review — `pr-skipped` + `step-end: already-green`, the job ends
409
- `green`, no PR and no escalation (a cadenced no-op run is silent and free). A FAILED
410
- check falls through to the PR path and reds there — an unknown fence state is never a
411
- green. The PR step **hands the checkout back**: the starting branch is read before
412
- anything moves and restored on every path; a failed restore is a loud `workdir-red`
413
- naming the stranded branch (it never un-opens a real PR). Outcomes: `green | escalated | unapproved-spec | job-red |
414
- smoke-red | config-red | pricing-red | provider-red | cap-halt | close-unsupported |
415
- step-red:<id>` — `provider-red` is a transport throw from the drafting call, OR a worker
416
- round the API cut off mid-generation (`truncated:max_tokens`, bare-agent 0.27.0/BA-6 —
417
- before which it laundered into a clean finish, F25); in both cases no verdict exists and the
418
- failed round's spend is only partly known (F6). `cap-halt` is drafting spend consuming the whole job budget before a
419
- valid config existed. Both are decision-ready escalations with a terminal `job-end`:
420
- the spine never dangles. **Every `job-end` carries the money, on every path**:
421
- `{ outcome, spentUsd, spendComplete }` (plus `step`/`cause`/`detail` where the outcome has
422
- them). `spentUsd` is the accumulated sum of PRICED rounds ONLY — never an estimate from
423
- token counts or averages (cap-not-estimate) — and it is stated even on the pre-token reds,
424
- where the honest figure is a real `0`. `spendComplete` says whether that figure is EXACT:
425
- `false` means one or more rounds came back unpriced (F6), so `spentUsd` is a FLOOR
426
- ("at least $X", true total unknowable) and must not be read as a total. Both fields are
427
- present on all outcomes, so a consumer never branches on field presence and never has to
428
- launder a missing `spentUsd` into `$0`. A text-mode job invoked without `opts.target` is a `job-red`
429
- before ANY provider call (and `interpret` itself throws a TypeError for direct
430
- callers) — reds-before-tokens applies to the call, not just the spec.
431
-
432
- **The plan flow (Layer 2).** A plan-shape spec routes through the SAME `runJob` entry —
433
- same approval gate, same smoke, same metered ledger, same job-end money contract
434
- (`job-start` carries `shape: 'plan'` + the goal instead of a steps list; no `opts.target`
435
- needed — plan steps are tool-mode by construction). The flow (`runPlan`, also exported
436
- for direct callers who own their own ledger): **close precheck** (`already-green` is a
437
- DISTINCT zero-token outcome; a forbidden-zone verdict escalates before spend) → **checks
438
- preflight** (every SIGNED check runs once at $0 — an unrunnable check is a `check-red`
439
- stop before tokens, not a fault mid-plan) → **SCOUT** (read-only by construction: the
440
- write verbs are not in its menu; hard-bounded rounds) → **PLAN** (the decompose call —
457
+ ### `runJob(spec, { approvals, workdir, provider, nativeProvider?, providerFor?, emit, capRuns?, strikeLimit?, shellCapUsd?, closeTimeoutMs?, layerRoot?, bridge?, priorSpentUsd?, priorSpendComplete?, priorWallMs?, resumeSeed?, resumeGrades? })` → outcome — `src/run.js`
458
+
459
+ The last four are the RESUME fold and are documented under *Resuming a killed run* below;
460
+ they default to `0` / `true` / `0` / `null`, so a fresh run passes none of them.
461
+
462
+ The runner — the shell's top layer, and the ONE entry. It composes everything below it and
463
+ interprets nothing itself. Sequence: **approval gate** (human-signs-always — refuses an
464
+ unapproved spec before ANY provider call: `unapproved-spec`) → **primitive smoke** (litectx
465
+ known-answer round-trip before tokens: `smoke-red` — a silent degradation throws nothing) →
466
+ **the plan flow** (`runPlan`, below) under the ONE cumulative ledger, whose ceiling is
467
+ `min(job budget − spent, shell cap)`.
468
+
469
+ Outcomes: `green | already-green | escalated | unapproved-spec | job-red | smoke-red |
470
+ plan-red | check-red | close-red | close-unsupported | recipe-stale | pricing-red |
471
+ provider-red | interpreter-red | cap-halt | wall-halt | step-stalled | step-red:<id>`. `provider-red` is a
472
+ transport throw or a worker round the API cut off mid-generation (`truncated:max_tokens`,
473
+ BA-6 — before which it laundered into a clean finish, F25): no verdict exists and the failed
474
+ round's spend is only partly known (F6). `cap-halt` is the wallet; `wall-halt` is the clock
475
+ (F64 — a timeout derived from the run's own deadline is a governance stop, never a transport
476
+ casualty). **`cap-halt` reaches you from the close-fix loop too, not only from a step:** the
477
+ shell spells exhaustion (strikes on a step, and — since v1.46 — strikes in the fix loop as
478
+ well, `capRuns` only while that loop's trend instrument is blind) and a money-gate halt with
479
+ the same category, so both the
480
+ step loop and the fix loop read the WALLET to tell them apart — a drained wallet is
481
+ `cap-halt` (the resume-to-cap checkpoint), exhaustion with money still on the table is
482
+ the designed `escalated` terminal ("the close is still red"). The fix worker's gate is built
483
+ with the wallet at its most drained, which is exactly where a money cut would otherwise
484
+ masquerade as a capability read (F45). A `cap-halt` from the wallet also emits the
485
+ decision-ready `money-halt` record described under the plan flow below — the kept verdict, the
486
+ trend and the three levers — so a consumer never has to reconstruct the stop from the outcome
487
+ name alone. `step-stalled` is the F66 stall fuse giving up: no
488
+ completed round for 5 minutes,
489
+ three times on one call — each stall silently abandons the hung call and reissues it
490
+ (self-heal first); only the third throws. Inside a step it is the THIRD replan trigger
491
+ (with `cap-halt` and `step-variance`); outside a step it escalates under its own name, never
492
+ laundered into `provider-red`, and carries `spendComplete:false` (an abandoned call may
493
+ already have been billed). **The reissue is wall-aware:** past the run's deadline the fuse does
494
+ not self-heal — self-heal is what a run does with time left — and it gives up as `wall-halt`,
495
+ not `step-stalled`, since a replan has nothing left to re-allocate. Every one of them is a
496
+ decision-ready escalation with a terminal `job-end`: the spine never dangles.
497
+
498
+ **Every `job-end` carries the money, on every path**: `{ outcome, spentUsd, spendComplete }`
499
+ (plus `step`/`cause`/`detail` where the outcome has them). `spentUsd` is the accumulated sum
500
+ of PRICED rounds ONLY — never an estimate from token counts or averages (cap-not-estimate) —
501
+ and it is stated even on the pre-token reds, where the honest figure is a real `0`.
502
+ `spendComplete` says whether that figure is EXACT; `false` means `spentUsd` is a FLOOR ("at
503
+ least $X", true total unknowable) and must not be read as a total. Four things set it, all
504
+ the same class — a call whose cost is unknowable from here: a round that came back unpriced
505
+ (F6); a run that ABSORBED a stall, because the fuse's silent reissue may pay twice for one
506
+ answer and the flag is one-way whatever the outcome; a `wall-halt` that cut a call
507
+ mid-flight (`cutMidCall`); and a RESUME whose fold was itself a floor
508
+ (`priorSpendComplete: false` — an unknown does not heal by being carried forward, so every
509
+ round of this attempt being priced repairs nothing about the one before it). A wall stop read BETWEEN iterations or steps has no call in flight,
510
+ so it stays exact. Both fields are present on all outcomes, so a consumer never branches on
511
+ field presence and never has to launder a missing `spentUsd` into `$0`.
512
+
513
+ **The plan flow (Layer 2).** `job-start` carries `shape: 'plan'` + the goal; plan steps are
514
+ tool-mode by construction. The flow (`runPlan`, also exported for direct callers who own
515
+ their own ledger): **close precheck** (`already-green` is a
516
+ DISTINCT zero-token outcome; a forbidden-zone verdict escalates before spend) → **check-menu
517
+ preflight** (`check-menu` names the derived menu, then every OFFERED stage's chain runs once
518
+ at $0 — an unrunnable stage is a `check-red` stop before tokens, not a fault mid-plan) →
519
+ **SCOUT** (read-only by construction: neither the
520
+ write-class nor the store-class verbs are in its menu; hard-bounded rounds) → **PLAN** (the decompose call —
441
521
  the planner never sees the repo, only the scout blob; drafted against a schema
442
522
  description with check NAMES only; `validatePlan` gates it, one redraft with the reds
443
523
  fed back, then `plan-red`) → **EXECUTE** (strictly sequential micro-loops: `ralph` with
444
- the exit-evaluator judge; tree snapshots at step start; the gap names every failing
524
+ the exit-evaluator judge, each step under its own STRIKE LADDER — below; tree snapshots at
525
+ step start; the gap names every failing
445
526
  wall — mechanical genre, F46's measured mechanism; artifacts feed forward labeled by
446
- step id) → **ONE replan**, triggered by exhaustion only (an instrument stop never
527
+ step id) → **ONE replan**, triggered by exhaustion OR variance (an instrument stop never
447
528
  replans) → **the operator's close**, a red feeding ONE bounded fix loop judged by the
448
- REAL close. `plan-executed` (the plan-as-executed record, design law #2) lands on the
529
+ REAL close and governed by the close TREND rule below (`capRuns`, default 3, survives only as
530
+ its blind fallback). `plan-executed` (the plan-as-executed record, design law #2) lands on the
449
531
  spine on every path that executed steps. Additional outcomes: `already-green |
450
- plan-red | check-red | close-red`. Worker prompts hold the v1.12 §5 contract
532
+ plan-red | check-red | close-red | wall-halt`. Worker prompts hold the v1.12 §5 contract
451
533
  (mutation-proven): the absolute repo root, the step's action/target, prior artifacts,
452
534
  the gap — NEVER the budget, the close command, a check's command, or the arbiter's books.
453
535
 
536
+ **The strike ladder — how a STEP ends (F77–F79).** A plan step is **not** bounded by a
537
+ number of iterations. It runs until it stops making PROGRESS: a *strike* is a red iteration
538
+ that repeats an already-seen gap or wrote nothing, and `strikeLimit` strikes (shell option on
539
+ `runJob`/`runPlan`, **default 2**) end the step. Concretely — repeats are matched against a
540
+ **seen-set** for the whole step, never just the last gap (an A→B→A oscillator is real and
541
+ last-only reads it as progress); the gap is normalized for COMPARISON only (TAP `duration_ms`
542
+ lines, ISO stamps and `ms` figures dropped so a re-run is byte-stable — error counts, file
543
+ names and line numbers all SURVIVE, so `30 errors` → `22 errors` is progress and not a
544
+ repeat), and the gap the worker sees is never rewritten; "wrote" is the **count delta of the
545
+ gate audit's allow-decision write/edit records** (the F32 instrument — never `git status`,
546
+ never a tree diff, and never the path SET, which is constant after iteration 1 when a step
547
+ rewrites one target); strikes are **sticky** — a good iteration in between never repays one.
548
+ Everything else that bounds a step is unchanged and each still ends it on its own authority:
549
+ the wallet, the wall, the variance meter and the stall fuse. The ceiling is **arbiter
550
+ territory** — the plan cannot express it, tighten it or raise it — and the practical
551
+ consequence for a caller is that a **converging step now runs on the money and wall you
552
+ signed** instead of stopping at a count, so size those two numbers for the job (the ladder
553
+ adds no third ceiling of its own). The exhaustion terminal is the same `cap-halt` category
554
+ as before; the spine gains a per-iteration `ladder` record
555
+ (`{iteration, strike, strikes, limit, wrote, repeatOf, distinctGaps}`), and the `cap-halt`,
556
+ escalation and wall-stop records carry `strikes`/`strikeLimit`/`distinctGaps` beside the
557
+ iteration count rather than a cap the step no longer has. **The replan brief
558
+ names the mechanism**: which shape ended the step (converging-cut / stalled-no-write /
559
+ repeated-gap, with the iteration numbers as evidence), the gap trajectory, and how much money
560
+ and wall the stop left unspent (time reported UNBOUNDED when no wall was set, never `0`). It
561
+ names no culprit file — F28 applies to the replan channel too.
562
+
563
+ **The close TREND rule — how the CLOSE-FIX LOOP ends (PRD v1.46 §4).** `capRuns` retired as
564
+ that loop's governor once its own $0 replay over every archived fix loop came back clean (0
565
+ greens harmed — all three historical fix-loop greens converted in ≤ 2 verdicts; 1 real waste
566
+ case caught, dead flat at 2 errors for 7 verdicts until the wall). It now stops on the same
567
+ 2-strike no-progress rule, read off a DIFFERENT signal: the close's own graded numbers, **per
568
+ stage** (`src/trend.js`). A stage's series is the first number on the first red-marked line of
569
+ its output, compared only against that stage's own BEST so far (never last-only, the same
570
+ oscillator reason the ladder keeps a seen-set); reaching a LATER stage than ever before is
571
+ progress too, since a staged close is first-red-wins. Two consecutive comparable readings with
572
+ nothing improving ends the loop, under the unchanged `cap-halt` terminal and the unchanged
573
+ `escalated` outcome. **Never across stages** — merging a staged close's axes into one series
574
+ reads a real see-saw (suppressions 12 → 5 while the type errors it re-exposed went 2 → 0) as a
575
+ regression, and that mistake is inexpressible here rather than avoided by care. A stage whose
576
+ output carries no number donates NOTHING — not a zero, not a strike (F6): while the instrument
577
+ cannot compare, `capRuns` is what bounds the loop. It bounds the **consecutive run of
578
+ unreadable gradings**, not the loop's opening — it lifts the moment a stage reports a
579
+ comparable number and **re-arms the moment one stops**, so a loop that reads, goes blind and
580
+ reads again is never bounded by it, while a loop that never compares anything binds on exactly
581
+ the iteration the fixed count always bought it (F84: gating it on "has this loop ever compared
582
+ anything" made a single stage ADVANCE disarm it for the rest of the run, and a numberless-red
583
+ close then had money and the wall for a bound). Two known accepted limits, both stated at the
584
+ site: (1) "lower is better" is not derivable from prose, so a floor-shaped stage (`N tests
585
+ executed, below the seed's M`) reads as not-improving rather than converging — the fail-safe
586
+ direction, since a false "flat" costs one conservative stop and a false "converging" costs a
587
+ top-up spent on a dead run; (2) a series is one bucket per stage NAME, and a stage name is not
588
+ an axis — a close free to red one stage on two different populations (`N error(s) in <scope>`
589
+ vs `the target files are clean but M error(s) exist outside them`) donates both genres to one
590
+ series, and a run crossing that seam can read converging on work that only swapped which wall
591
+ it is behind. Neither is sharpened by teaching the reader to tell prose shapes apart (the F49
592
+ precedent); the second's root fix is a stage split in the close, which is the spec's to change.
593
+ The spine gains a per-iteration `ladder` record here too; every reading names its `governor`
594
+ (`step-ladder` | `close-trend`) so two instruments under one event type can never be averaged
595
+ into one number.
596
+
597
+ **A MONEY halt is decision-ready (PRD v1.46 §2).** A cut by the wallet is the same KIND of
598
+ stop the wall is, so it now reads out the same way: the verdict already minted is KEPT (never
599
+ discarded, never re-derived), and a `money-halt` spine record carries `budgetUsd`,
600
+ `remainingUsd` **with its own `spendComplete`** (the remaining is `budget − spent`, so on a run
601
+ whose spend is a FLOOR it is a CEILING and says so rather than reading as the number — the same
602
+ duty `wall-halt` has carried since W-2; it is the ledger's one figure, never re-derived here),
603
+ the kept `verdict`/`stage`, the run's own per-stage `trend`
604
+ (`converging | flat | unknown`) with the `reading` and `series` it judged, and three
605
+ `options` — top up and resume, revise the spec, abandon. Emitted at every site that cuts a run
606
+ on money (the step loop, the close-fix loop, and the scout/draft relay), exactly as
607
+ `wall-halt` is. The trend obeys the accuracy law above and says `unknown` out loud when the
608
+ close rendered no number (F6). **The library only ever REPORTS**: `budgetUsd` is in the spec
609
+ hash, so a top-up is a spec edit a human signs — nothing in a run may widen its own budget.
610
+
611
+ **ONE progress instrument, read by both halts.** The wall halt used to run its own: byte
612
+ equality of the last two close gaps, reading `stalled` / `moving` / `unknown` beside the money
613
+ halt's `flat` / `converging` / `unknown` on the same run. Both now read `runTrend`, the run's
614
+ own `src/trend.js` reader. Where a stage reported a comparable NUMBER it decides; where none
615
+ did, the byte comparison survives INSIDE `unknown` as a separate `motion` field
616
+ (`changed | unchanged | null`) and as a clause in the `reading` — never as the headline
617
+ verdict, because "the close's output changed" is not "the run got better" (F6 in a trend's
618
+ coat), and never mapped onto `trend`, because that would put the promotion on the spine.
619
+ `unchanged` points the lever at the goal; `changed` points it at reading the last close output.
620
+ The two readers stay SEPARATE INSTANCES for separate questions (`src/trend.js`, "ONE PER
621
+ SERIES"): the halt readouts ask *was the RUN converging when its allowance cut it*, the
622
+ close-fix loop's governor asks *is THIS loop out of ideas*.
623
+
624
+ **A resumed leg's readout judges the whole CHAIN.** `readResume`'s `restart.grades` carries the
625
+ dead leg's recorded close grades forward — `[{stage, value}]`, counts and stage names only,
626
+ never a gap byte — and `runJob`/`runPlan` take them as `resumeGrades`, which seed the run
627
+ trend's BASELINES (`createTrend({seed})`). Without them a resumed leg restarts the trend at its
628
+ own first close and can report `flat` — *revise the goal* — on a run that was converging when
629
+ its allowance ran out. The seed folds into the series and the per-stage best and into nothing
630
+ else: no iteration counted, no strike minted, no `comparableEver`, no stage position — it is
631
+ history, not this leg's evidence, so the blind-cap backstop still governs a leg whose own
632
+ readings never compare. The close-fix loop's governor is deliberately NOT seeded (it is the
633
+ leg-local question). Sources, in order: the `ladder` records the live governor already wrote
634
+ (`governor: 'close-trend'`, taken verbatim), the `close-precheck`/`outer-close` records, and —
635
+ only on a spine predating that governor — the `close-verdict` records after the `fix-loop`
636
+ marker, which is the same population and excludes a STEP's failing exit. Known limit: one spine
637
+ is read, so a resume of a resume inherits the previous leg's chain and not the one before it.
638
+
639
+ **Time and materials (T + A, PRD v1.27/v1.29).** `runPlan` starts ONE wall clock per run
640
+ (`createClock`, `src/clock.js`) from the signed `maxWallMs` and emits `wall-clock` with the
641
+ requested AND enforced numbers up front, plus the `closeStages` count that explains the gap
642
+ between them (`enforcedMs = maxWallMs + closeStages × closeTimeoutMs`, one expression — the
643
+ stage count comes from `stageClose`, the same staging the runner executes). Enforcement is a
644
+ **between-round deadline** — the only seam that exists, since `loop.stop()` cannot cut an
645
+ in-flight call (F61: fired at 500ms,
646
+ returned at 4,018ms) — so an attempt that crosses the deadline mid-flight emits `wall-bounded`,
647
+ is judged, and feeds its gap forward exactly like a round-bounded attempt. A call that never
648
+ returns is bounded by the two per-call numbers the same clock derives and `runPlan` passes to
649
+ every provider call: `timeoutMs` (BA-18, idle — reset by every byte) and `deadlineMs` (BA-19,
650
+ TOTAL duration — reset by nothing, so it is the bound for a stream that trickles forever, F66's
651
+ 274-minute call). Both are the wall's own remainder read at call time, and `deadlineMs` is
652
+ **omitted entirely** on a time-unbounded run — no wall, no derived ceiling (F45). Because both
653
+ came from the wall, a trip past the cap routes `wall-halt`, never `provider-red`; the
654
+ discriminator is `clock.expired()` and never the error code, so a timeout with time still on the
655
+ clock stays the transport casualty it is (F64). **The close is never
656
+ bounded by the wall and always runs to completion** — a deadline that kills grading leaves the
657
+ run unreadable after the money is spent (the F45 class, money generalized to time); what the
658
+ wall stops is the START of new work. Three sites decide the run-level terminal `wall-halt`, all
659
+ on the same clock: the step loop after a step returns, a step that would BEGIN already expired,
660
+ and the close-fix loop before it opens another iteration — which then stops on the verdict the
661
+ last close already minted (hamr: *"when time is up, keep the grade we already have and stop"*),
662
+ carrying that verdict, the iterations spent, and the SAME progress reading the money halt
663
+ carries (`trend`: `converging | flat | unknown`, plus `motion`) so the human can pick between
664
+ raising `maxWallMs` (resume-to-cap: the stop IS the checkpoint) and revising the goal — both
665
+ spec edits, both a new hash to re-sign. `cutMidCall` on the `wall-halt` record splits the deadline seen INSIDE a
666
+ provider call from the deadline read between them. Time reporting is
667
+ licensed at ~90% accuracy by ruling: imprecision is fine, reporting an unknown or unbounded
668
+ duration as `0` never is (F6 extended to time — unbounded reports `null`, never `Infinity`).
669
+ The planner (never the worker) receives a **materials** block at draft and at replan —
670
+ `{ balanceUsd, remainingMs }`, what is LEFT as a balance, never a rate: F57 measured a 150×
671
+ spread on verification-gap duration, so a per-round constant describes almost no real round
672
+ and a rate framing creates a rush-or-fake incentive. The replan trigger gains a second axis
673
+ (`step-variance`): a step consuming ≥ `VARIANCE_THRESHOLD` (0.5) of the run's REMAINING money
674
+ or time with its exits unmoved is pre-empted at the head of its next attempt so the planner
675
+ re-allocates instead. **Known and recorded (F63): that trigger fired 0 times across 18
676
+ archived spines / 54 steps** (near misses 0.35–0.45) — the threshold is arbiter territory,
677
+ deliberately not fitted to those points. The ONE-replan ceiling is unchanged.
678
+
679
+ **Per-step model tiers — `providerFor` (P).** A plan step may declare a `model` TIER
680
+ (`sonnet` \| `haiku`, `STEP_MODELS`). The tier menu is signed into the schema; mapping a tier
681
+ onto a real model (and any per-model provider params) is the RUNNER's territory, so both
682
+ `runJob` and `runPlan` take `providerFor?: (tier: string) => provider`. It is called ONLY for
683
+ a step that declared a tier, once per such step — the scout, the plan drafter and every
684
+ untiered step stay on `opts.provider`, so a caller that never plans tiers can omit it
685
+ entirely. Cache per tier on your side if you want one instance reused. **A plan naming a tier
686
+ when no factory was supplied is an `interpreter-red` STOP**, named as a wiring gap rather than
687
+ a plan defect: running the default tier as if the choice had been honoured is the F50
688
+ blind-instrument class, and the whole point of the field is that the choice is observable.
689
+
454
690
  **Two worker surfaces — API and native clipipe (BA-16, module 4d).** The plan flow is
455
691
  provider-agnostic: the close, the checks, and the exit evaluator are commands and form
456
692
  checks, so ONLY the worker differs. `job.provider === 'clipipe-subscription'` drives tools
@@ -472,17 +708,14 @@ per-attempt bound and surfaces `max_turns` as a BOUNDED attempt (judged, gap fed
472
708
  the native analog of `loop.stop()`. Live-validated green end-to-end on the real CLI; the
473
709
  Loop path is untouched.
474
710
 
475
- **Resume-to-cap (close-first skip):** every predicate step runs its close FIRST, before
476
- any tokens (`close-precheck` on the spine, output scrubbed at capture like every close).
477
- Already-green skips the step for zero tokens as a DISTINCT record — `step-end` outcome
478
- `already-green`, never plain `green`: nothing was done, so it mints no learning credit
479
- and runs no on-green retention. A close that cannot RUN is a `broken-close` escalation
480
- before any provider call. **Config drafting is deferred** to the first step that
481
- actually needs a worker — the sealed one-shot + one-redraft is unchanged, and a config
482
- is always drafted fresh per run (never inherited), but a rerun whose closes all green
483
- pays ZERO provider calls. So the resume story is: a `cap-halt` stop is the checkpoint
484
- (the workdir + the closes), the human raises `budgetUsd` (a new spec hash — re-sign),
485
- and the rerun picks up exactly where the budget died.
711
+ **Resume-to-cap (close-first):** the job's close runs FIRST, before any tokens
712
+ (`close-precheck` on the spine, output scrubbed at capture like every close). Already-green
713
+ is a DISTINCT zero-token outcome, never plain `green`: nothing was done, so it mints no
714
+ learning credit and runs no on-green retention. A close that cannot RUN is a `broken-close`
715
+ escalation before any provider call. So the resume story is: a `cap-halt` or `wall-halt`
716
+ stop IS the checkpoint (the workdir + the close), the human raises `budgetUsd` or
717
+ `maxWallMs` (a new spec hash — re-sign), and the rerun picks up where the budget died,
718
+ paying zero provider calls for anything already green.
486
719
 
487
720
  **Unpriced is never free (F6/F12):** the ledger meters **per ROUND** (`worker-round`, at
488
721
  bare-agent's `onLlmResult` seam) — money is counted as it is spent, never at the end of an
@@ -490,19 +723,11 @@ attempt: a tool-mode attempt that HALTS never returns, so accounting its result
490
723
  the whole attempt's spend (a real run bought $1.4375 of tokens and the ledger reported
491
724
  $0.0048). Any round whose cost is the honest null halts `pricing-red`, decision-ready; a
492
725
  null never accumulates as $0, so the hard cap cannot be gamed by an unpriced provider path.
493
- The drafting call is metered on its own priced path, and the shell **reserves a drafting
494
- allowance** from the job budget (F9): the ceiling advertised to the drafter is
495
- `(budgetUsd − reserve) ÷ predicate steps`, so a drafter that claims the ceiling it is given
496
- always validates (the prompt and the validator must never enforce different numbers).
497
-
498
- The **÷ steps** is F40: the config is drafted once and re-validated at every step against
499
- what is LEFT, so a ceiling sized to the whole pot goes stale the moment step 1 spends and
500
- the next step reds `bounds` on an unchanged config with money still available. A per-step
501
- share still fits after its predecessors have spent (the shares sum to `budgetUsd − reserve`).
502
- Single-predicate-step jobs are arithmetically unchanged. This is a DRAFTING bound, not an
503
- enforcement one: cap-not-estimate is intact, and a step that genuinely needs more than its
504
- share still cap-halts cleanly with a resume point rather than silently borrowing from the
505
- steps after it.
726
+ The plan and scout calls are metered on that same priced path — a drafting call is a
727
+ provider call and is never accounted around the ledger (F6). The scout runs under its own
728
+ reserved round bound, with the LAST round reserved for a toolless summary: the bound halts
729
+ a scout mid-tool-use, and text is its only deliverable, so without that reservation it
730
+ spends its whole allowance exploring and reports nothing (F59 — measured on 15 of 18 runs).
506
731
 
507
732
  **What the worker is told (tool mode):** the absolute repository root (F10 — bare-agent's
508
733
  shell tools resolve relative paths against the PROCESS cwd, so a worker with no root is
@@ -513,14 +738,335 @@ re-run with the close's verdict; without this a model one-shots and can eat the
513
738
  reads before ever writing). It is NOT allowed to read the run's own machinery (F14): the gate
514
739
  audit, the smoke store and the litectx store are denied — the agent neither authors the
515
740
  arbiter nor reads its books. N2 bounds (honest): `gold`/`rubric` closes refuse
516
- `close-unsupported` (execution lands at N4); `target` is required only for text-mode
517
- steps.
518
-
519
- ### `extractRules({ config, provider, priorRules, revisionDiff? })` — `src/extract.js`
520
-
521
- One sealed LLM call distilling a lineage's rules from ledger facts after a green run.
522
- `MAX_RULES`/`MAX_RULE_CHARS` bounds enforced mechanically post-call, rejected whole.
523
- Malformed output is a red as data (`rules: null`) — the caller keeps prior rules.
741
+ `close-unsupported` (execution lands at N4).
742
+
743
+ ### The reuse registry (Layer 3) — `src/bridges.js`, `src/selection.js`
744
+
745
+ A **bridge** is the plan a green actually executed, kept so the next run of the same SHAPE
746
+ starts from it instead of cold. Storage is a directory of plain JSON files at an
747
+ **operator-supplied path** — no database, no default location (a missing registry reds
748
+ rather than being conjured). Everything here is either a pure derivation or an explicit
749
+ read/write of one file; nothing decides a run.
750
+
751
+ **Writing the box (R1 — only a green writes it).** `mintBridge(meta, record)` builds a new
752
+ entry from a FIRST green — there is deliberately no way to create one without a green, so a
753
+ failed plan cannot enter the registry even by accident. `appendGreen(bridge, record)` adds
754
+ the plan AS EXECUTED as the next **version** plus a history row; `appendRed(bridge, record)`
755
+ writes a **history row only** and never touches `versions` — a red demotes, it does not edit
756
+ the recipe. Both are pure (a new entry is returned; the input is untouched).
757
+ `saveBridge(dir, bridge)` writes atomically (temp + rename) and REFUSES an entry its own
758
+ validator rejects; `loadBridge` / `loadRegistry` read back, and a malformed file is
759
+ **skipped and reported** (`ok:false` with one red per bad file) rather than silently
760
+ dropped. `makeRegistry` is separate from `saveBridge` on purpose: saving must never conjure
761
+ a registry from a typo'd path.
762
+
763
+ **Status is DERIVED, never stored** (`deriveStatus`): **candidate** = one green, **proven** =
764
+ greens on ≥2 DISTINCT patients, and a `red` on a proven entry drops it back to candidate.
765
+ A **casualty is not a red** — only the literal outcome `red` demotes; `provider-red`,
766
+ `wall-halt`, `close-crashed` and friends are casualties and are never evidence in either
767
+ direction. There is deliberately **no probability score**: n=1 is an anecdote whichever way
768
+ it points, and a percentage over 2–3 runs is fake precision. `costUsd`/`wallMs`/`rounds` are
769
+ a number or an EXPLICIT `null`, and the key is REQUIRED either way, so an unknown has to be
770
+ said rather than omitted (F6 — an omitted key is how a `?? 0` gets written downstream).
771
+
772
+ **`loadGate(bridge, job)` → `{ ok, reds }` — the load-time gate.** Exactly three checks,
773
+ asked *"is this the right KIND of recipe?"*: the job's **verdict type**, the **close-stage
774
+ kinds** (v1: the same stage names in the same order), and every stored verb **within THIS
775
+ job's signed menu** (an omitted `tools` means the concrete current `TOOL_MENU`, MED-1).
776
+ **Nothing about paths, scopes or targets** — those are instance-bound and are EXPECTED to
777
+ red: every recipe from a different day names yesterday's bricks, and that is what a recipe
778
+ IS. The full `validatePlan` still judges the TWEAKED draft at draft time; no second, looser
779
+ path exists for an inherited plan. A failing gate is a RESULT (`recipe-stale`), never a throw.
780
+
781
+ **A CHANGED CLOSE therefore expires every bridge minted under the old one.** The stored
782
+ `closeStageNames` must match the job's stage names in the same order, so adding, renaming,
783
+ splitting or reordering a stage makes existing entries fail the gate by construction. That is
784
+ the doctrine, not a rough edge: a different close is a different KIND of job, and a recipe
785
+ proven against one ruler has not been proven against another. The remedy is a fresh green under
786
+ the new close, which mints the next version — an entry is **never edited into agreement**, and
787
+ falling back to cold stays the caller's decision. In this repo the `u-*` close split
788
+ (2026-08-05) put `aurora-u-spawner` and `litectx-u` in exactly that state; both are re-minted by
789
+ their next green, not repaired.
790
+
791
+ **`runJob`/`runPlan` take `bridge?`** — a validated entry to reuse. The gate runs **at the
792
+ door**: before the clock, before the close precheck, before any token. On a pass, the
793
+ **newest** version's plan rides into the FIRST drafting prompt as a starting draft
794
+ (`bridge-loaded {name, versions, runid}` on the spine) and everything after it is the
795
+ ordinary shipped path — same validator, same redraft-on-reds, same ONE-replan ceiling, and a
796
+ **replan never re-injects the bridge** (it drafts from the run's own state). On a fail the
797
+ run returns the distinct terminal **`recipe-stale`** with `bridge-gate {outcome, name, reds}`
798
+ and a decision-ready escalation, having spent nothing. **Falling back to cold is the
799
+ caller's decision, not an automatic silent fallback** — starting a paid run on a decision
800
+ nobody made is the same class of error as widening a cap to manufacture a green. Omit
801
+ `bridge` and the flow is byte-identical to a pre-Layer-3 run: no new event, and no starting
802
+ draft anywhere in the prompt.
803
+
804
+ **`renderListing(registry)` / `selectionPrompt(listing, ask)` — `src/selection.js`.** Pure
805
+ text: they read no file, call no model and decide nothing. `renderListing` takes
806
+ `loadRegistry`'s result (or a bare array) and renders one compact block per bridge — name,
807
+ the goal sentence it greened, status, greens/reds with the last outcome, and the cost/time
808
+ BAND of its greens. An unknown cost or duration renders `UNKNOWN` and a partial aggregate
809
+ says how many it skipped (never a `$0.00` that reads as exact); entries that could not be
810
+ read are NAMED, so the listing can never be quietly shorter than the directory. Ordering is
811
+ deterministic by name and it never throws. `selectionPrompt` wraps that listing with the ask
812
+ and D3's two standing rules — *"none matches"* is a first-class answer that means the agent
813
+ drafts new, and a PINNED workflow may be refused only EXPLICITLY, with a reason. The
814
+ selection CALL, the pin/shortlist/force-cold flow and the parse of the answer are YOURS —
815
+ or `runReuse` below, which is the shipped one.
816
+
817
+ ### The reuse ENVELOPE and `runReuse` (Layer 3, D7) — `src/reuse.js`
818
+
819
+ `runReuse` is `runJob` under an operator-signed **envelope**: try a stored workflow, then
820
+ another, then draft cold — hamr's *"$5 and 30 mins x2 then start anew"*. It composes; it
821
+ decides nothing the arbiter owns.
822
+
823
+ **`validateEnvelope(input, { job? })` → `{ ok, reds, envelope }`.** The envelope is
824
+ `{ perTryBudgetUsd, perTryWallMs, bridgeTries }` and **all three are required and
825
+ explicit — there is no default for any of them**, for `maxWallMs`'s reason: a defaulted
826
+ cap is a silent second ceiling. `bridgeTries: 0` is legal and means force-cold. With a
827
+ `job`, the composition is checked: the per-try numbers may only **TIGHTEN** the signed
828
+ spec — a per-try budget above `job.budgetUsd`, or a wall above `job.maxWallMs`, is
829
+ `envelope-widens`, never a silent raise. (A spec with no `maxWallMs` is time-unbounded by
830
+ explicit choice, so any wall tightens it.)
831
+
832
+ **`resolveTrySpec(job, envelope)` → the per-try spec.** Every try — cold leg included —
833
+ runs THIS spec, so the envelope's numbers are the numbers all the way down. Tightening
834
+ makes a genuinely different spec, so it makes a different **spec hash**: the tightened run
835
+ is a new spec VERSION and `runJob`'s approval gate refuses it until the operator signs
836
+ that hash. `runReuse` hands `approvals` straight through and has no way to forge one. An
837
+ envelope equal to the spec's own numbers leaves this spec hash-identical.
838
+
839
+ **`resolveReuse(job, envelope)` → `{ schema: 'reuse-v1', spec, bridgeTries }`, and
840
+ `reuseSpecHash(resolved)` → the hash your operator signs. Show THIS one at your approval
841
+ gate** (`scripts/run-reuse.mjs` does). The envelope is three numbers and the third is a
842
+ MULTIPLIER — the worst case a reuse run authorizes is
843
+ `perTryBudgetUsd × (bridgeTries + 1)` — so a signature over the per-try spec alone covers
844
+ only budget and wall, and `--tries 0` and `--tries 9` print the same hash. The signed
845
+ artifact is therefore the per-try spec WRAPPED with the count. It is a wrapper rather than
846
+ a field on the spec because `validateJob` reds unknown top-level fields: a `bridgeTries`
847
+ written onto the spec would flip the hash and then make every try a `job-red`. The hash is
848
+ composed from `jobSpecHash` (so MED-1's resolved-`tools` pinning is inherited, with no
849
+ second canonicalizer to drift) under a `reuse-v1` domain prefix, and `runReuse` reads its
850
+ loop bound back off the same resolved object — the count enforced is the count signed.
851
+ **This is a signing-SCHEME change: any hash signed before the count was folded in no
852
+ longer matches, by design, and there is no legacy acceptance path — re-sign once.** Your
853
+ gate still mints `approvals` for `runJob` at the PER-TRY SPEC's `jobSpecHash`, which the
854
+ matching approval hash entails (it is a function of exactly that hash plus the count).
855
+
856
+ **`selectBridge({ registry, job, ask, provider, pinned?, shortlist?, forceCold?, exclude? })`.**
857
+ ONE model call on the drafter-tier provider **you** hand in (no provider is constructed
858
+ inside), rendering `renderListing` + `selectionPrompt` and parsing a strict
859
+ `{"choice": <name|null>, "reason": "…"}` through the repo's one `extractArtifact`. Returns
860
+ `{ choice, reason, called, refused, forcedCold, red, costUsd, spendComplete, candidates }`.
861
+ `forceCold` and an empty candidate set skip the call entirely ($0, no tokens). A name that
862
+ is not on the listing it was given is a named red, never used. **A pin does not bypass the
863
+ call** (D3): it is stated in the prompt, and if the answer names anything else the result
864
+ is `{refused: true}` — the substitute is NOT adopted and the decision goes back to you.
865
+ Cost is metered and reported: `costUsd` is a number or an explicit `null` (F6).
866
+
867
+ **`runReuse({ job, approvals, registryDir, envelope, patient, workdir, provider, emit, … })`.**
868
+ Envelope → registry → up to `bridgeTries` tries → the cold leg. Per try: selection runs
869
+ against a **freshly reloaded** registry with the already-tried names excluded, the chosen
870
+ entry rides into `runJob` (whose own load gate is the D2 shape check — a `recipe-stale`
871
+ refusal costs $0 and simply moves to the next candidate), and then **the box is written
872
+ (R1)**: a green appends a VERSION carrying the plan AS EXECUTED (read from the run's own
873
+ `plan-accepted`) plus a history row; a graded red appends a history row only; a casualty
874
+ appends a row under its **own** outcome name and cannot demote. **A green ends the loop.**
875
+ Tries exhausted → a cold run under the same per-try numbers, whose green mints a new
876
+ bridge named for the job slug (or appends, if that name already holds greens — clobbering
877
+ a green is exactly what R1 exists to prevent). A cold RED writes nothing: the entry bar is
878
+ a green.
879
+
880
+ **Which outcomes are a graded RED** (`REUSE_GRADED_RED`): only `escalated` — the terminal
881
+ where the close judged the tree, the bounded fix loop spent its attempts with money still
882
+ on the table, and the close was still red. Everything else non-green is a **casualty** and
883
+ keeps its own name: `cap-halt`/`wall-halt` (governance), `provider-red`/`step-stalled`
884
+ (transport), `close-red`/`close-unsupported` (the close FAULTED and rendered no judgment),
885
+ `recipe-stale` (refused at the door), `plan-red`/`step-red:*`/`check-red` (the flow stopped
886
+ before the close ever judged). All of them are recorded in full on the history row, which
887
+ is where D6 puts the detail a human reads; the coarse status ladder is left alone.
888
+
889
+ **Result:** `{ outcome, tries[], selection[], spentUsd, spendComplete, bridgeWrites[],
890
+ decision, options, detail, reds, triesUsed, triesAuthorized, envelope, specHash,
891
+ approvalHash }` — `specHash` is the per-try spec's (what `runJob`'s own gate reads),
892
+ `approvalHash` is the operator's, covering all three envelope numbers. Every
893
+ try row is **decision-ready**: `bridge`, `runOutcome`, `verdictClass`, `failingStage`,
894
+ `spentUsd` vs `capUsd`, `wallMs` vs `wallCapMs`, `capBound`, `wallBound`, `rounds`, and
895
+ `closeReached`. `spentUsd` is the sum of PRICED figures only across tries AND selection
896
+ calls; one unknown makes `spendComplete` false, and a try whose `job-end` never landed
897
+ reports `spentUsd: null` — never a `0` that reads as exact (F6). On a RESTARTED try,
898
+ `rounds` folds both attempts exactly as `spentUsd` and `wallMs` do (the row states one span,
899
+ not a count from one leg against money from two), and the fold is declared on `try-start` as
900
+ `priorRounds`; a leg whose turns cannot be counted leaves it `null` and the row keeps the
901
+ unknown rather than reporting the half it can see.
902
+
903
+ > **The selection calls sit OUTSIDE the per-try caps**, by construction: they happen
904
+ > before a try starts, so no try's ledger can hold them. They are small (one short prompt
905
+ > each, no tools) and they ARE metered — every one lands in `selection[].costUsd` and in
906
+ > the total — but a reader budgeting `perTryBudgetUsd × (bridgeTries + 1)` should know
907
+ > that is the caps' sum, not the run's ceiling. Named here rather than folded in: an
908
+ > advertised number the run does not enforce is the thing this repo does not ship.
909
+
910
+ > **F45, and why there is no threshold here.** A try's budget must fund **the attempt PLUS
911
+ > its close** — a cap that dies mid-grading produces an unreadable row, and an unreadable
912
+ > row is a casualty, not evidence. Nothing in this process can know what a close costs (it
913
+ > is your command, at your price), so **no threshold is invented** — threshold-setting is
914
+ > arbiter territory, set from a measured base rate, never fitted to the sample in hand.
915
+ > What the module does instead is make the bind VISIBLE: `closeReached: false` next to
916
+ > `capBound: true` on a row IS the diagnosis. Size the envelope with that in hand.
917
+
918
+ Fallthrough to the next bridge, and to cold, is automatic **only because the envelope
919
+ pre-authorized it**. Nothing else is: a refused pin (`selection-refused`), an unusable
920
+ selection answer (`selection-red`), a missing registry (`registry-red`) and an
921
+ envelope that will not compose (`envelope-red`) all STOP with a decision-ready escalation
922
+ and hand the call back to you. So do the three answers **no further try could change** —
923
+ `unapproved-spec` (a tightened envelope makes a new spec version, and it is signed, not
924
+ inherited: the decision hands you the exact hash), `job-red`, and `smoke-red` — which end
925
+ the run on the try that hit them rather than reproducing themselves down the whole
926
+ envelope. Every category this emits is in the ledger's executable excluded-set, so none of
927
+ them lands as an unclassified library bug.
928
+
929
+ `scripts/run-reuse.mjs` is the reference operator runner: `--job`, `--registry`, and the
930
+ three envelope numbers `--budget` / `--wall` / `--tries`, plus optional `--pin` /
931
+ `--force-cold`, with the approval gate on `reuseSpecHash` (all three numbers — a hash
932
+ signed for a different `--tries` is refused, and says so), the F67 outside watchdog
933
+ (sized for the SUM of the tries, not one), the seed refusal, the gate-audit relocation and
934
+ the secrets scan.
935
+
936
+ #### Resuming a killed run (module C)
937
+
938
+ A reuse run is up to `tries + 1` full jobs long, so a kill mid-run is a real event and it
939
+ must not cost the whole envelope. **`readResume(events, {deathAt?, direct?, resumableOutcomes?})`**
940
+ reads the dead run's own spine back into the state a resume continues from: `{ started,
941
+ approvalHash, completed[], tried[], restart (with its step-level `seed` and its close
942
+ `grades`), r1Missing, spentUsd,
943
+ spendComplete, carrySpentUsd, carrySpendComplete, ended, greened, … }`. Hand that object to
944
+ **`runReuse({…, resume})`** and the run picks up where it stopped.
945
+
946
+ Two options widen WHICH spines it reads, both OFF by default so the reuse loop's own
947
+ semantics are byte-unchanged (PRD v1.46 §3):
948
+
949
+ - **`direct: true`** reads a plain `runJob` spine — no envelope, no try windows — as ONE
950
+ implicit try opened at `job-start`, through the same window machinery rather than a second
951
+ reader. Without it such a spine is honestly refused (`started: false`), which is what a
952
+ reuse runner needs. `job-start` carries the DECLARED fold (`priorSpentUsd` with its
953
+ `priorSpendComplete`, and `priorWallMs`, each present only when there is one) exactly as
954
+ `try-start` does, so a chain of resumes adds only each attempt's own new rounds instead of
955
+ re-deriving and double-billing. **An inherited FLOOR is one** even when the money it qualifies
956
+ is `$0`: a leg whose every round came back unpriced folds nothing countable and is still not
957
+ exact, so the declaration is emitted to carry that unknown forward rather than letting it read
958
+ as an exact zero (F6).
959
+ - **`resumableOutcomes: ['cap-halt', 'wall-halt']`** reclassifies a landed `job-end` whose
960
+ outcome is a GOVERNANCE HALT as a restart rather than a completed row. "A landed job-end is
961
+ complete" is true of a VERDICT; a halt means an operator-owned allowance ran out with the
962
+ work on disk and the plan on the spine. A green or any red stays non-resumable under every
963
+ setting — a verdict already rendered is never re-bought.
964
+
965
+ `scripts/run-u.mjs --resume <runid|path>` is the operator-side consumer: it skips the patient
966
+ reset (`resumeTreeGate` instead — a dirty tree is what a resume expects; only a moved HEAD
967
+ stops it), folds the halted run's money and wall in, re-enters at the checkpoint, and arms the
968
+ outside watchdog on the REMAINING wall. The top-up itself stays a spec edit the human signs.
969
+ **A zero wall remainder REFUSES the launch** (exit 2, before the key and before the patient):
970
+ W-2 says a run out of time keeps the grade it has and stops, so a resume with no time to start
971
+ anything in is a decision, not an unbounded launch — and launching it used to hand the outside
972
+ watchdog `--wall-ms 0`, which it defaulted to `null` and armed no deadline at all under a banner
973
+ still advertising one. The refusal names the lever: raise `maxWallMs`, which moves the spec hash
974
+ and is re-signed.
975
+
976
+ - **Completed tries are not re-run.** They come back as rows (marked `inherited`) and
977
+ their bridges stay excluded from every later selection. A try whose `job-end` landed —
978
+ the close judged it — counts as completed even if the kill beat its `try-end`; paying
979
+ again for a verdict already rendered is exactly what resume exists to avoid. If the
980
+ kill also beat the registry write, that row is LOST and named (`r1Missing`); nothing is
981
+ invented in its place.
982
+ - **A mid-flight try RESTARTS** against the same workflow, with **no second selection
983
+ call** (that pick was made and paid for), and it is not counted as consumed.
984
+ - **It restarts at the STEP it died on, not at its beginning.** hamr's ruling: *"start
985
+ last step instead from the beginning, why would i want to waste more money on something
986
+ i already started"*. `restart.seed` is the finest checkpoint the dead spine can PROVE —
987
+ `{phase, plan, completedSteps[{id, seq, by}], planSeq}` — and it rides into `runJob`
988
+ (and on to `runPlan`) as `resumeSeed`:
989
+ - **scout: skipped** (`scout-skipped`, never silence). Its survey is not on the spine,
990
+ so the one consequence is stated rather than discovered: a replan after a resume
991
+ drafts from an empty survey plus its failure brief.
992
+ - **plan: reloaded, not re-drafted** — from the LAST `plan-accepted` in the window (a
993
+ replan emits its own, and the post-replan plan is the one that was running). It is
994
+ **re-validated** against the spec signed NOW; a plan that no longer validates is
995
+ `plan-red` by name (`resume-plan-red`) with nothing run. It is then re-emitted as this
996
+ leg's own `plan-accepted {phase:'resume'}`, because R1 mints a bridge version from the
997
+ plan AS EXECUTED read back off the spine.
998
+ - **completed steps: skipped**, in prefix order by id, each with its own `step-skipped`
999
+ record naming the event that proves it (`step-end{green}` — or an earlier
1000
+ `step-skipped`, so a resume of a resume does not re-pay for what the first one saved).
1001
+ Never a fake step-start/step-end pair; `plan-executed` records them as `skipped`.
1002
+ - **`phase: 'close'`** (an `outer-close` in the window) means every step is done: they
1003
+ are all skipped and the run goes straight to the close, which re-runs for no tokens.
1004
+ - A window with **no `plan-accepted`** (a scout/draft death) has no seed and simply
1005
+ restarts the try — nothing paid is re-payable there. If that window is itself an
1006
+ abandoned resume, the checkpoint is read from the newest abandoned window **of the
1007
+ same try** (measured on the real killed run: leg 3 died in its redraft, and reading
1008
+ only its own window would have discarded a checkpoint leg 2 paid $8.18 to reach).
1009
+ - The worker's conversation is **not** replayed — a transcript is not a checkpoint, and
1010
+ every attempt is fresh by the loop's own design.
1011
+ - **Its TREND baselines come with it.** `restart.grades` is the dead leg's close grades in
1012
+ order (`[{stage, value}]` — counts and stage names only, never a gap byte), and it rides
1013
+ into `runJob`/`runPlan` as `resumeGrades` beside `resumeSeed` — on the LIBRARY path
1014
+ (`runReuse` hands it to the restarted try itself) as well as through `run-u --resume`;
1015
+ `resume-start`'s `restart` states `gradesInherited`, a count, and only when there is one.
1016
+ It seeds the run trend's
1017
+ baselines so the resumed leg's `money-halt`/`wall-halt` readouts judge the CHAIN: a leg
1018
+ that re-grades an unchanged tree is flat on its own evidence while the RUN — what the
1019
+ top-up decision is actually about — may have been converging when the money ran out. See
1020
+ the trend section above for what the seed does and, deliberately, does not touch.
1021
+ - **Under the REMAINDER, never a fresh allotment.** The killed attempt's spend and wall
1022
+ FOLD IN (`runJob`'s `priorSpentUsd`/`priorSpendComplete`/`priorWallMs`, `createClock`'s
1023
+ `priorElapsedMs`), so
1024
+ the restart gets what is left of the signed per-try numbers. The caps are never
1025
+ rewritten — they are in the spec hash. An unfundable remainder is an honest `cap-halt`
1026
+ (or `wall-halt`, below one close timeout: a try that cannot fund its close produces an
1027
+ unreadable row) with **nothing launched**. Topping up is a new envelope, and a new
1028
+ envelope is a new signature.
1029
+ - **Money stays honest end to end.** `carrySpentUsd` (completed tries + selection calls)
1030
+ seeds the resumed ledger; the mid-try fold is deliberately NOT in it, because it comes
1031
+ back inside the restarted try's own terminal — counting it in both places would bill it
1032
+ twice. A try row's `spentUsd` is therefore the try's WHOLE spend across both attempts,
1033
+ and a declared floor stays a floor — through ALL four of `runJob`'s floor causes: a
1034
+ landed resumable `job-end` whose own `spendComplete` is `false` (a stall, or a call cut
1035
+ mid-flight) floors the restart fold exactly like a declared prior floor or an unpriced
1036
+ round does (F83's laundering finding, fixed the same day; the killed-mid-flight restart
1037
+ with no `job-end` at all is control-pinned unchanged).
1038
+ - **Refusals** (`resume-red`, in the ledger's excluded set): a seed signed under a
1039
+ DIFFERENT envelope than the one being enforced (both hashes are named), and a restart
1040
+ whose workflow has left the registry (substituting another would override a decision
1041
+ the runner did not make).
1042
+ - **The patient is CONTINUED, not reset** — `resumeTreeGate({head, seed, dirty})`: a dirty
1043
+ tree is the dead tries' real progress; only HEAD moving off the seed (a commit or rebase
1044
+ under the dead run) stops the resume.
1045
+ - **One run, one log:** the resumed process appends to the dead spine and passes
1046
+ `makeSpine(file, {startSeq})` so `seq` stays monotonic, keeps the dead run's runid (the
1047
+ R1 rows point at a spine FILE), and leaves the cumulative `gate-audit.jsonl` in the
1048
+ workdir to be relocated once at the end as usual.
1049
+ - **Known limit (F6):** a call killed BEFORE it returned wrote no event, so money it may
1050
+ already have been billed for is not on the spine and cannot be folded — the same limit
1051
+ every guard here has (a hung `generate()` leaves zero trace).
1052
+
1053
+ `scripts/run-reuse.mjs --resume <runid|spine path>` is the operator seam: it refuses a
1054
+ spine that is missing, corrupt mid-file, already terminal, or whose process is STILL
1055
+ ALIVE (pids from the run's `runner-start` record and the watchdog report, discriminated
1056
+ by `/proc/<pid>/cmdline`, because a live pid can be a recycled stranger), prints the whole
1057
+ reconstruction as a preview before you sign — including WHERE it picks up ("step 2 of 3
1058
+ \"verify-strict-typecheck\" — 1 step already finished and SKIPPED, not re-paid"), so the
1059
+ signature covers an attempt whose real cost you can see — archives the killed run's
1060
+ watchdog record so the readout cannot mistake it for this run's (defensively: a truncated
1061
+ report is not evidence of anything, and it can no longer abort a signed resume), and sizes the
1062
+ outside watchdog for the work that is actually LEFT. **Nothing left to run REFUSES the launch**
1063
+ (exit 2), with a lever per cause, because they are different asks: `NO WALL LEFT` when the
1064
+ restarted try burned its whole per-try wall (raise `--wall`), `NOTHING TO ARM` when every
1065
+ authorized attempt has already run (raise `--tries`, or read the run as it stands). Both are in
1066
+ the approval hash, so either is a new signature. Two smaller refusals in the same family: a flag
1067
+ given no value (`--tries` with nothing after it) stops rather than reading `Number('') === 0`
1068
+ and reshaping the run being signed, and `runner-start`'s recorded `argv` is redacted at the
1069
+ write site — the spine is append-only, so a scan after the bytes land is too late.
524
1070
 
525
1071
  ### `updateLedger({ ledgerFile, spineFiles })` → `{ appended, fold }` — `src/ledger.js`
526
1072
 
@@ -537,9 +1083,14 @@ failures can't derive), `runtime-red`, `provider-red`, `pricing-red` (F6), possi
537
1083
  `capability-gap` (cap-halt + request-red in one spine), `broken-close` (consumer-attributed),
538
1084
  `request-red` (admission demand for a locked verb — keyed on the red's structured
539
1085
  `verb` field, prose-quoted verb as legacy fallback), `retention-red`, `config-red`
540
- (drafting friction — attributed to bareloop's own schema/prompt). Deliberate exclusions:
541
- bare `cap-halt` (a budget story), `close-verdict`/`artifact-red` (worker stories),
542
- `gate-red` (governance working as intended), `pr-red` (operator environment).
1086
+ (drafting friction — attributed to bareloop's own schema/prompt). Deliberate exclusions
1087
+ (`EXCLUDED_ESCALATIONS`, a runtime set — anything outside classified ∪ excluded is
1088
+ counted as unmapped, never dropped): `cap-halt`/`wall-halt` (budget stories, money and
1089
+ time), `step-stalled` (the stall fuse firing is our governance, not an observed provider
1090
+ failure), `step-variance` (a planning story), `gate-red`/`smoke-red` (governance working
1091
+ as intended / already counted), `hitl-close`/`close-unsupported` (by design),
1092
+ `close-timeout`/`close-killed`/`close-crashed` (the arbiter's own named terminals, F17);
1093
+ `close-verdict`/`artifact-red` stay worker stories, `pr-red` operator environment.
543
1094
  `suggestedAsk` on every row is a template seed for an upstream ask — filing stays human;
544
1095
  status rows (`open → filed → fixed → consumed`) are human-appended, and the fold shows
545
1096
  the latest per key. Pure pieces exported for custom folds: `classifyIncidents(events,
@@ -549,9 +1100,12 @@ panel reads the same file at N6.
549
1100
  ## Architecture
550
1101
 
551
1102
  Three layers. An **outer shell** (dumb, permanent): per-run budget cap via bareguard,
552
- retry cap, verdict collection, escalation routing — stateless across runs; nothing inside
553
- negotiates with it. An **emergent middle**: the authored workflow config — steps, per-step
554
- verdict class, memory binding, write scopes — schema-validated. A **floor**: append-only
1103
+ retry cap (a count on the close-fix loop, a shell-owned STRIKE ceiling on a plan step),
1104
+ verdict collection, escalation routing — stateless across runs; nothing inside
1105
+ negotiates with it. An **emergent middle**: the AGENT-authored plan — bounded steps, each
1106
+ with its granted verbs, its round cap, its narrowed write scope and its
1107
+ form-checkable exits — schema-validated by `validatePlan` before tokens burn (the
1108
+ operator-authored `steps[]` shape is deleted, PRD v1.32). A **floor**: append-only
555
1109
  JSONL spine (single source for every UI), litectx store per job, per-run ledger. Built on
556
1110
  the bare suite: bareagent, bareguard, litectx, barebrowse, baremobile — the full surface
557
1111
  is disclosed to the authoring agent; only admitted verbs are callable per job.