bareloop 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,392 @@
1
+ /**
2
+ * Validate the reuse envelope — the three numbers the operator signs before the run.
3
+ *
4
+ * `{ perTryBudgetUsd, perTryWallMs, bridgeTries }`, all REQUIRED and explicit. There is
5
+ * no default for any of them and there will not be one: hamr's ruling on `maxWallMs`
6
+ * generalises — a defaulted cap is a silent second ceiling, and a cap the operator did
7
+ * not state is a cap nobody chose.
8
+ *
9
+ * `bridgeTries: 0` is legal and means FORCE COLD: zero stored workflows are tried, the
10
+ * run drafts from scratch, and no selection call is made (nothing to select among).
11
+ *
12
+ * With a `job`, the composition rule is enforced: the per-try numbers may only TIGHTEN
13
+ * the signed spec. A per-try budget above `job.budgetUsd`, or a per-try wall above
14
+ * `job.maxWallMs`, is `envelope-widens` — the runner may narrow the operator's cap, never
15
+ * widen it. A spec that sets NO `maxWallMs` is time-unbounded by explicit choice, so any
16
+ * wall is a tightening of it.
17
+ *
18
+ * @param {unknown} input the operator's envelope
19
+ * @param {{job?: unknown}} [opts] the SIGNED spec, when the composition is being checked
20
+ * @returns {{ ok: boolean, reds: Red[], envelope: any }}
21
+ */
22
+ export function validateEnvelope(input: unknown, { job }?: {
23
+ job?: unknown;
24
+ }): {
25
+ ok: boolean;
26
+ reds: Red[];
27
+ envelope: any;
28
+ };
29
+ /**
30
+ * The PER-TRY spec: the signed job with the envelope's caps written onto it. Pure — the
31
+ * input spec is never mutated.
32
+ *
33
+ * This is what every try actually runs, cold leg included, so "$5 and 30 mins x2 then
34
+ * start anew" means the same numbers all the way down.
35
+ *
36
+ * **It is a spec VERSION, not a spec edit made behind the operator's back.** When the
37
+ * envelope equals the spec's own numbers the result is hash-IDENTICAL and the existing
38
+ * approval covers it. When it tightens, the hash MOVES — and `runJob`'s approval gate
39
+ * refuses the run until the operator has signed that hash. That refusal is the feature:
40
+ * the tightened numbers are the ones enforced, so they are the ones signed.
41
+ *
42
+ * @param {object} job the signed spec
43
+ * @param {{perTryBudgetUsd: number, perTryWallMs: number}} envelope a validated envelope
44
+ * @returns {object} the per-try spec
45
+ */
46
+ export function resolveTrySpec(job: object, envelope: {
47
+ perTryBudgetUsd: number;
48
+ perTryWallMs: number;
49
+ }): object;
50
+ /**
51
+ * The RESOLVED REUSE ARTIFACT — the thing the operator actually signs.
52
+ *
53
+ * The envelope is THREE numbers, and the third one is a MULTIPLIER: the worst case a
54
+ * reuse run authorizes is `perTryBudgetUsd × (bridgeTries + 1)`. A signature taken over
55
+ * the per-try spec alone covers only two of the three — `--tries 0` and `--tries 9` hash
56
+ * identically while authorizing ten times the spend. So the signed artifact is this
57
+ * WRAPPER, and `reuseSpecHash` is what an approval gate prints and compares.
58
+ *
59
+ * It is a wrapper rather than a field on the spec because `validateJob` reds every
60
+ * unknown top-level field: a `bridgeTries` written onto the per-try spec would flip the
61
+ * hash and then make every try a `job-red`. The spec half stays exactly the spec `runJob`
62
+ * executes, unchanged in kind.
63
+ *
64
+ * `bridgeTries` is READ ONCE, here, and the run's loop bound is read back off this same
65
+ * object — the number enforced and the number signed are the same number from the same
66
+ * place, never two reads of the envelope that a later edit could separate.
67
+ *
68
+ * @param {object} job the signed spec
69
+ * @param {{perTryBudgetUsd: number, perTryWallMs: number, bridgeTries: number}} envelope
70
+ * @returns {{schema: string, spec: any, bridgeTries: number}}
71
+ */
72
+ export function resolveReuse(job: object, envelope: {
73
+ perTryBudgetUsd: number;
74
+ perTryWallMs: number;
75
+ bridgeTries: number;
76
+ }): {
77
+ schema: string;
78
+ spec: any;
79
+ bridgeTries: number;
80
+ };
81
+ /**
82
+ * The APPROVAL HASH: one number covering all three envelope numbers.
83
+ *
84
+ * Composed from the repo's ONE spec hasher rather than re-canonizing the wrapper, so the
85
+ * per-try spec half is hashed exactly as `jobSpecHash` hashes it — MED-1's resolved-`tools`
86
+ * pinning included, and with no second canonicalizer that could drift from it. The
87
+ * `reuse-v1` prefix is domain separation: a reuse approval can never collide with a bare
88
+ * job approval.
89
+ *
90
+ * **This is a signing-SCHEME change, and it invalidates every hash signed before it — by
91
+ * design.** A signature that did not cover the try count never covered the run's worst
92
+ * case, so it is not carried forward: there is no legacy acceptance path, and re-signing
93
+ * once is the whole migration.
94
+ *
95
+ * @param {{spec: any, bridgeTries: number}} resolved a `resolveReuse` artifact
96
+ * @returns {string} sha256 hex
97
+ */
98
+ export function reuseSpecHash(resolved: {
99
+ spec: any;
100
+ bridgeTries: number;
101
+ }): string;
102
+ /**
103
+ * Ask the model which stored workflow (if any) this run should start from — ONE call, on
104
+ * the drafter-tier provider the CALLER hands in. No provider is constructed here: the
105
+ * provider binding is shell territory, exactly as it is for every other call in this
106
+ * repo.
107
+ *
108
+ * The three operator overrides ride into the same call rather than around it:
109
+ * - `forceCold` skips everything ($0, no tokens) — the operator has already decided.
110
+ * - `shortlist` restricts the LISTING to those names, so the model chooses among the
111
+ * operator's set and cannot reach past it.
112
+ * - `pinned` does NOT bypass the call. D3: the model may refuse a pin only EXPLICITLY,
113
+ * with a reason. So the pin is stated in the prompt, and if the answer names anything
114
+ * other than the pin the result is `{refused: true}` — the substitute is NOT adopted,
115
+ * and the decision goes back to the operator. Neither direction is silent.
116
+ *
117
+ * An EMPTY candidate set also skips the call: paying a model to pick from an empty
118
+ * listing buys nothing, and "there is nothing to reuse" is not a judgment call.
119
+ *
120
+ * The answer is parsed with the repo's ONE model-output parser (`extractArtifact`) and
121
+ * then strictly: an object with a `choice` that is null or a name FROM THE LISTING, plus
122
+ * a `reason`. A name that is not on the listing is a named red, never used — a picker
123
+ * that can name a workflow nobody offered is a picker that can reach outside the
124
+ * operator's set.
125
+ *
126
+ * Cost is metered and reported (F6): `costUsd` is the priced figure or an explicit null,
127
+ * and `spendComplete` says whether that figure is exact. A skipped call is a real 0.
128
+ *
129
+ * @param {object} opts
130
+ * @param {unknown} opts.registry `loadRegistry`'s result, or a bare array of entries
131
+ * @param {object} opts.job the job this run is signed against (only its goal is used here)
132
+ * @param {string} opts.ask the ask, in the operator's own words
133
+ * @param {any} opts.provider the drafter-tier provider binding (caller-owned)
134
+ * @param {string|null} [opts.pinned] the operator's pin
135
+ * @param {string[]|null} [opts.shortlist] the operator's shortlist
136
+ * @param {boolean} [opts.forceCold]
137
+ * @param {string[]} [opts.exclude] names already tried this run
138
+ * @returns {Promise<any>} `{ choice, reason, called, refused, forcedCold, red, costUsd,
139
+ * spendComplete, candidates, listing }`
140
+ */
141
+ export function selectBridge({ registry, job, ask, provider, pinned, shortlist, forceCold, exclude }: {
142
+ registry: unknown;
143
+ job: object;
144
+ ask: string;
145
+ provider: any;
146
+ pinned?: string | null | undefined;
147
+ shortlist?: string[] | null | undefined;
148
+ forceCold?: boolean | undefined;
149
+ exclude?: string[] | undefined;
150
+ }): Promise<any>;
151
+ /**
152
+ * Read a killed reuse run's OWN SPINE back into the state a resume continues from.
153
+ *
154
+ * hamr's checkpoint ruling is the whole contract: *"money, signature and checkpoint
155
+ * (starts from where it stopped) if mid loop, restart that loop"*. So this reader
156
+ * answers exactly four questions, and nothing else:
157
+ *
158
+ * 1. **Which tries COMPLETED** — they are never re-run, and their bridges stay
159
+ * spoken for exactly as if the loop had carried on.
160
+ * 2. **Was the process mid-try** — a `try-start` with no matching `try-end`. That
161
+ * try was never graded, so it is not consumed: it RESTARTS from its beginning.
162
+ * 3. **What that dead attempt already spent, in money and in wall time** — folded
163
+ * into the restart so it runs under the REMAINDER of its signed per-try numbers.
164
+ * 4. **Which envelope this spine belongs to** — the `reuse-start` record's own
165
+ * approval hash, so a resume can prove it is continuing the SAME signed run.
166
+ *
167
+ * **F45 governs the arithmetic here.** A window's spend is summed from `worker-round`
168
+ * ONLY — the same event `runJob`'s own ledger accounts, no other. The selection calls
169
+ * are real money and are counted, but as the RUN's cost and never as a try's: a
170
+ * picker's tokens are not a worker's, and attributing them to a try would misstate
171
+ * both the fold and the row. `worker-turn` (native attribution, cost null by design)
172
+ * is deliberately not summed, exactly as the live ledger does not sum it.
173
+ *
174
+ * **The declared fold, not a re-derivation.** A restarted try's own `try-start`
175
+ * carries the fold it inherited (`priorSpentUsd`/`priorWallMs`), so a resume OF a
176
+ * resume adds only its own new rounds. Re-deriving from the whole file would bill an
177
+ * abandoned attempt twice. The REPLAN ledger (`priorReplans`/`priorReplanGrantUsed`)
178
+ * folds on this same mechanism and deliberately not on `readGradeSeed`'s: a grade seed
179
+ * feeds a readout, so its documented one-leg-per-resume shortening can only under-claim
180
+ * a direction, while an under-claimed CEILING is a refilled allowance and every kill
181
+ * would buy one (PRD v1.12 — the replan bound is the RUN's).
182
+ *
183
+ * **KNOWN LIMIT, stated rather than discovered later.** A call that was killed BEFORE it
184
+ * returned left no event at all — no `worker-round`, no `selection-result` — so money it
185
+ * may already have been billed for is not on the spine and cannot be folded. This reader
186
+ * can only account writers that WROTE (the same limit the runner's own guards have: a
187
+ * hung `generate()` leaves zero trace). The fold is therefore a floor in exactly the case
188
+ * `priorSpendComplete` already marks, and one lost selection call is re-paid by the
189
+ * resumed pick rather than silently attributed to a try.
190
+ *
191
+ * **A try whose `job-end` landed is COMPLETE, not mid-flight.** The close already
192
+ * rendered its verdict; restarting would pay a second time for an answer already in
193
+ * hand. When the death fell between that verdict and the registry write, the row that
194
+ * was lost is NAMED (`r1Missing`) rather than silently absent — and never re-derived,
195
+ * because a green's version has to be the plan AS EXECUTED and this reader is not
196
+ * where that is decided.
197
+ *
198
+ * **THE HALT AMENDMENT (PRD v1.46 §3).** "A landed `job-end` is COMPLETE" is true of
199
+ * a VERDICT, and a governance halt is not one: a money cap-halt (and, W-2-symmetric,
200
+ * a wall-halt) means the run ran out of an operator-owned allowance with its work on
201
+ * disk and its plan on the spine — the exact case hamr's ruling was about ("why would
202
+ * i want to waste more money on something i already started"). `resumableOutcomes`
203
+ * names which terminals get read that way. It is EMPTY by default, so the reuse
204
+ * loop's own semantics are byte-unchanged: there, a cap-halted try was graded, its
205
+ * registry row was written, and turning it into a restart would re-run a try whose
206
+ * fold leaves it no money at all and duplicate its row. Green and every red stay
207
+ * non-resumable under any setting — a verdict already rendered is never re-bought.
208
+ *
209
+ * **DIRECT SPINES (`direct`).** A `runJob` spine has no `reuse-start` and no try
210
+ * windows, so this reader saw one as "not a reuse run" and every priced round in it
211
+ * as a STRAY. Opted in, the run itself is read as ONE implicit try opened at
212
+ * `job-start` — the same window machinery, never a second reader, because the whole
213
+ * failure class this reader exists to survive (F45) is the writer nobody modelled.
214
+ * The fold it declares comes off `job-start`'s own `priorSpentUsd`, exactly as a
215
+ * restarted try's comes off its `try-start`: re-deriving from the file would bill an
216
+ * abandoned attempt twice in a chain of resumes.
217
+ *
218
+ * @param {any[]} events the dead spine's events, parsed, in file order
219
+ * @param {{deathAt?: number|null, direct?: boolean, resumableOutcomes?: string[]}} [opts]
220
+ * `deathAt`: when the process is judged to have died (the watchdog's kill record,
221
+ * which is later and better evidence than the last event). Defaults to the last
222
+ * event's own timestamp — the last sign of life there is.
223
+ * @returns {any} `{ started, approvalHash, specHash, patient, job, bridgeTries,
224
+ * perTryBudgetUsd, perTryWallMs, ended, endOutcome, greened, completed, tried,
225
+ * selectionCostUsd, selectionComplete, strayRounds, r1Missing, restart, deathAtKnown,
226
+ * spentUsd, spendComplete, carrySpentUsd, carrySpendComplete }`
227
+ */
228
+ export function readResume(events: any[], { deathAt, direct, resumableOutcomes }?: {
229
+ deathAt?: number | null;
230
+ direct?: boolean;
231
+ resumableOutcomes?: string[];
232
+ }): any;
233
+ /**
234
+ * The PATIENT gate for a resume, which is deliberately NOT the fresh-launch one.
235
+ *
236
+ * A fresh reuse run refuses a dirty tree: a run that inherits the previous run's edits
237
+ * measures the wrong thing. A RESUME is the opposite case — the dead tries' work is real
238
+ * progress this run already paid for, the tries share one workdir by design, and the tree
239
+ * is reset between RUNS by the operator, never by the harness. So a dirty tree is exactly
240
+ * what a resume expects to find, and it is continued as the run left it: not reset, not
241
+ * refused.
242
+ *
243
+ * What IS refused is HEAD moving off the seed. That is not the run's own work — nothing
244
+ * in the flow commits — so it means a human committed, rebased or checked something out
245
+ * under the dead run. The reconstruction's premise (this tree is where try N left it) no
246
+ * longer holds, and that decision comes back to the operator.
247
+ *
248
+ * @param {{head: string, seed: string, dirty: string}} state as `git rev-parse HEAD` and
249
+ * `git status --porcelain` report it
250
+ * @returns {{ok: boolean, detail: string|null}}
251
+ */
252
+ export function resumeTreeGate({ head, seed, dirty }: {
253
+ head: string;
254
+ seed: string;
255
+ dirty: string;
256
+ }): {
257
+ ok: boolean;
258
+ detail: string | null;
259
+ };
260
+ /**
261
+ * Run a job under the reuse envelope: select a stored workflow, try it, and — if the
262
+ * envelope pre-authorized it — try the next one, then draft cold.
263
+ *
264
+ * The shape, end to end:
265
+ *
266
+ * 1. **Envelope** validated against the SIGNED spec (tighten-only). A red stops here,
267
+ * before the registry is read and before anything is spent.
268
+ * 2. **Registry** loaded from the operator-supplied directory (never a default location).
269
+ * A missing directory is a named red; a malformed ENTRY is skipped, reported, and
270
+ * carried into the listing so the picker is never handed a silently shorter set.
271
+ * 3. **Up to `bridgeTries` tries.** Each try re-runs selection against a FRESHLY RELOADED
272
+ * registry with the already-tried names excluded — so the next pick sees the row the
273
+ * last try just wrote, and cannot re-pick a workflow this run already spent money on.
274
+ * The chosen entry rides into `runJob`, whose own load gate is the D2 shape check; a
275
+ * gate refusal (`recipe-stale`) costs $0 and simply moves to the next candidate.
276
+ * 4. **After each try the box is written (R1)** — green appends a version + a row, a
277
+ * graded red appends a row, a casualty appends a row under its own name. Saved
278
+ * atomically, one write per try.
279
+ * 5. **A green ENDS the loop.** The job is done; there is nothing left to try.
280
+ * 6. **Tries exhausted → the cold leg**, under the SAME per-try numbers ("then start
281
+ * anew"). Its green mints a NEW bridge named for the job slug — or appends to the
282
+ * entry of that name if one already exists, because clobbering a green is exactly the
283
+ * thing R1 exists to prevent.
284
+ *
285
+ * Fallthrough is automatic ONLY because the envelope pre-authorized it (D7) — that is
286
+ * what makes it not a per-failure nag, and it is also why nothing else here is automatic:
287
+ * a refused pin, an unreadable selection answer and a missing registry all STOP and hand
288
+ * the decision back.
289
+ *
290
+ * **RESUME (module C).** With `resume` — a `readResume` reading of the dead run's own
291
+ * spine — the same loop picks up where a killed run stopped: the completed tries are
292
+ * seeded in (never re-run, their bridges never re-offered), a try that was mid-flight is
293
+ * RESTARTED from its beginning against the SAME workflow and with NO second selection
294
+ * call (the pick was made and paid for), and it runs under the REMAINDER of its own
295
+ * signed per-try numbers. The remainder is expressed as a FOLD of the dead attempt's
296
+ * spend and wall — never as tightened caps — because the caps are in the spec hash and
297
+ * rewriting them would need a signature nobody typed. If the remainder cannot fund the
298
+ * restart the run caps honestly, having launched nothing.
299
+ *
300
+ * @param {object} opts
301
+ * @param {object} opts.job the signed job spec
302
+ * @param {unknown} opts.approvals approval records — passed straight through to `runJob`;
303
+ * this module never mints one (a tightened envelope makes a NEW spec version, and the
304
+ * operator signs it)
305
+ * @param {string} opts.registryDir the operator-supplied bridge registry
306
+ * @param {unknown} opts.envelope `{ perTryBudgetUsd, perTryWallMs, bridgeTries }`
307
+ * @param {string} opts.patient the instance this run works on — the distinct-patient
308
+ * count IS the status ladder (D6/R2), so it is required and never guessed from a path
309
+ * @param {string} opts.workdir the run directory
310
+ * @param {any} opts.provider the worker provider binding
311
+ * @param {any} [opts.selectionProvider] the drafter-tier provider for the selection call
312
+ * (defaults to `provider`)
313
+ * @param {(type: string, data?: object) => object} opts.emit spine emitter
314
+ * @param {string} [opts.ask] the ask in the operator's words (defaults to the job's goal)
315
+ * @param {string|null} [opts.pinned]
316
+ * @param {string[]|null} [opts.shortlist]
317
+ * @param {boolean} [opts.forceCold]
318
+ * @param {string} [opts.runid] the run id the bridge rows point back at (defaults to a
319
+ * fresh one); the operator runner passes its own so a row names its spine file
320
+ * @param {any} [opts.providerFor] per-step model-tier factory, forwarded
321
+ * @param {any} [opts.nativeProvider] native provider factory, forwarded
322
+ * @param {number} [opts.capRuns] forwarded
323
+ * @param {number} [opts.strikeLimit] forwarded (a plan step's strike ceiling)
324
+ * @param {number} [opts.closeTimeoutMs] forwarded
325
+ * @param {boolean} [opts.layerRoot] forwarded
326
+ * @param {any} [opts.resume] RESUME (module C) — a `readResume` reading of the KILLED
327
+ * run's own spine. Its `approvalHash` must match this run's, or the run refuses:
328
+ * a resume continues ONE signed run, not any run.
329
+ * @param {typeof shippedRunJob} [opts.runJob] the job runner. An injected seam of the
330
+ * same class as `provider`: a try is a real paid run, and the loop's own logic —
331
+ * selection order, exclusion, minting, exhaustion — is not testable against one.
332
+ * @param {() => number} [opts.now] the clock, injected (per-try wall measurement)
333
+ * @returns {Promise<any>} `{ outcome, tries, selection, spentUsd, spendComplete,
334
+ * bridgeWrites, decision, options, reds, triesAuthorized, triesUsed, envelope, specHash,
335
+ * approvalHash }` — `specHash` is the per-try spec's (what `runJob`'s own gate reads);
336
+ * `approvalHash` is the OPERATOR's, covering all three envelope numbers
337
+ */
338
+ export function runReuse(opts: {
339
+ job: object;
340
+ approvals: unknown;
341
+ registryDir: string;
342
+ envelope: unknown;
343
+ patient: string;
344
+ workdir: string;
345
+ provider: any;
346
+ selectionProvider?: any;
347
+ emit: (type: string, data?: object) => object;
348
+ ask?: string | undefined;
349
+ pinned?: string | null | undefined;
350
+ shortlist?: string[] | null | undefined;
351
+ forceCold?: boolean | undefined;
352
+ runid?: string | undefined;
353
+ providerFor?: any;
354
+ nativeProvider?: any;
355
+ capRuns?: number | undefined;
356
+ strikeLimit?: number | undefined;
357
+ closeTimeoutMs?: number | undefined;
358
+ layerRoot?: boolean | undefined;
359
+ resume?: any;
360
+ runJob?: typeof shippedRunJob | undefined;
361
+ now?: (() => number) | undefined;
362
+ }): Promise<any>;
363
+ /** @typedef {{code: string, path: string, detail?: string}} Red */
364
+ /**
365
+ * The GRADED-RED set: the runJob outcomes in which the operator's close actually
366
+ * rendered a verdict on the tree and that verdict was RED. Only these demote (D6).
367
+ *
368
+ * `escalated` is the one. It is the terminal runPlan returns when the close judged the
369
+ * tree, the bounded fix loop ran its attempts with money still on the table, and the
370
+ * close was still red — i.e. the recipe was graded by the job's own truth and failed.
371
+ *
372
+ * Everything else non-green is a CASUALTY and keeps its own name:
373
+ * - `cap-halt` / `wall-halt` — governance stops. A money cut is never a capability read
374
+ * (F45) and the wall keeps the verdict already minted (W-2).
375
+ * - `provider-red` / `step-stalled` / `pricing-red` — transport and metering.
376
+ * - `close-red` / `close-unsupported` — the close FAULTED (broken, timed out, killed,
377
+ * crashed). It rendered no judgment, so its exit says nothing about the tree.
378
+ * - `recipe-stale` — the load gate refused the recipe at the door, $0 spent.
379
+ * - `plan-red` / `step-red:<id>` / `check-red` — the plan flow stopped before the close
380
+ * ever judged. Read as evidence about the RECIPE these would be a guess; they are
381
+ * recorded in full on the history row, which is where D6 puts the detail a human
382
+ * reads, and the coarse status ladder is left alone.
383
+ * - `unapproved-spec` / `job-red` / `smoke-red` / `interpreter-red` — the run never
384
+ * started.
385
+ */
386
+ export const REUSE_GRADED_RED: readonly string[];
387
+ export type Red = {
388
+ code: string;
389
+ path: string;
390
+ detail?: string;
391
+ };
392
+ import { runJob as shippedRunJob } from './run.js';
package/types/run.d.ts CHANGED
@@ -34,9 +34,55 @@
34
34
  * metered claude-json text provider for the toolless drafter); ignored on every Loop-driven provider
35
35
  * @param {(type: string, data?: object) => object} opts.emit spine emitter
36
36
  * @param {(tier: string) => any} [opts.providerFor] P: per-step model-tier provider factory (forwarded to the plan flow)
37
- * @param {number} [opts.capRuns] shell-owned per-step attempt cap
37
+ * @param {number} [opts.capRuns] shell-owned iteration cap for the plan flow's
38
+ * CLOSE-FIX loop (a plan STEP runs under the strike ladder instead)
39
+ * @param {number} [opts.strikeLimit] shell-owned strike ceiling for a plan step's ladder
38
40
  * @param {number} [opts.shellCapUsd] the shell's hard USD ceiling
39
41
  * @param {number} [opts.closeTimeoutMs] close wall-clock cap (shell territory)
42
+ * @param {unknown} [opts.bridge] Layer 3 — a bridge-v1 entry to reuse as the drafter's
43
+ * STARTING DRAFT (design record 2026-08-01, D4), forwarded to the plan flow. Omitted is
44
+ * the cold path and is byte-identical to a pre-Layer-3 run. WHICH entry (the listing, an
45
+ * operator pin, the LLM's pick) is the caller's decision, and so is what to do when the
46
+ * load gate refuses one: a refusal returns the distinct `recipe-stale` outcome with zero
47
+ * spend rather than silently drafting cold, because starting a paid run on a decision
48
+ * nobody made is the same class of error as widening a cap to manufacture a green.
49
+ * @param {number} [opts.priorSpentUsd=0] RESUME (module C) — money a PREVIOUS, killed
50
+ * attempt of this same try already spent. It seeds the ledger, so the restarted
51
+ * attempt runs under the REMAINDER of the signed `budgetUsd` and the terminal
52
+ * `job-end` states the try's WHOLE spend across both attempts. hamr's ruling: "a
53
+ * budget ceiling folds in prior spend so re-invoking cannot silently widen it" —
54
+ * the fold is the seam precisely because the CAP is in the spec hash, so
55
+ * tightening the cap instead would make a new spec version needing a signature
56
+ * nobody typed. The caller reconstructs the figure from the dead run's own spine.
57
+ * @param {boolean} [opts.priorSpendComplete=true] RESUME — was that fold EXACT?
58
+ * `readResume` reports false when any round inside the dead attempt came back
59
+ * unpriced, which makes the fold a FLOOR. The unknown does not heal by being carried
60
+ * forward, so it rides one-way onto every `job-end` this run emits: a total built on
61
+ * a floor is a floor, and reporting it as exact is F6 in a resume's coat.
62
+ * @param {number} [opts.priorWallMs=0] RESUME — the same ruling in a time coat: wall
63
+ * time the killed attempt already consumed, folded into the run clock so the
64
+ * restart gets the remainder of the SIGNED wall, never a fresh allotment.
65
+ * @param {any} [opts.resumeSeed] RESUME (module C v2) — WHERE inside the killed attempt
66
+ * the kill landed, read off its own spine by `readResume`: the plan it accepted and the
67
+ * steps that reached their exits. Forwarded verbatim to the plan flow, which reloads
68
+ * the plan instead of re-scouting and re-drafting, and skips the finished steps.
69
+ * hamr's ruling: *"start last step instead from the beginning, why would i want to
70
+ * waste more money on something i already started"*. Absent is the ordinary path.
71
+ * @param {{stage?: string|null, value?: number|null}[]} [opts.resumeGrades] RESUME — the
72
+ * close grades the killed leg recorded (`readResume`'s `restart.grades`), forwarded to
73
+ * the plan flow as the trend's BASELINES. A resumed run's halt readout judges the whole
74
+ * chain, not just the leg: without them a leg that re-grades an unchanged tree reports
75
+ * `flat` — "revise the goal" — on a run that was converging when the money ran out.
76
+ * Counts and stage names only; no close bytes cross this seam. Absent is the cold path.
77
+ * @param {{count?: number, grantUsed?: boolean}|null} [opts.resumeReplans] RESUME — the
78
+ * replan LEDGER the chain has already spent (`readResume`'s `restart.replans` /
79
+ * `restart.replanGrantUsed`), forwarded to the plan flow, which seeds its ceiling with
80
+ * it. Its sibling above feeds a READOUT; this one feeds a BOUND, and that is the whole
81
+ * difference: `replanned`/`varianceGrantUsed` are locals in `runPlan`, so without this
82
+ * every kill hands the next leg a fresh allowance of an allowance PRD v1.12 makes the
83
+ * RUN's ("unlimited replanning launders thrash as adaptation"). It also rides onto this
84
+ * run's `job-start` as `priorReplans`, the declared-fold precedent `priorSpentUsd`
85
+ * already sets, so a resume OF a resume folds once. Absent is the cold path.
40
86
  * @param {boolean} [opts.layerRoot=false] Layer R (within-run ratchet) — shell
41
87
  * territory, threaded to the plan flow. Defaults OFF
42
88
  * (decided 2026-07-21): fixation is extinct on every current job (F41), so
@@ -46,10 +92,10 @@
46
92
  * binding.)
47
93
  * @returns {Promise<string>} outcome: 'green' | 'already-green' | 'escalated' |
48
94
  * 'unapproved-spec' | 'job-red' | 'smoke-red' | 'plan-red' | 'check-red' |
49
- * 'close-red' | 'close-unsupported' | 'pricing-red' | 'provider-red' |
95
+ * 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' | 'provider-red' |
50
96
  * 'interpreter-red' | 'cap-halt' | 'wall-halt' | 'step-stalled' | `step-red:<id>`
51
97
  */
52
- export function runJob(rawSpec: object | string, { approvals, workdir, provider, nativeProvider, providerFor, emit, capRuns, shellCapUsd, closeTimeoutMs, layerRoot }: {
98
+ export function runJob(rawSpec: object | string, { approvals, workdir, provider, nativeProvider, providerFor, emit, capRuns, strikeLimit, shellCapUsd, closeTimeoutMs, layerRoot, bridge, priorSpentUsd, priorSpendComplete, priorWallMs, resumeSeed, resumeGrades, resumeReplans }: {
53
99
  approvals: unknown;
54
100
  workdir: string;
55
101
  provider: any;
@@ -62,8 +108,22 @@ export function runJob(rawSpec: object | string, { approvals, workdir, provider,
62
108
  emit: (type: string, data?: object) => object;
63
109
  providerFor?: ((tier: string) => any) | undefined;
64
110
  capRuns?: number | undefined;
111
+ strikeLimit?: number | undefined;
65
112
  shellCapUsd?: number | undefined;
66
113
  closeTimeoutMs?: number | undefined;
114
+ bridge?: unknown;
115
+ priorSpentUsd?: number | undefined;
116
+ priorSpendComplete?: boolean | undefined;
117
+ priorWallMs?: number | undefined;
118
+ resumeSeed?: any;
119
+ resumeGrades?: {
120
+ stage?: string | null;
121
+ value?: number | null;
122
+ }[] | undefined;
123
+ resumeReplans?: {
124
+ count?: number;
125
+ grantUsed?: boolean;
126
+ } | null | undefined;
67
127
  layerRoot?: boolean | undefined;
68
128
  }): Promise<string>;
69
129
  export type Red = {
@@ -0,0 +1,46 @@
1
+ /**
2
+ * D3's listing: what the operator inspects and what the selecting model reads — the SAME
3
+ * text, so the two can never be choosing from different sets.
4
+ *
5
+ * Per bridge, one compact block: name, the job sentence it greened, status, greens/reds
6
+ * with the last outcome, and the cost/time band of its greens.
7
+ *
8
+ * Total and never throws: a listing is a display surface, and one malformed entry must
9
+ * not take the whole listing down (`listingRow` holds the same contract). Deterministic
10
+ * by name, whatever order the entries arrived in — a set that reshuffles between calls
11
+ * would make one picker's answer unreadable against another's.
12
+ *
13
+ * @param {unknown} registry `loadRegistry`'s result (`{ok, reds, bridges}`) or a bare
14
+ * array of entries. The RESULT form is preferred: it carries the skip reds, and the
15
+ * skips are the half a bare array cannot tell you about.
16
+ * @returns {string}
17
+ */
18
+ export function renderListing(registry: unknown): string;
19
+ /**
20
+ * The SELECT-WORKFLOW prompt: the text a later module sends to ask which workflow (if
21
+ * any) the run should start from. Pure — it sends nothing and parses nothing.
22
+ *
23
+ * D3's two standing rules are IN the prompt rather than enforced after the fact:
24
+ * - *"none matches"* is a first-class answer, and it means the agent drafts new. A picker
25
+ * with no way to decline would return the closest thing on the shelf every time, which
26
+ * is how a registry becomes a lookup table.
27
+ * - a PINNED workflow may be refused **only explicitly, with a reason** — never silently
28
+ * ignored or quietly substituted. The operator's pick is the operator's; overriding it
29
+ * invisibly is the one thing a picker must not be able to do.
30
+ *
31
+ * The output contract is stated so the answer is machine-readable, but the PARSE belongs
32
+ * to the runner that sends this (modules 4/5) — and the runner's parser and this shape are
33
+ * one contract in two places, which is why the shape is spelled out here rather than left
34
+ * to the model.
35
+ *
36
+ * @param {string} listing `renderListing`'s text — verbatim, so the picker sees exactly
37
+ * what the human inspecting the registry sees
38
+ * @param {string} ask the job's goal sentence, in the operator's own words
39
+ * @param {string|null} [pinned] the operator's PIN, stated IN the call. D3 rules that a
40
+ * pin is never bypassed and never silently overridden — so the pin does not skip the
41
+ * call, it enters it, and the model's only legal ways out are confirming it or
42
+ * refusing it by name with a reason. Stated here rather than assembled by the runner:
43
+ * a second spelling of the pin sentence is a second prompt, and the two would drift.
44
+ * @returns {string}
45
+ */
46
+ export function selectionPrompt(listing: string, ask: string, pinned?: string | null): string;
package/types/spine.d.ts CHANGED
@@ -1,6 +1,13 @@
1
1
  /**
2
2
  * Create an emitter bound to one JSONL file.
3
3
  * @param {string} file absolute path; created on first emit
4
+ * @param {{startSeq?: number}} [opts] `startSeq`: the last `seq` already IN the file, for a
5
+ * process that CONTINUES an existing spine (a resumed run appends to the dead run's own
6
+ * log — module C). `seq` is monotonic per SPINE, so a second series restarting at 1
7
+ * would give one append-only log two rows with the same number and silently break every
8
+ * reader that orders by it. Default 0: a fresh file, which is every other caller.
4
9
  * @returns {(type: string, data?: object) => object} emit — returns the event as written
5
10
  */
6
- export function makeSpine(file: string): (type: string, data?: object) => object;
11
+ export function makeSpine(file: string, { startSeq }?: {
12
+ startSeq?: number;
13
+ }): (type: string, data?: object) => object;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Read one close gap into its stage and its count. Pure, and both halves are
3
+ * independently nullable: a gap with no header still yields its count, a gap
4
+ * with no red-marked line still yields its stage.
5
+ * @param {string} [gap] the close's gap, exactly as `runStages` built it
6
+ * @returns {{stage: string|null, value: number|null}}
7
+ */
8
+ export function readGrade(gap?: string): {
9
+ stage: string | null;
10
+ value: number | null;
11
+ };
12
+ /**
13
+ * Build one trend reader. ONE PER SERIES: the money readout wants the whole run's
14
+ * close grades (the precheck is the seed the work is measured against), while the
15
+ * fix governor wants only the grades its own loop produced. Two instances, one
16
+ * rule — never one instance with a phase field, which is how a shared reader comes
17
+ * to answer two questions with one number.
18
+ *
19
+ * @param {object} [opts]
20
+ * @param {string[]} [opts.stageOrder] the staged close's stage names IN ORDER, so
21
+ * "the run got further than it ever had" is readable. Empty disables that signal
22
+ * entirely (an unknown ordering is unknown, never assumed).
23
+ * @param {number} [opts.limit] consecutive no-progress readings that strike the
24
+ * loop out (default `FIX_STRIKE_LIMIT`).
25
+ * @param {number|null} [opts.blindCap] the RETIRED count, kept as the bound for
26
+ * the case the instrument cannot read at all. Counted in iterations PAST the
27
+ * baseline, so a blind loop runs exactly the number of attempts the old fixed
28
+ * count bought it. `null` = no fallback bound (money and the wall still apply).
29
+ * @param {{stage?: string|null, value?: number|null}[]} [opts.seed] RESUME — the
30
+ * grades a PREVIOUS leg of this same run already recorded, oldest first, read off
31
+ * its own spine. Baselines only (see THE SEED above). Empty/omitted is the cold
32
+ * path and is byte-identical to the pre-resume reader.
33
+ */
34
+ export function createTrend({ stageOrder, limit, blindCap, seed }?: {
35
+ stageOrder?: string[] | undefined;
36
+ limit?: number | undefined;
37
+ blindCap?: number | null | undefined;
38
+ seed?: {
39
+ stage?: string | null;
40
+ value?: number | null;
41
+ }[] | undefined;
42
+ }): {
43
+ /**
44
+ * Read one RED close grade. A green never reaches here (both callers stop on
45
+ * satisfied), so every reading is a failing grade by construction.
46
+ * @param {{gap?: string, stage?: string|null, value?: number|null}} o either a
47
+ * raw gap (parsed here) or an already-read `{stage, value}` — never both
48
+ * spellings of the same reading in one call.
49
+ * @returns {{iteration: number, stage: string, value: number|null, improved: boolean,
50
+ * comparable: boolean, noProgress: number, limit: number, stageIndex: number}}
51
+ */
52
+ record({ gap, stage, value }?: {
53
+ gap?: string;
54
+ stage?: string | null;
55
+ value?: number | null;
56
+ }): {
57
+ iteration: number;
58
+ stage: string;
59
+ value: number | null;
60
+ improved: boolean;
61
+ comparable: boolean;
62
+ noProgress: number;
63
+ limit: number;
64
+ stageIndex: number;
65
+ };
66
+ /** has the loop run out of ideas — or, while the instrument is blind, out of
67
+ * the count it replaced? Money and the wall are unchanged and bound it either way.
68
+ * The blind arm counts the streak PAST its own first reading, the same `- 1` the
69
+ * blind-from-birth path always used: a streak's opening grade is the baseline the
70
+ * blind iterations are measured from, so a never-comparable run binds on exactly
71
+ * the reading it always did. */
72
+ struckOut: () => boolean;
73
+ /** the reader's own books. COUNTS AND STAGE NAMES ONLY — no close bytes ever
74
+ * enter a record from here, because the spine is append-only forever. */
75
+ report: () => {
76
+ iterations: number;
77
+ strikes: number;
78
+ limit: number;
79
+ blind: boolean;
80
+ stages: {
81
+ stage: string;
82
+ values: number[];
83
+ }[];
84
+ };
85
+ /**
86
+ * The DECISION-READY readout (PRD v1.46 §2): which way the run was going when
87
+ * something cut it, in the three verdicts hamr named, and the lever each
88
+ * implies. The library only reports — it never adjusts a budget (hard line).
89
+ * @returns {{trend: 'converging'|'flat'|'unknown', motion: 'changed'|'unchanged'|null,
90
+ * reading: string, lever: string, series: {stage: string, values: number[]}[]}}
91
+ */
92
+ verdict(): {
93
+ trend: "converging" | "flat" | "unknown";
94
+ motion: "changed" | "unchanged" | null;
95
+ reading: string;
96
+ lever: string;
97
+ series: {
98
+ stage: string;
99
+ values: number[];
100
+ }[];
101
+ };
102
+ };
103
+ /** The strikes that end the close-fix loop. Shell-owned, exactly where the fixed
104
+ * count was, and the SAME number the step ladder uses — hamr's 2. Validated by a
105
+ * $0 replay over all 8 archived fix loops: 0 greens harmed (every historical
106
+ * fix-loop green converted in <= 2 verdicts) and 1 real waste case caught
107
+ * (reuse-msc6w93z, dead flat at 2 errors for 7 verdicts until the wall). */
108
+ export const FIX_STRIKE_LIMIT: 2;