bareloop 0.11.0 → 0.13.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.
@@ -78,6 +78,25 @@
78
78
  * @returns {string} the labelled block, or '' when there is nothing to show
79
79
  */
80
80
  export function closeGapBlock(gap: string | null | undefined): string;
81
+ /**
82
+ * BA-21 — the WRITE side of the pricing-provenance signal (read side: `rateProvenance` /
83
+ * `spendProvenance` in src/ledger.js). bare-agent >=0.37 rides `rateSource` beside
84
+ * `pricing` on every metering payload; both metering callbacks below forward it onto the
85
+ * spine through THIS one helper, because two sites spelling one rule are two instruments
86
+ * (the ripgrep fix that landed in ci.yml but not publish.yml).
87
+ *
88
+ * Forwarded VERBATIM — the write side reports what upstream said, and src/ledger.js is
89
+ * the one place that decides what counts as a guess. And an ABSENT provenance stays
90
+ * absent: a payload carrying no `rateSource` (bare-agent <0.37, and every round already
91
+ * in the archive) emits no field at all, so a reader sees UNKNOWN rather than a label we
92
+ * invented. Never defaulted to `null` here — `null` is upstream saying "nothing was
93
+ * priced", which is a different fact from "nobody told us".
94
+ * @param {any} arg an `onLlmResult` / `onTurn` payload
95
+ * @returns {{rateSource?: string|null}} the field to spread into the spine record
96
+ */
97
+ export function rateSourceFields(arg: any): {
98
+ rateSource?: string | null;
99
+ };
81
100
  /**
82
101
  * What the NEXT attempt is told about the bound that cut the previous one.
83
102
  *
@@ -140,6 +159,16 @@ export function boundedNote(bounded: {
140
159
  * Rule A-v2's law (final write step only, framed wide). `priorChecks`: the predecessor
141
160
  * plan's carried checks, replan phase only — Rule B's law (keep every one). Empty/omitted
142
161
  * renders byte-identically to the pre-rules prompt.
162
+ * @param {string|null} [doorNote] MODULE 8 — the signer's own words, when a previous run
163
+ * of this spec ended at the review door and they took the RERUN door. Additive and
164
+ * omitted-by-default, so every existing caller renders byte-identically. It is a
165
+ * REQUIREMENT the plan must meet, never a replacement for the goal: the signed goal and
166
+ * the signed close are unchanged, and what the person added is what the machine could
167
+ * not see.
168
+ * @param {boolean|'cap'|'diff'} [readShim] the read shim's ARM. Does it carry G1? Then G1
169
+ * is STATED as well as enforced (the mailbox precedent again: a law only the validator
170
+ * knows costs a redraft per draft). The arms without it — `false` and `'diff'` — render
171
+ * byte-identically to the pre-shim prompt.
143
172
  */
144
173
  export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxStepRounds: number, failure: string | null, scopes: string[] | undefined, materials?: {
145
174
  balanceUsd?: number | null;
@@ -148,7 +177,7 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
148
177
  }, startingDraft?: object | null, checkFacts?: {
149
178
  seedRed?: string[];
150
179
  priorChecks?: string[];
151
- }): string;
180
+ }, doorNote?: string | null, readShim?: boolean | "cap" | "diff"): string;
152
181
  /**
153
182
  * Execute a validateJob-GREEN plan-shape job (goal/verdictType/close).
154
183
  * Called by runJob after the approval gate, validation, and the smoke — this
@@ -167,6 +196,13 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
167
196
  * `false` → the drafter has no tools, so a native session would report NO cost — return a
168
197
  * metered claude-json TEXT provider (`--output-format json`, `parse:'claude-json'`) instead,
169
198
  * so its spend is never invisible. The Loop path (`anthropic-api`) never touches this.
199
+ * @param {any} [opts.judgeProvider] softgreen — the provider a JUDGED close stage runs its
200
+ * locate call through, pinned by the operator to `JUDGE_MODEL` (src/judged.js). Separate
201
+ * from `provider`/`providerFor` on purpose: the judge tier is not a step knob and never
202
+ * agent-selectable, and the worker's binding is not a legal stand-in for it. Absent is not
203
+ * a fall-back — a judged stage with no seam instrument-STOPS as a wiring gap, the same
204
+ * answer a native job with no `nativeProvider` gets. A close with no judged stage never
205
+ * reads it.
170
206
  * @param {(type: string, data?: object) => object} opts.emit spine emitter (the caller's METERED emit)
171
207
  * @param {() => number} opts.remainingUsd the one wallet: what is left of the signed budget right now
172
208
  * @param {() => boolean} [opts.isUnpriced] has any round come back with a null cost? (F6) — the
@@ -190,6 +226,17 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
190
226
  * @param {number} [opts.scoutRounds] the read-only survey's round bound (F59: the LAST round is
191
227
  * reserved — a scout that spends every round on tools gets one toolless round to write its
192
228
  * survey, because the bound halts it mid-tool-use and text is its only deliverable)
229
+ * @param {boolean|'cap'|'diff'} [opts.readShim=false] THE READ SHIM (src/readshim.js) — the
230
+ * ARM to run, one of the Phase 2 pre-registration's four: `false` (A0, off — the default),
231
+ * `'cap'` (A1: cap + pointer + next-unseen-slice + G1), `'diff'` (A2: the diff lever alone,
232
+ * no cap, no pointer, no G1), `true` (A3: every lever). Anything else THROWS at the entry
233
+ * below rather than coercing into a truthy shim — a mis-spelled arm that silently ran A3
234
+ * is a corrupted battery row, invisible afterwards.
235
+ * OFF is byte-identical to the pre-shim run in every observable, G1 included: the frozen
236
+ * A0 baseline arm must be exactly today's behaviour, and a guard firing under a disabled
237
+ * shim would quietly make the baseline a treatment arm. The default-flip waits on a paid
238
+ * contrast that has not been approved (the `layerRoot` precedent, F41 — an unproven lever
239
+ * ships OFF and earns its default).
193
240
  * @param {boolean} [opts.layerRoot=false] Layer R — the within-run ratchet (src/root.js),
194
241
  * scoped PER STEP's ralph loop (each micro-wheel is the Layer-1 atom). Shell-assembled from
195
242
  * the step's own books: per-attempt write-sets from the F32 workerWrites audit (teed for
@@ -306,12 +353,40 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
306
353
  * is, before anything is read at all. After the fix loop opens the ruling is gone, so once the mechanical
307
354
  * stages pass again the run PAUSES for a second review rather than converting the same
308
355
  * sentence forever.
356
+ * @param {{decision: string, text?: string|null, receivedAt?: string|null}|null} [opts.heldRuling]
357
+ * F102 — the decision the CHECKPOINT carries (`readResume`'s `restart.pendingDecision`):
358
+ * an answer a person already gave, on a leg that stopped before a single round was
359
+ * bought for it. It is applied exactly as a fresh one is, through the same seam, so
360
+ * the person is never asked the same question twice; the spine records that it came
361
+ * from a record rather than from a person, and `receivedAt` says when they said it.
362
+ *
363
+ * A leg handed BOTH is refused (`hitl-decision-red`, naming the held decision): two
364
+ * answers to one question is ambiguity, and a merge would pick a winner nobody chose.
365
+ * Absent is every ordinary run and every leg the operator answers directly.
366
+ * @param {number} [opts.priorSpentUsd=0] F103 — money the CHAIN spent before this leg,
367
+ * for the `engagement` record's money half ONLY. It bounds nothing here: the wallet
368
+ * is the ledger's one level up, and `remainingUsd` already nets this fold against the
369
+ * signed budget on every path (the signed budget is the chain's ceiling, and a rerun
370
+ * spends what remains of it — never a refill). Reporting, never governance.
371
+ * @param {boolean|null} [opts.reviewDoor] MODULE 8 — does this run END at the review
372
+ * door (PRD v1.71 §3)? `true`/`false` is the operator's explicit choice in either
373
+ * direction; absent is the CLASS default (`doorOpens`): always for soft-green,
374
+ * whose credit is held until a person accepts, and never for green-class unless
375
+ * asked. It changes what the run RECORDS and never what it returns — the door is
376
+ * a disposition, and the loop's verdict is not the door's to touch.
377
+ * @param {{text: string, fromRunid?: string|null, receivedAt?: string|null}|null} [opts.doorRerun]
378
+ * MODULE 8 — the signer took the RERUN door on a previous run: a FRESH ENGAGEMENT
379
+ * (§3.4) against the same signed spec, carrying their words. Two things follow, and
380
+ * only two: the already-green shortcut is refused (the tree passing the close is
381
+ * precisely the state the person rejected), and the words reach the PLANNER as new
382
+ * authoring. It is never a `humanRuling` — that answers a close's human STAGE, and
383
+ * a green-class job has none. Empty words are refused at the same seam every door is.
309
384
  * @returns {Promise<string>} 'green' | 'already-green' | 'escalated' | 'plan-red' |
310
385
  * 'check-red' | 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' |
311
386
  * 'branch-red' | 'cap-halt' | 'wall-halt' | 'provider-red' | 'interpreter-red' |
312
387
  * 'step-stalled' | 'hitl-pause' | 'hitl-decision-red' | `step-red:<id>`
313
388
  */
314
- export function runPlan(job: any, { workdir, provider, nativeProvider, providerFor, emit, remainingUsd, isUnpriced, spendComplete, capRuns, strikeLimit, closeTimeoutMs, maxStepRounds, layerRoot, scoutRounds, bridge, now, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling }: {
389
+ export function runPlan(job: any, { workdir, provider, nativeProvider, providerFor, judgeProvider, emit, remainingUsd, isUnpriced, spendComplete, capRuns, strikeLimit, closeTimeoutMs, maxStepRounds, layerRoot, readShim, scoutRounds, bridge, now, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, priorSpentUsd, reviewDoor, doorRerun }: {
315
390
  workdir: string;
316
391
  provider: any;
317
392
  nativeProvider?: ((o: {
@@ -320,6 +395,7 @@ export function runPlan(job: any, { workdir, provider, nativeProvider, providerF
320
395
  maxTurns: number;
321
396
  hasTools: boolean;
322
397
  }) => any) | undefined;
398
+ judgeProvider?: any;
323
399
  emit: (type: string, data?: object) => object;
324
400
  remainingUsd: () => number;
325
401
  isUnpriced?: (() => boolean) | undefined;
@@ -329,6 +405,7 @@ export function runPlan(job: any, { workdir, provider, nativeProvider, providerF
329
405
  closeTimeoutMs?: number | undefined;
330
406
  maxStepRounds?: number | undefined;
331
407
  scoutRounds?: number | undefined;
408
+ readShim?: boolean | "cap" | "diff" | undefined;
332
409
  layerRoot?: boolean | undefined;
333
410
  bridge?: unknown;
334
411
  priorWallMs?: number | undefined;
@@ -348,6 +425,18 @@ export function runPlan(job: any, { workdir, provider, nativeProvider, providerF
348
425
  decision: string;
349
426
  text?: string;
350
427
  } | null | undefined;
428
+ heldRuling?: {
429
+ decision: string;
430
+ text?: string | null;
431
+ receivedAt?: string | null;
432
+ } | null | undefined;
433
+ priorSpentUsd?: number | undefined;
434
+ reviewDoor?: boolean | null | undefined;
435
+ doorRerun?: {
436
+ text: string;
437
+ fromRunid?: string | null;
438
+ receivedAt?: string | null;
439
+ } | null | undefined;
351
440
  }): Promise<string>;
352
441
  /**
353
442
  * The recorded bound-reason's ceiling in the WORKER-facing note. The reason is a
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Resolve the `readShim` flag into its arm. THROWS on anything else — the BA-4
3
+ * param-guard class, and deliberately not a red.
4
+ *
5
+ * An unrecognised value is OPERATOR input, arriving before a single token is
6
+ * spent, and there is exactly one safe way to fail on it: loudly, at once. The
7
+ * failure this guard exists to prevent is `readShim: 'diff '` (or 'Diff', or
8
+ * 'cap+diff') coerced by truthiness into A3 — a battery row that RAN the wrong
9
+ * arm, recorded the arm it was asked for, and is invisible in the results
10
+ * afterwards. That is the blind-instrument class this programme keeps paying
11
+ * for, and it would corrupt the contrast rather than merely fail a run.
12
+ *
13
+ * A red was the alternative and is wrong here twice over: a red is a verdict on
14
+ * the AGENT's work (the plan, the artifact), and this is the operator's own
15
+ * argument; and a red would need a terminal outcome name, which is arbiter
16
+ * territory and out of this change's scope.
17
+ * @param {boolean|'cap'|'diff'|undefined} [flag]
18
+ * @returns {ReadShimArm}
19
+ */
20
+ export function readShimArm(flag?: boolean | "cap" | "diff" | undefined): ReadShimArm;
21
+ /**
22
+ * The persona line this arm is entitled to — empty when the arm changes nothing
23
+ * the worker can perceive. One arm, one line: the two are never concatenated,
24
+ * because a cap arm has no diff and a diff arm has no cap.
25
+ * @param {ReadShimArm} arm
26
+ * @returns {string}
27
+ */
28
+ export function readShimStrategy(arm: ReadShimArm): string;
29
+ /** @typedef {{file: string, path: string, startLine: number, endLine: number, detail: string}} StaleReq */
30
+ /** @typedef {{text: string, outcome: 'stale-served'|'stale-absent'|'stale-past-eof'}} StaleServe */
31
+ /**
32
+ * Create the shim: one delivery ledger, and the two seams that speak for it.
33
+ *
34
+ * `wrapRead` is the read tool's wrapper (the ledger's only writer). `serveStale`
35
+ * is the answer to a stale `ctx_get` pointer, and it deliberately holds NO
36
+ * reference to the ledger — see its own note. Both live in one object because
37
+ * they are one worker's shim: the read seam and the retrieval seam the cap
38
+ * steers a capped worker towards.
39
+ * @param {{cap?: number, arm?: ReadShimArm}} [opts] `cap`: delivery bound in bytes (the
40
+ * shipped value is `READ_SHIM_CAP`; the option exists so the native path can pin the
41
+ * two bounds to one number rather than two constants drifting apart). `arm`: which
42
+ * levers are live (`readShimArm`). Defaults to ALL of them, so every caller written
43
+ * before the arms existed keeps the behaviour it was written against.
44
+ */
45
+ export function createReadShim({ cap, arm }?: {
46
+ cap?: number;
47
+ arm?: ReadShimArm;
48
+ }): {
49
+ arm: ReadShimArm;
50
+ cap: number;
51
+ wrapRead: (tool: {
52
+ name: string;
53
+ execute: (args: any) => Promise<any>;
54
+ }) => {
55
+ name: string;
56
+ execute: (args: any) => Promise<any>;
57
+ };
58
+ serveStale: ({ file, path, startLine, endLine, detail }: StaleReq) => StaleServe | null;
59
+ };
60
+ /**
61
+ * The one-tool spelling, kept for every caller written before the shim became an
62
+ * object: wrap this read tool with its own private ledger and hand it back.
63
+ * @param {{name: string, execute: (args: any) => Promise<any>}} tool
64
+ * @param {{cap?: number, arm?: ReadShimArm}} [opts]
65
+ * @returns {{name: string, execute: (args: any) => Promise<any>}} the same tool
66
+ */
67
+ export function wrapReadTool(tool: {
68
+ name: string;
69
+ execute: (args: any) => Promise<any>;
70
+ }, opts?: {
71
+ cap?: number;
72
+ arm?: ReadShimArm;
73
+ }): {
74
+ name: string;
75
+ execute: (args: any) => Promise<any>;
76
+ };
77
+ /** The delivery cap, in bytes. Equal to `NATIVE_READ_CAP` by construction — the
78
+ * native path's CLI display bound and this cap bound the SAME seam, so one
79
+ * number keeps them from double-truncating each other (see planrun's wiring). */
80
+ export const READ_SHIM_CAP: number;
81
+ /** The legal spellings, in one place so an error message cannot drift from the
82
+ * set it describes. */
83
+ export const READ_SHIM_ARMS: readonly string[];
84
+ /** F19 at the bound rather than the verb (the NATIVE_READ_STRATEGY precedent):
85
+ * a cap the worker does not know about is a worker that reads the same file
86
+ * three times wondering why it keeps starting over. It states the bound, what a
87
+ * re-read does, and the two verbs that read a symbol whole — the capability the
88
+ * cap makes mandatory (G1). */
89
+ export const READ_SHIM_STRATEGY: "\nREAD LIMIT: a shell_read of a file over 24KB returns only the next 24KB of it, followed by a notice saying which bytes you got. This is NOT the whole file. Reading the same path again continues where the last read stopped, and once you hold all of it a re-read says so instead of re-sending the bytes. To read a function IN FULL, use ctx_recall(<symbol>) then ctx_get(<pointer>) \u2014 that returns the entire function however large its file is. To locate a line, use shell_grep(<pattern>). Never try to understand a large file by reading it whole.";
90
+ /** The DIFF arm's own line. It says nothing about a 24KB limit and nothing about
91
+ * continuation reads, because under A2 neither exists — a persona describing
92
+ * machinery that is off is a lie to the worker, and a worker rationing its reads
93
+ * against an imaginary cap is a different treatment than the arm names.
94
+ *
95
+ * It does not repeat the steer at ctx_recall/ctx_get either: that steer is the
96
+ * CAP's compensation (G1's other half), and offering it here would import half
97
+ * of A1 into A2. The delivery itself carries `diffNotice`'s framing inline; this
98
+ * line exists so the first diff is not a surprise. */
99
+ export const READ_SHIM_DIFF_STRATEGY: "\nRE-READS: if you shell_read a file you have already been shown in full and it has changed since, the result is a unified DIFF against the copy you already hold \u2014 not the file itself. It is labelled as one. Lines starting '-' were removed, '+' were added, ' ' are unchanged context; apply it to the copy you have and you hold the current file.";
100
+ export type StaleReq = {
101
+ file: string;
102
+ path: string;
103
+ startLine: number;
104
+ endLine: number;
105
+ detail: string;
106
+ };
107
+ export type StaleServe = {
108
+ text: string;
109
+ outcome: "stale-served" | "stale-absent" | "stale-past-eof";
110
+ };
111
+ export type ReadShimArm = {
112
+ on: boolean;
113
+ cap: boolean;
114
+ pointer: boolean;
115
+ diff: boolean;
116
+ g1: boolean;
117
+ };
package/types/reuse.d.ts CHANGED
@@ -136,7 +136,8 @@ export function reuseSpecHash(resolved: {
136
136
  * @param {boolean} [opts.forceCold]
137
137
  * @param {string[]} [opts.exclude] names already tried this run
138
138
  * @returns {Promise<any>} `{ choice, reason, called, refused, forcedCold, red, costUsd,
139
- * spendComplete, candidates, listing }`
139
+ * spendComplete, candidates, quarantineSkips, listing }` — `quarantineSkips` names every
140
+ * workflow held back for want of a signer's accept, with its reason (module 6)
140
141
  */
141
142
  export function selectBridge({ registry, job, ask, provider, pinned, shortlist, forceCold, exclude }: {
142
143
  registry: unknown;
@@ -164,8 +165,11 @@ export function selectBridge({ registry, job, ask, provider, pinned, shortlist,
164
165
  * 4. **Which envelope this spine belongs to** — the `reuse-start` record's own
165
166
  * approval hash, so a resume can prove it is continuing the SAME signed run.
166
167
  *
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
168
+ * **F45 governs the arithmetic here.** A window's spend is summed from
169
+ * `ACCOUNTED_ROUND_TYPES` ONLY — the ledger's OWN list (src/run.js), imported rather
170
+ * than re-spelled, so a record `runJob` charges and this reader has never heard of
171
+ * cannot silently widen a resumed run's ceiling by exactly that amount. It was one
172
+ * event (`worker-round`) until the close itself could spend (`judge-round`). The selection calls
169
173
  * are real money and are counted, but as the RUN's cost and never as a try's: a
170
174
  * picker's tokens are not a worker's, and attributing them to a try would misstate
171
175
  * both the fold and the row. `worker-turn` (native attribution, cost null by design)
@@ -360,6 +364,7 @@ export function resumeTreeGate({ head, seed, dirty }: {
360
364
  * @param {number} [opts.strikeLimit] forwarded (a plan step's strike ceiling)
361
365
  * @param {number} [opts.closeTimeoutMs] forwarded
362
366
  * @param {boolean} [opts.layerRoot] forwarded
367
+ * @param {boolean|'cap'|'diff'} [opts.readShim] the read shim's ARM (A0/A1/A2/A3), forwarded verbatim
363
368
  * @param {any} [opts.resume] RESUME (module C) — a `readResume` reading of the KILLED
364
369
  * run's own spine. Its `approvalHash` must match this run's, or the run refuses:
365
370
  * a resume continues ONE signed run, not any run.
@@ -393,10 +398,148 @@ export function runReuse(opts: {
393
398
  strikeLimit?: number | undefined;
394
399
  closeTimeoutMs?: number | undefined;
395
400
  layerRoot?: boolean | undefined;
401
+ readShim?: boolean | "cap" | "diff" | undefined;
396
402
  resume?: any;
397
403
  runJob?: typeof shippedRunJob | undefined;
398
404
  now?: (() => number) | undefined;
399
405
  }): Promise<any>;
406
+ /**
407
+ * WHAT A GREEN WRITES, in ONE place. Lifted verbatim out of `runReuse`'s own
408
+ * closure when the U runner needed it too (2B) — two spellings of "what a green
409
+ * writes" would be the two-transforms class applied to the ledger itself, which is
410
+ * the failure `greenParts` exists one level down to prevent.
411
+ *
412
+ * Two legs, and the split is which caller already holds an entry:
413
+ * - A BRIDGE IN HAND (the reuse leg) APPENDS. No shape check is needed: `runJob`'s
414
+ * load gate already proved at the door, before a token, that this entry is the
415
+ * same kind of recipe as this job (a mismatch returns `recipe-stale` and never
416
+ * gets here).
417
+ * - NO BRIDGE (the cold leg, and every U run) mints — unless a name already holds
418
+ * greens, in which case it APPENDS, because a cold run of a job that already has
419
+ * a bridge is another version of that workflow and overwriting it would destroy
420
+ * the green that minted it.
421
+ *
422
+ * But the name is the JOB's slug, and a job's CLOSE can be re-signed under that same
423
+ * slug. Appending across two close shapes does two wrong things at once: it counts a
424
+ * green another close rendered toward this entry's distinct-patient status (a false
425
+ * `proven`), and it hands the load gate — which matches on exactly these stage names
426
+ * — a plan that satisfied a different verification. So the shape is checked with the
427
+ * gate's own predicate, and a mismatch neither appends NOR discards the green: it
428
+ * forks to a deterministic derived name, with the fork ON the record.
429
+ *
430
+ * THE HOLD IS NOT DECIDED HERE. `quarantined` comes off `greenParts` reading the
431
+ * class the run was signed under (`record.verdictType`, module 6), so a caller that
432
+ * forgets to pass it mints an UNHELD green rather than a held one — which is why
433
+ * both callers pass the spec's own field and never a literal.
434
+ *
435
+ * KNOWN LIMIT, named rather than discovered later: `appendGreen` carries the entry's
436
+ * LABEL (`goal`, `closeStageNames`, `toolsUsed`) forward untouched, so a new version
437
+ * whose plan uses a verb the founding green did not is not reflected in `toolsUsed`.
438
+ * The load gate reads that field, so the gate's verb check goes slightly stale across
439
+ * versions. It is not a safety hole — the gate only ever gets more permissive there,
440
+ * and the TWEAKED plan still passes the full `validatePlan` against the job's signed
441
+ * menu at draft time, which is the check that actually decides. Fixing it properly
442
+ * means `appendGreen` taking a label update, which is module 1's contract and hamr's
443
+ * call, not something to change from the caller.
444
+ *
445
+ * Returns the WRITE RECORD both runners report — never throws, and a refusal is a
446
+ * record with `action: 'none'` and named reds rather than an absence a reader has to
447
+ * branch on.
448
+ * @param {{registryDir: string, bridge?: any|null, meta: any, record: any}} o
449
+ * @returns {{name: string|null, action: string, file: string|null, reds: any[], [k: string]: any}}
450
+ */
451
+ export function writeGreenRow({ registryDir, bridge, meta, record }: {
452
+ registryDir: string;
453
+ bridge?: any | null;
454
+ meta: any;
455
+ record: any;
456
+ }): {
457
+ name: string | null;
458
+ action: string;
459
+ file: string | null;
460
+ reds: any[];
461
+ [k: string]: any;
462
+ };
463
+ /**
464
+ * THE U RUNNER'S TERMINAL WRITE (2B) — does the run that just ended earn a registry
465
+ * row, and if so, write it.
466
+ *
467
+ * `scripts/run-u.mjs` has always kept standalone bridge FILES and never a registry,
468
+ * so its review door could describe a held learning credit it had no row to release:
469
+ * `--decide accept` reached `recordDoor`, found no green row for the runid and died
470
+ * `no-row-for-run`. This is the row, and STORAGE IS ALL IT IS — selection, promotion
471
+ * and reuse execution stay parked (layer-3-reuse). Nothing here reads a row back.
472
+ *
473
+ * FOUR REFUSALS, each one a guard that already existed somewhere and is kept:
474
+ * - NO REGISTRY, no row. The registry path is operator-supplied and never
475
+ * conjured; without one the door says, in words, that nothing was released.
476
+ * - ALREADY-GREEN mints nothing. The close was satisfied before this run did
477
+ * anything, so nothing here earned a version — the reuse-credit leak block, from
478
+ * the writing side rather than the door's.
479
+ * - A GREEN WITH NO PLAN mints nothing. The artifact that inherits is the one that
480
+ * RAN, and there is none (R1).
481
+ * - A CLASS WHOSE CREDIT IS NOT HELD mints nothing HERE. That is `green` today, and
482
+ * it is main's behaviour kept rather than a new rule: this runner has never
483
+ * written a registry row for a green-class run, and widening what enters the
484
+ * registry is a reuse-rung question, not a side effect of fixing a door. The
485
+ * predicate is `quarantinesCredit` — the ONE spelling of "is this class held" —
486
+ * so the row this runner mints and the row a door releases can never disagree
487
+ * about which classes they are talking about.
488
+ *
489
+ * The row is born HELD for exactly the reason any judged green is: `greenParts` reads
490
+ * `verdictType` off the record. Nothing here sets the flag.
491
+ * @param {{registryDir: string|null|undefined, job: any, name: string|null,
492
+ * outcome: string, plan: any, record: any}} o
493
+ * @returns {{minted: boolean, reason: string|null, write: any|null}}
494
+ */
495
+ export function writeRunGreenRow({ registryDir, job, name, outcome, plan, record }: {
496
+ registryDir: string | null | undefined;
497
+ job: any;
498
+ name: string | null;
499
+ outcome: string;
500
+ plan: any;
501
+ record: any;
502
+ }): {
503
+ minted: boolean;
504
+ reason: string | null;
505
+ write: any | null;
506
+ };
507
+ /**
508
+ * Apply a signer's door decision to the workflow a run wrote its row against:
509
+ * record the disposition, and — on `accept` over a HELD judged green — release the
510
+ * learning credit that green has been carrying unspent (PRD v1.71 §3).
511
+ *
512
+ * This is the ONE seam between a person's answer and the box. It is deliberately
513
+ * separate from `runReuse`: the door opens AFTER the run has ended (a green run is
514
+ * over, non-blocking, and the person may answer minutes or days later), so the
515
+ * decision cannot ride the run's own return path. The runner calls this with the
516
+ * runid it printed at the door.
517
+ *
518
+ * Never throws and never conjures: a missing registry, a missing entry, a
519
+ * malformed one and a runid with no green row all come back as named reds. The
520
+ * loop's verdict is untouched in every branch — this writes a disposition, never a
521
+ * result.
522
+ *
523
+ * @param {object} opts
524
+ * @param {string} opts.registryDir the operator-supplied registry directory
525
+ * @param {string} opts.name the workflow the run wrote to
526
+ * @param {string} opts.runid the RUN's id, exactly as the bridge row records it
527
+ * @param {string} opts.decision one of the three doors
528
+ * @param {string|null} [opts.at] when the person answered
529
+ * @returns {{ok: boolean, reds: any[], released: boolean, file: string|null}}
530
+ */
531
+ export function applyDoorDecision({ registryDir, name, runid, decision, at }: {
532
+ registryDir: string;
533
+ name: string;
534
+ runid: string;
535
+ decision: string;
536
+ at?: string | null | undefined;
537
+ }): {
538
+ ok: boolean;
539
+ reds: any[];
540
+ released: boolean;
541
+ file: string | null;
542
+ };
400
543
  /** @typedef {{code: string, path: string, detail?: string}} Red */
401
544
  /**
402
545
  * The GRADED-RED set: the runJob outcomes in which the operator's close actually
@@ -0,0 +1,81 @@
1
+ /** the door's own record, off the run's spine — the LAST one, because a run that
2
+ * ends at a door writes exactly one and a reader that guesses which is which is a
3
+ * reader that will one day answer the wrong question.
4
+ * @param {any[]} events the run's spine, parsed
5
+ * @returns {any|null} */
6
+ export function doorRecordOf(events: any[]): any | null;
7
+ /**
8
+ * IS THE DOOR STILL OPEN? The same 60-day TTL a hitl checkpoint keeps
9
+ * (`PAUSE_TTL_MS`) and deliberately the same NUMBER — the door replaced `cancel`
10
+ * precisely because an unanswered door and an unresumed pause are one state, and
11
+ * two TTLs for one state is how they come to disagree.
12
+ *
13
+ * An UNREADABLE stamp refuses too: unknown is not young (F6). Reading the age is
14
+ * not the same as deciding on it, so this states both the answer and the arithmetic.
15
+ * @param {any} door the door record
16
+ * @param {{now?: () => number, ttlMs?: number}} [opts]
17
+ * @returns {{ok: boolean, ageMs: number|null, ttlMs: number, detail: string}}
18
+ */
19
+ export function doorAgeGate(door: any, { now, ttlMs }?: {
20
+ now?: () => number;
21
+ ttlMs?: number;
22
+ }): {
23
+ ok: boolean;
24
+ ageMs: number | null;
25
+ ttlMs: number;
26
+ detail: string;
27
+ };
28
+ /**
29
+ * Answer a run's review door.
30
+ *
31
+ * Never throws and never conjures: a malformed decision, a run that opened no
32
+ * door, an expired one, a tree that moved and a registry that is not there all
33
+ * come back as named reds. The loop's verdict is untouched on every path — this
34
+ * function has no way to write one.
35
+ *
36
+ * @param {object} opts
37
+ * @param {any} opts.job the SIGNED spec the run was judged by (its close is what an accept re-proves)
38
+ * @param {string} opts.workdir the patient, as it stands NOW
39
+ * @param {any[]} opts.events the run's own spine, parsed
40
+ * @param {string} opts.decision one of the three doors
41
+ * @param {string|null} [opts.text] the rerun's words — the gap, and refused when empty
42
+ * @param {number} [opts.closeTimeoutMs] the per-stage bound for the accept's re-run
43
+ * @param {string|null} [opts.registryDir] the workflow registry, when the runner keeps one
44
+ * @param {string|null} [opts.name] the workflow this run wrote its row against
45
+ * @param {string|null} [opts.runid] the RUN's id, exactly as the bridge row records it
46
+ * @param {string|null} [opts.at] when the person answered
47
+ * @param {() => number} [opts.now] the clock, injected for the TTL
48
+ * @param {number} [opts.ttlMs]
49
+ * @param {((type: string, data: any) => any)|null} [opts.emit] the run's spine, to record the disposition on
50
+ * @returns {Promise<{ok: boolean, decision: string|null, next: string|null, released: boolean,
51
+ * doorRecorded: boolean, reds: any[], mechanical: any, door: any, text: string|null,
52
+ * options: string[], ttlMs: number, note: string|null}>}
53
+ */
54
+ export function answerReviewDoor({ job, workdir, events, decision, text, closeTimeoutMs, registryDir, name, runid, at, now, ttlMs, emit, }: {
55
+ job: any;
56
+ workdir: string;
57
+ events: any[];
58
+ decision: string;
59
+ text?: string | null | undefined;
60
+ closeTimeoutMs?: number | undefined;
61
+ registryDir?: string | null | undefined;
62
+ name?: string | null | undefined;
63
+ runid?: string | null | undefined;
64
+ at?: string | null | undefined;
65
+ now?: (() => number) | undefined;
66
+ ttlMs?: number | undefined;
67
+ emit?: ((type: string, data: any) => any) | null | undefined;
68
+ }): Promise<{
69
+ ok: boolean;
70
+ decision: string | null;
71
+ next: string | null;
72
+ released: boolean;
73
+ doorRecorded: boolean;
74
+ reds: any[];
75
+ mechanical: any;
76
+ door: any;
77
+ text: string | null;
78
+ options: string[];
79
+ ttlMs: number;
80
+ note: string | null;
81
+ }>;
package/types/run.d.ts CHANGED
@@ -34,6 +34,13 @@
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 {any} [opts.judgeProvider] SOFTGREEN — the provider a JUDGED close stage runs its
38
+ * locate call through, wired by the operator and PINNED to `JUDGE_MODEL` (src/judged.js:
39
+ * the only tier with established injection resistance upstream). Forwarded to the plan
40
+ * flow. It is its own seam and not `provider`/`providerFor`, because the judge tier is
41
+ * never a step knob and never agent-selectable; absent, a judged stage instrument-stops as
42
+ * a wiring gap rather than grading on whatever binding happened to be at hand. Its spend
43
+ * lands on this ledger as `judge-round` (see `ACCOUNTED_ROUND_TYPES`).
37
44
  * @param {number} [opts.capRuns] shell-owned iteration cap for the plan flow's
38
45
  * CLOSE-FIX loop (a plan STEP runs under the strike ladder instead)
39
46
  * @param {number} [opts.strikeLimit] shell-owned strike ceiling for a plan step's ladder
@@ -90,6 +97,19 @@
90
97
  * collision walk would mint a `-2` beside the work the resume exists to continue. The
91
98
  * fold's precedent exactly: the chain's state is DECLARED, never re-derived. Forwarded
92
99
  * verbatim to the plan flow; absent is the cold path.
100
+ * @param {boolean|'cap'|'diff'} [opts.readShim=false] THE READ SHIM (src/readshim.js) —
101
+ * WHICH ARM to run, threaded to the plan flow. The Phase 2 pre-registration's four:
102
+ * `false` A0 — off (the default)
103
+ * `'cap'` A1 — cap + pointer + next-unseen-slice + G1, no diff
104
+ * `'diff'` A2 — the diff lever alone: no cap, no pointer, no G1
105
+ * `true` A3 — every lever
106
+ * Anything else THROWS below, before a token is spent: a mis-spelled arm coerced
107
+ * by truthiness into A3 would run one treatment under another's label and be
108
+ * invisible in the results afterwards.
109
+ * OFF is byte-identical to the pre-shim run in every observable (G1 included): the
110
+ * frozen A0 baseline arm has to be exactly today, so a guard firing under a disabled
111
+ * shim would make the baseline a treatment arm. The default-flip belongs to a
112
+ * paid contrast nobody has approved yet (the `layerRoot` precedent, F41).
93
113
  * @param {boolean} [opts.layerRoot=false] Layer R (within-run ratchet) — shell
94
114
  * territory, threaded to the plan flow. Defaults OFF
95
115
  * (decided 2026-07-21): fixation is extinct on every current job (F41), so
@@ -102,13 +122,26 @@
102
122
  * RESUMES a paused run and forwarded verbatim to the plan flow. Absent on every
103
123
  * ordinary run; never authored and never defaulted (a defaulted answer to "is this
104
124
  * done?" is the rubber-stamp the class exists to prevent, 2026-08-12 §4).
125
+ * @param {{decision: string, text?: string|null, receivedAt?: string|null}|null} [opts.heldRuling]
126
+ * F102 — the answer the CHECKPOINT holds (`readResume`'s `restart.pendingDecision`):
127
+ * a decision a person already gave on a leg that stopped before a single round was
128
+ * bought for it. Forwarded verbatim; the plan flow applies it exactly as a fresh one
129
+ * and refuses a leg handed both. It is also what decides whether the TIME fold this
130
+ * run declares is the chain's or this engagement's (F103).
131
+ * @param {boolean|null} [opts.reviewDoor] MODULE 8 — does this run END at the review
132
+ * door (PRD v1.71 §3)? Forwarded verbatim to the plan flow, which owns the class
133
+ * default. It changes what the run RECORDS and never what it returns: the door is a
134
+ * disposition and the loop's verdict is not the door's to touch.
135
+ * @param {{text: string, fromRunid?: string|null, receivedAt?: string|null}|null} [opts.doorRerun]
136
+ * MODULE 8 — this leg IS the fresh engagement a signer commissioned at a previous
137
+ * run's review door, carrying their words. Forwarded verbatim.
105
138
  * @returns {Promise<string>} outcome: 'green' | 'already-green' | 'escalated' |
106
139
  * 'unapproved-spec' | 'job-red' | 'smoke-red' | 'plan-red' | 'check-red' |
107
140
  * 'close-red' | 'close-unsupported' | 'recipe-stale' | 'branch-red' | 'pricing-red' | 'provider-red' |
108
141
  * 'interpreter-red' | 'cap-halt' | 'wall-halt' | 'step-stalled' |
109
142
  * 'hitl-pause' | 'hitl-decision-red' | `step-red:<id>`
110
143
  */
111
- export function runJob(rawSpec: object | string, { approvals, workdir, provider, nativeProvider, providerFor, emit, capRuns, strikeLimit, shellCapUsd, closeTimeoutMs, layerRoot, bridge, priorSpentUsd, priorSpendComplete, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling }: {
144
+ export function runJob(rawSpec: object | string, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider, emit, capRuns, strikeLimit, shellCapUsd, closeTimeoutMs, layerRoot, readShim, bridge, priorSpentUsd, priorSpendComplete, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun }: {
112
145
  approvals: unknown;
113
146
  workdir: string;
114
147
  provider: any;
@@ -120,6 +153,7 @@ export function runJob(rawSpec: object | string, { approvals, workdir, provider,
120
153
  }) => any) | undefined;
121
154
  emit: (type: string, data?: object) => object;
122
155
  providerFor?: ((tier: string) => any) | undefined;
156
+ judgeProvider?: any;
123
157
  capRuns?: number | undefined;
124
158
  strikeLimit?: number | undefined;
125
159
  shellCapUsd?: number | undefined;
@@ -138,12 +172,40 @@ export function runJob(rawSpec: object | string, { approvals, workdir, provider,
138
172
  grantUsed?: boolean;
139
173
  } | null | undefined;
140
174
  resumeBranch?: string | null | undefined;
175
+ readShim?: boolean | "cap" | "diff" | undefined;
141
176
  layerRoot?: boolean | undefined;
142
177
  humanRuling?: {
143
178
  decision: string;
144
179
  text?: string;
145
180
  } | null | undefined;
181
+ heldRuling?: {
182
+ decision: string;
183
+ text?: string | null;
184
+ receivedAt?: string | null;
185
+ } | null | undefined;
186
+ reviewDoor?: boolean | null | undefined;
187
+ doorRerun?: {
188
+ text: string;
189
+ fromRunid?: string | null;
190
+ receivedAt?: string | null;
191
+ } | null | undefined;
146
192
  }): Promise<string>;
193
+ /** @typedef {{code: string, path: string, detail?: string}} Red */
194
+ /**
195
+ * THE ROUND RECORDS THE ONE LEDGER SUMS — as data, because two of them now exist
196
+ * and a hand-spelled condition per record is how a paid call comes to be free.
197
+ *
198
+ * `worker-round` is the attempt's own spend, metered per ROUND rather than per
199
+ * attempt-return (F12: a halted attempt's spend was invisible by 300×).
200
+ * `judge-round` is the CLOSE's, and it is the first spend a close has ever had:
201
+ * the judged floor (softgreen) buys a pinned locate call per artifact, and the
202
+ * budget-identity rule is that a budget funds the attempt PLUS its close. Both
203
+ * carry the honest `costUsd` — a null lands as `unpriced` and halts, never as $0.
204
+ *
205
+ * What is deliberately NOT here: `worker-result` / `worker-plan`, which are
206
+ * attempt TOTALS of the same rounds and would double-count.
207
+ */
208
+ export const ACCOUNTED_ROUND_TYPES: readonly string[];
147
209
  export type Red = {
148
210
  code: string;
149
211
  path: string;