bareloop 0.11.0 → 0.12.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,280 @@
1
+ /**
2
+ * The rubric card: Q6's answer compiled into enumerated items, signed by the
3
+ * person and enumerated in the spec hash. `rule` selects from `JUDGE_RULES`;
4
+ * `text` is the signer's own words, carried so a red can be explained in them.
5
+ *
6
+ * Wherever a schema takes free text but only a closed set can satisfy it, hand
7
+ * over the enumerated set — so `rule` is a SELECT and `text` is the only free
8
+ * field, and it decides nothing.
9
+ * @param {any} card
10
+ * @returns {{ok: boolean, reds: string[]}}
11
+ */
12
+ export function validateCard(card: any): {
13
+ ok: boolean;
14
+ reds: string[];
15
+ };
16
+ /**
17
+ * THE CALIBRATION SET's gate. Never throws on proposed input — every failure is a
18
+ * named red in the shipped `{ok, reds}` shape.
19
+ *
20
+ * A CASE is `{id, artifact, expect: {verdict, reds: [{rule, fn}]}}`:
21
+ * `artifact` is REAL TEXT — what LOCATE will actually be pointed at, never a
22
+ * description of it; `verdict` is what `decide()` must render; and `reds` is the
23
+ * ITEMIZED expectation, because the floor is *graded correctly WITH ITEMIZED
24
+ * REDS* — a pipe that reds the right case for the wrong reason has not been
25
+ * calibrated, it has been lucky. `rule` selects from the CARD's own lines (the
26
+ * §4.3 ceiling made mechanical: the judge only ever checks lines the card
27
+ * names) and `fn` is the address `decide()` itself reports.
28
+ *
29
+ * THE SIZE and THE POLARITY are separate reds on purpose: one is hamr's
30
+ * threshold, the other is *the test must be able to FAIL*. A set of ten that can
31
+ * only pass is exactly as unusable as a set of nine, and one red for both would
32
+ * send an operator to fix the wrong half.
33
+ *
34
+ * `card` is REQUIRED and its absence is a red rather than a skipped check — a
35
+ * validator handed no card would report a set it never examined against anything.
36
+ * @param {any} cases @param {{card?: any}} [o]
37
+ * @returns {{ok: boolean, reds: {code: string, path: string, detail: string, [k: string]: any}[]}}
38
+ */
39
+ export function validateCalibrationSet(cases: any, { card }?: {
40
+ card?: any;
41
+ }): {
42
+ ok: boolean;
43
+ reds: {
44
+ code: string;
45
+ path: string;
46
+ detail: string;
47
+ [k: string]: any;
48
+ }[];
49
+ };
50
+ /**
51
+ * BOTH signed artifacts, judged independently and reported together. The card
52
+ * goes through `validateCard` rather than a second spelling of it.
53
+ * @param {{card: any, cases: any}} o
54
+ * @returns {{ok: boolean, reds: {code: string, path: string, detail: string, [k: string]: any}[]}}
55
+ */
56
+ export function validateJudgedArtifacts({ card, cases }: {
57
+ card: any;
58
+ cases: any;
59
+ }): {
60
+ ok: boolean;
61
+ reds: {
62
+ code: string;
63
+ path: string;
64
+ detail: string;
65
+ [k: string]: any;
66
+ }[];
67
+ };
68
+ /**
69
+ * A `decide()` result reduced to the shape a case STORES. ONE spelling, so the
70
+ * expectation a signer signs and the reading module 5 takes off the live pipe are
71
+ * one shape read twice rather than two that can disagree.
72
+ *
73
+ * `why` and `quote` are deliberately dropped: both are the judge's prose about
74
+ * one emission, no signer can predict either, and pinning them would fail an
75
+ * honest pipe on wording. What survives is the ADDRESS — which card line, which
76
+ * function — which is what "itemized reds" means.
77
+ * @param {any} decision
78
+ * @returns {{verdict: string, reds: {rule: string, fn: string}[]}}
79
+ */
80
+ export function expectedOf(decision: any): {
81
+ verdict: string;
82
+ reds: {
83
+ rule: string;
84
+ fn: string;
85
+ }[];
86
+ };
87
+ /**
88
+ * The SHAPE gate on a locate emission, and only the shape: is this the object
89
+ * the rest of the pipe can read at all? A shape miss is an ARTIFACT-RED — the
90
+ * emission is broken and the caller may buy another one.
91
+ *
92
+ * What is deliberately NOT here: per-rule field presence, and an empty function
93
+ * list. Both are well-formed emissions whose CONTENT is unusable, and content
94
+ * is `decide()`'s to red on (contract (a)) — routing them here would let a
95
+ * retry ladder spend money re-asking a model that answered perfectly clearly.
96
+ * It takes the facts and NOTHING else — no card. The shape of a locate emission
97
+ * is the same shape whatever the card asked for, and a second parameter nobody
98
+ * reads is a signature promising a coupling that does not exist.
99
+ * @param {any} facts
100
+ * @returns {{ok: boolean, reds: string[]}}
101
+ */
102
+ export function validateFacts(facts: any): {
103
+ ok: boolean;
104
+ reds: string[];
105
+ };
106
+ /**
107
+ * The locate prompt for ONE card — the only derivation in this module, and it
108
+ * runs card → prompt, one hop, one direction. A rule the card did not name buys
109
+ * no prompt tokens and produces no facts nobody reads.
110
+ * @param {any} card @returns {string}
111
+ */
112
+ export function locatePrompt(card: any): string;
113
+ /**
114
+ * ONE locate attempt. The caller owns the ladder — this function has no retry,
115
+ * no cap and no opinion about how many times it should be called, because a
116
+ * retry is a spend decision and spend decisions are the arbiter's.
117
+ *
118
+ * THE AXIS PRECEDENCE, and the order is load-bearing:
119
+ * 1. THE CALL FAILED → `provider-red`. A thrown or errored call has no
120
+ * knowable cost, so asking the meter about it would mislabel a transport
121
+ * casualty as a broken meter and send the operator to fix the wrong thing.
122
+ * 2. THE COST IS NULL → `pricing-red`. Checked BEFORE the emission is even
123
+ * looked at: a retry of an unpriced call buys more spend nobody can see,
124
+ * so this is the non-retryable stop and it outranks every retryable one.
125
+ * 3. TRUNCATED → `provider-red`, on its own field, never folded into a parse
126
+ * error. "The model was cut off" and "the model wrote nonsense" are two
127
+ * findings, and one field spelling both is how a reader confuses them.
128
+ * 4. UNPARSEABLE or SHAPE-BROKEN → `artifact-red`. No repair, ever.
129
+ *
130
+ * On every route out, red or clean, `onCost` fires exactly once with the honest
131
+ * read: a paid call that leaves no meter record is F12 wearing a judge's coat.
132
+ * @param {{artifactText: string, card: any, loopFactory: (o: {provider?: any, system: string}) => any,
133
+ * maxTokens?: number, attempt?: number, onCost?: (c: {costUsd: number|null, unpricedRounds: number}) => void}} o
134
+ * @returns {Promise<{ok: boolean, facts: any|null, red: {axis: string, detail: string}|null,
135
+ * costUsd: number|null, unpricedRounds: number, truncated: boolean, parseError: boolean,
136
+ * raw: ReturnType<typeof scrubRaw>}>}
137
+ */
138
+ export function runLocate({ artifactText, card, loopFactory, maxTokens, attempt, onCost }: {
139
+ artifactText: string;
140
+ card: any;
141
+ loopFactory: (o: {
142
+ provider?: any;
143
+ system: string;
144
+ }) => any;
145
+ maxTokens?: number;
146
+ attempt?: number;
147
+ onCost?: (c: {
148
+ costUsd: number | null;
149
+ unpricedRounds: number;
150
+ }) => void;
151
+ }): Promise<{
152
+ ok: boolean;
153
+ facts: any | null;
154
+ red: {
155
+ axis: string;
156
+ detail: string;
157
+ } | null;
158
+ costUsd: number | null;
159
+ unpricedRounds: number;
160
+ truncated: boolean;
161
+ parseError: boolean;
162
+ raw: ReturnType<typeof scrubRaw>;
163
+ }>;
164
+ /**
165
+ * Render the per-item verdicts over one locate result. PURE: same input, same
166
+ * output, forever — which is what makes a calibration set mean anything.
167
+ *
168
+ * FIRST-RED-WINS, in SIGNED CARD ORDER. Every item is still EVALUATED after the
169
+ * first red (calibration demands itemized reds, not a paragraph and not a short
170
+ * circuit); `firstRed` is the composed stage verdict's reason, and its
171
+ * stability comes from the card's own order rather than from whatever order the
172
+ * model happened to emit its functions in.
173
+ *
174
+ * `artifactText` is OPTIONAL and its absence is honest rather than lenient:
175
+ * with the artifact in hand every quote a rule consumes is verified to exist in
176
+ * it, and without it `decide()` simply does not make a claim it cannot support.
177
+ * Callers that HAVE the authoritative artifact (which is every real caller,
178
+ * since the same text was just judged) should always pass it.
179
+ * @param {any} facts a `runLocate` result's `facts`, or null
180
+ * @param {any} card
181
+ * @param {{artifactText?: string|null}} [opts]
182
+ * @returns {{verdict: 'pass'|'red', items: {rule: string, text: string, ok: boolean, reds: RuleRed[]}[],
183
+ * firstRed: string|null, reason: string|null}}
184
+ */
185
+ export function decide(facts: any, card: any, { artifactText }?: {
186
+ artifactText?: string | null;
187
+ }): {
188
+ verdict: "pass" | "red";
189
+ items: {
190
+ rule: string;
191
+ text: string;
192
+ ok: boolean;
193
+ reds: RuleRed[];
194
+ }[];
195
+ firstRed: string | null;
196
+ reason: string | null;
197
+ };
198
+ /**
199
+ * THE JUDGE MODEL, PINNED. Never agent-selectable and never a step knob: it is
200
+ * the only tier with established injection resistance upstream (BA-20's
201
+ * INJECTION_BATTERY is haiku-4.5 only), and §4.2's whole safety argument is
202
+ * worth exactly as much as that evidence. A judge-model bump forces a full
203
+ * recalibration — mandatory, per the model-bump dead-weight replay rule.
204
+ *
205
+ * THAT IS ENFORCED, not asked for, and it is enforced in three places because a
206
+ * rule with no wired detector is prose (F45):
207
+ * 1. the value here is STORED with the set it graded — `foldJudgedArtifacts`
208
+ * writes it as `closeDecl.calibration.judgeModel`, inside the spec, so
209
+ * `jobSpecHash` covers it by construction. Bump this constant and every
210
+ * signed judged spec hashes differently: the signature dies, and re-signing
211
+ * is what re-runs the calibration gate.
212
+ * 2. `validateCloseDecl` REQUIRES it on any stored set — a set nobody can
213
+ * attribute to a tier is a floor nobody can tell apart from a bumped one.
214
+ * 3. `runJudgedFloor` refuses to grade when the stored tier is not this one,
215
+ * naming both models. It does not degrade in either direction: grading on
216
+ * the new tier judges against a floor nobody calibrated, and the old tier is
217
+ * not on offer.
218
+ */
219
+ export const JUDGE_MODEL: "claude-haiku-4-5";
220
+ /**
221
+ * The locate call's token ceiling. The POC's two real artifacts came back well
222
+ * inside 2000; this doubles that for headroom on a longer artifact, and the
223
+ * direction is fail-safe either way — a ceiling that binds produces a TRUNCATED
224
+ * emission, which is a red with its own field, never a silent partial pass. A
225
+ * caller may override it per call; it is a default, not a threshold.
226
+ */
227
+ export const JUDGE_MAX_TOKENS: 4000;
228
+ /** the label every locate call is metered and persisted under — one spelling */
229
+ export const LOCATE_LABEL: "judged-locate";
230
+ /**
231
+ * The THREE axes a locate attempt can fail on, enumerated because the caller
232
+ * ROUTES on them and a route decided by reading prose is the class this
233
+ * programme keeps paying for. All three are shipped bareloop classes, borrowed
234
+ * with their existing meanings intact:
235
+ * - `pricing-red` the meter is broken. NOT retryable: a retry buys more
236
+ * spend nobody can see.
237
+ * - `provider-red` the call failed or was cut off. Retryable.
238
+ * - `artifact-red` the call succeeded and its emission is unusable.
239
+ * Retryable, under the CALLER's cap — this module runs one
240
+ * attempt and owns no ladder.
241
+ */
242
+ export const LOCATE_AXES: Readonly<{
243
+ PRICING: "pricing-red";
244
+ PROVIDER: "provider-red";
245
+ ARTIFACT: "artifact-red";
246
+ }>;
247
+ /**
248
+ * THE OWNED RULE TABLE. Frozen: the rulebook is the arbiter's, and a caller that
249
+ * could add a rule at runtime would be authoring the arbiter.
250
+ * @type {Readonly<Record<string, {id: string, ask: string, check: (fn: any, lines: Set<string>|null) => RuleRed[]}>>}
251
+ */
252
+ export const JUDGE_RULES: Readonly<Record<string, {
253
+ id: string;
254
+ ask: string;
255
+ check: (fn: any, lines: Set<string> | null) => RuleRed[];
256
+ }>>;
257
+ /** the enumerated set a card may name — handed over verbatim in every refusal */
258
+ export const JUDGE_RULE_IDS: readonly string[];
259
+ /**
260
+ * THE SIZE — hamr, 2026-08-18, verbatim: *"go build 10, we could double later"*.
261
+ *
262
+ * Not a number measurement found, and not one the agent may revisit: a size
263
+ * change is a spec-level threshold change and stays operator territory (the
264
+ * standing no-agent-threshold-picking rule). The second half of his sentence is
265
+ * part of the ruling — doubling later is explicitly allowed — which is why it is
266
+ * ONE constant read by the schema, the validator and the refusal alike.
267
+ */
268
+ export const CALIBRATION_SIZE: 10;
269
+ /** the two verdicts a case may expect — `decide()`'s own closed set */
270
+ export const CASE_VERDICTS: readonly string[];
271
+ export function defaultJudgeLoop({ provider, system }: {
272
+ provider: any;
273
+ system: string;
274
+ }): import("bare-agent").Loop;
275
+ export type RuleRed = {
276
+ fn: string;
277
+ why: string;
278
+ quote: string | null;
279
+ };
280
+ import { scrubRaw } from './text.js';
package/types/kinds.d.ts CHANGED
@@ -1,3 +1,22 @@
1
+ /**
2
+ * Does this run end at a review door? The operator's flag WINS in both
3
+ * directions and is never inferred: `true` opens it for any class, `false` shuts
4
+ * it for any class, and `null`/absent is the class default above.
5
+ * @param {any} job the signed spec (its `verdictType` is the class)
6
+ * @param {boolean|null} [reviewDoor] the operator's explicit choice, or null
7
+ * @returns {boolean}
8
+ */
9
+ export function doorOpens(job: any, reviewDoor?: boolean | null): boolean;
10
+ /**
11
+ * The stages an `accept` re-runs: the MECHANICAL ones, and never a judged floor
12
+ * or a person (`SEED_EXEMPT_KINDS`, ruling 8's set, for the same reason it was
13
+ * drawn — a machine may re-prove a machine's reading, and may not re-prove a
14
+ * judgment). hamr's N4 ruling: an accept re-runs the mechanical stages, because a
15
+ * door lives 60 days and nothing freezes a tree.
16
+ * @param {any[]|null|undefined} stages
17
+ * @returns {any[]}
18
+ */
19
+ export function mechanicalStages(stages: any[] | null | undefined): any[];
1
20
  /**
2
21
  * D8's seed, READ rather than typed: HEAD at run start. A close stores the
3
22
  * counting rule and never the number (D12), so which commit the run is measured
@@ -235,6 +254,60 @@ export function normalizeHumanRuling(ruling: any): {
235
254
  } | null;
236
255
  why: string | null;
237
256
  };
257
+ /**
258
+ * WHICH ANSWER IS THIS LEG ACTING ON — the fresh one, or the one the checkpoint
259
+ * held? F102's cure needs one seam that can say, because two callers ask the same
260
+ * question (`runJob` declares the fold on `job-start` before `runPlan` reads the
261
+ * ruling at all), and two spellings of "is this a fresh engagement" is how the
262
+ * record and the clock come to disagree about the same leg.
263
+ *
264
+ * THE AMBIGUITY IS REFUSED, NEVER MERGED. A checkpoint that holds a decision and a
265
+ * command line that carries another are two answers to one question. Merging them
266
+ * picks a winner nobody chose; overriding silently discards words a person typed
267
+ * (the F98/F28 class at a process boundary). So it comes back as a refusal that
268
+ * NAMES the held decision, and the operator resolves it — by answering the held
269
+ * one, or by starting a fresh run against the tree as it stands.
270
+ *
271
+ * `fresh` is F103's half: a `rerun` is a FRESH ENGAGEMENT (design record §3.4 —
272
+ * *"redo/rerun comes with new authoring for money+time and keeps accounting of
273
+ * this far and this session separate counters"*), and nothing else is. An `accept`
274
+ * commissions no work and a `pause` runs nothing, so neither opens one.
275
+ *
276
+ * A REVIEW DOOR's rerun (module 8) is the SAME fact arriving one level out, and it
277
+ * is folded in here rather than beside the clock, because "is this a fresh
278
+ * engagement" gets ONE spelling — a second one is how a record and a clock come to
279
+ * disagree about the same leg, which is the very thing this resolver exists to
280
+ * prevent. It is deliberately NOT read as a ruling: a `humanRuling` answers a
281
+ * close's human STAGE and is refused for a close that has none, so the door's words
282
+ * stay in their own parameter and only this reading is shared.
283
+ *
284
+ * PRECEDENCE, stated because both CAN be handed to one leg: a door rerun makes the
285
+ * engagement fresh whatever the ruling says, and the two are OR'd, never merged.
286
+ * Freshness is the FAIL-SAFE direction — a fresh clock costs a leg nothing, while
287
+ * inheriting a dead leg's wall is F103's measured, structurally-impossible
288
+ * engagement. The door's own validation (empty words are refused) stays where it
289
+ * is, at the seam that accepts it.
290
+ * @param {any} fresh the answer arriving on this invocation (`--decide`), or null
291
+ * @param {any} held the answer the CHECKPOINT carries (`readResume`'s
292
+ * `restart.pendingDecision`), or null. `receivedAt` is when the PERSON said it —
293
+ * carried through so the resumed leg records the receipt, not its own re-reading.
294
+ * @param {any} [doorRerun] MODULE 8 — the review door's rerun, when this leg IS the
295
+ * fresh engagement a signer commissioned at a previous run's door. Presence is the
296
+ * whole of what is read here; the words are the accepting seam's business.
297
+ * @returns {{ok: boolean, why: string|null, ruling: {decision: string, text: string|null}|null,
298
+ * source: 'operator'|'checkpoint'|null, receivedAt: string|null, fresh: boolean}}
299
+ */
300
+ export function resolveHumanRuling(fresh: any, held: any, doorRerun?: any): {
301
+ ok: boolean;
302
+ why: string | null;
303
+ ruling: {
304
+ decision: string;
305
+ text: string | null;
306
+ } | null;
307
+ source: "operator" | "checkpoint" | null;
308
+ receivedAt: string | null;
309
+ fresh: boolean;
310
+ };
238
311
  /**
239
312
  * Run ONE declared stage against a workdir at a seed.
240
313
  *
@@ -350,17 +423,45 @@ export const STOP_FAULTS: Readonly<{
350
423
  }>;
351
424
  export const AGGREGATES: readonly string[];
352
425
  export const SIGNS: readonly number[];
353
- /** every kind this executor implements. `judged-floor` / `human-confirms`
354
- * are catalogue entries and are deliberately absent here: declaring one is
355
- * counted demand (the request-red path), and reaching one at runtime is a
356
- * stop — never a red, and never a silent pass. */
426
+ /** every kind this executor implements. It is asserted EQUAL to the catalogue's
427
+ * own live half by the suite, both directions: a kind the catalogue offers and
428
+ * this executor cannot run is a close that stops at runtime, and a kind this
429
+ * executor runs that the catalogue never names is a capability nothing can
430
+ * reach. `judged-floor` joined at softgreen module 2 — the runner and the
431
+ * catalogue unlock in ONE commit for exactly that reason. */
357
432
  export const LIVE_KINDS: readonly string[];
433
+ /**
434
+ * The judged stage's ladder: ONE attempt plus ONE retry, and no more.
435
+ *
436
+ * The POC measured a malformed emission roughly 1 in 6, which is exactly the
437
+ * shape a single retry buys down and a longer ladder buys nothing more of — each
438
+ * further attempt is another paid call against the same unchanged artifact and
439
+ * the same unchanged prompt. It is a CEILING the arbiter owns, never a knob a
440
+ * declaration can widen: a stage that could re-ask a model forever is a stage
441
+ * that can spend a budget without ever rendering a verdict.
442
+ *
443
+ * A `pricing-red` is never retried at all (see `runJudgedFloor`): a retry of an
444
+ * unpriced call only buys more spend nobody can see (F6).
445
+ */
446
+ export const JUDGE_ATTEMPTS: 2;
447
+ /**
448
+ * The ceiling on how many artifacts ONE judged stage may buy a judgement of —
449
+ * the sibling of `MAX_TERMS` and `MAX_STAGES`, and here for the same reason: a
450
+ * runaway list is a bill, not a red. Enforced by the catalogue's validator
451
+ * before any token, so a declaration that names more is refused at the gate
452
+ * rather than discovered at the invoice.
453
+ */
454
+ export const MAX_JUDGED_PATHS: 8;
358
455
  /** RULING 8 — the stages the seed-verdict read (D12) must NOT run. A judged
359
456
  * number at seed is unstable and a person at seed is an interview, not a
360
457
  * measurement; their bar comes from calibration (soft-green) or from the person
361
458
  * themselves (hitl), never from the seed. The skip is RECORDED by name rather
362
459
  * than taken in silence (F59: absent is not empty), so `seedRead` still returns
363
460
  * one row per declared stage. */
461
+ /** the one kind a MODEL renders. Named once for the same reason `HUMAN_KIND` is:
462
+ * the authoring pipeline, the card, the catalogue and the close each key a
463
+ * decision on it, and five literals is how they come to disagree. */
464
+ export const JUDGED_FLOOR_KIND: "judged-floor";
364
465
  export const SEED_EXEMPT_KINDS: readonly string[];
365
466
  /** the one kind a PERSON renders — named once, because the runner keys three
366
467
  * decisions on it (is there a door to answer, which stage paused, whose ruling
@@ -377,6 +478,29 @@ export const HUMAN_KIND: "human-confirms";
377
478
  * here forces a forever-decision at the one moment the person has least reason
378
479
  * to make one. */
379
480
  export const HUMAN_DECISIONS: readonly string[];
481
+ /** the door's record type on the spine. It is a DISPOSITION, never a verdict:
482
+ * hamr's law, verbatim — *"it's important not to change the loop self verdict"*.
483
+ * The close mints the verdict before this is written, and nothing downstream of
484
+ * this record may change what the ledger recorded. */
485
+ export const REVIEW_DOOR: "review-door";
486
+ /** the terminals a door opens on: the verdict-bearing ones, and only those. A red,
487
+ * a casualty and a governance halt are not offered to a person as merchandise —
488
+ * they are already decision-ready in their own right, with their own levers. */
489
+ export const DOOR_OPEN_OUTCOMES: readonly string[];
490
+ /** the classes whose door opens WITHOUT being asked for. Soft-green is here
491
+ * because the judge is young: its green is quarantined at mint (module 6) and a
492
+ * person's `accept` is the only thing that releases the credit, so a run that
493
+ * never offered the door would hold its own credit forever.
494
+ *
495
+ * `green` is deliberately ABSENT, and that is the one operator-facing default in
496
+ * this module: a green-class run behaves exactly as it always has unless the
497
+ * runner asks for the door (`reviewDoor: true`, `--review-door`). PRD v1.71 §3
498
+ * says *"the door at the end of EVERY run"* and also that a green door is
499
+ * NON-BLOCKING, and the two readings differ only in whether an unasked-for door
500
+ * appears on an existing job's spine. Flipping it is one line — adding `'green'`
501
+ * here — and it is hamr's to flip, not this module's to assume. A constant, never
502
+ * a threshold; widening it is arbiter territory. */
503
+ export const REVIEW_DOOR_CLASSES: readonly string[];
380
504
  /**
381
505
  * The arbiter's book NAMES, one home. F98's mechanism was exactly this list
382
506
  * living as independent literals: the fence deny arrays (src/planrun.js,
@@ -405,6 +529,16 @@ export type Red = {
405
529
  * territory: a declaration parameterises kinds, never the contracts.
406
530
  * `humanRuling` is the SIGNER's answer at a hitl pause, carried in from the
407
531
  * runner — never authored, never defaulted, and absent on every ordinary run.
532
+ * `judgeLoop` is the PAID seam the judged stage runs its locate call through, and
533
+ * it is operator territory for the same reason the worker's provider is: this
534
+ * module owns no provider and never picks one, and the judge tier is PINNED
535
+ * (`JUDGE_MODEL`) rather than chosen. Absent is not a fall-back to some other
536
+ * model — the stage stops as a wiring gap, the same answer `runPlan` gives a
537
+ * native job with no native factory.
538
+ * `onJudgeCost` is the METER, and it is the only reason a judged stage can be
539
+ * funded honestly: this executor cannot reach the run's ledger, so every locate
540
+ * call's cost is reported OUT through it, once per call, on every route including
541
+ * the red ones — a paid call that leaves no meter record is F12 in a judge's coat.
408
542
  * `onStage` is a REPORTING seam and nothing else: the seed read spawns a real
409
543
  * toolchain per stage (a suite is the slow one) and a whole read is one opaque
410
544
  * await, so the shell gets told as each stage lands. It decides nothing, it is
@@ -424,6 +558,19 @@ export type Ctx = {
424
558
  decision: string;
425
559
  text?: string | null;
426
560
  } | null;
561
+ judgeLoop?: (o: {
562
+ system: string;
563
+ }) => any;
564
+ onJudgeCost?: (c: {
565
+ stage: string | null;
566
+ kind: string | null;
567
+ path: string;
568
+ attempt: number;
569
+ label: string;
570
+ model: string;
571
+ costUsd: number | null;
572
+ unpricedRounds: number;
573
+ }) => void;
427
574
  onStage?: (row: {
428
575
  stage: string | null;
429
576
  kind: string | null;
@@ -140,6 +140,12 @@ export function boundedNote(bounded: {
140
140
  * Rule A-v2's law (final write step only, framed wide). `priorChecks`: the predecessor
141
141
  * plan's carried checks, replan phase only — Rule B's law (keep every one). Empty/omitted
142
142
  * renders byte-identically to the pre-rules prompt.
143
+ * @param {string|null} [doorNote] MODULE 8 — the signer's own words, when a previous run
144
+ * of this spec ended at the review door and they took the RERUN door. Additive and
145
+ * omitted-by-default, so every existing caller renders byte-identically. It is a
146
+ * REQUIREMENT the plan must meet, never a replacement for the goal: the signed goal and
147
+ * the signed close are unchanged, and what the person added is what the machine could
148
+ * not see.
143
149
  */
144
150
  export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxStepRounds: number, failure: string | null, scopes: string[] | undefined, materials?: {
145
151
  balanceUsd?: number | null;
@@ -148,7 +154,7 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
148
154
  }, startingDraft?: object | null, checkFacts?: {
149
155
  seedRed?: string[];
150
156
  priorChecks?: string[];
151
- }): string;
157
+ }, doorNote?: string | null): string;
152
158
  /**
153
159
  * Execute a validateJob-GREEN plan-shape job (goal/verdictType/close).
154
160
  * Called by runJob after the approval gate, validation, and the smoke — this
@@ -167,6 +173,13 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
167
173
  * `false` → the drafter has no tools, so a native session would report NO cost — return a
168
174
  * metered claude-json TEXT provider (`--output-format json`, `parse:'claude-json'`) instead,
169
175
  * so its spend is never invisible. The Loop path (`anthropic-api`) never touches this.
176
+ * @param {any} [opts.judgeProvider] softgreen — the provider a JUDGED close stage runs its
177
+ * locate call through, pinned by the operator to `JUDGE_MODEL` (src/judged.js). Separate
178
+ * from `provider`/`providerFor` on purpose: the judge tier is not a step knob and never
179
+ * agent-selectable, and the worker's binding is not a legal stand-in for it. Absent is not
180
+ * a fall-back — a judged stage with no seam instrument-STOPS as a wiring gap, the same
181
+ * answer a native job with no `nativeProvider` gets. A close with no judged stage never
182
+ * reads it.
170
183
  * @param {(type: string, data?: object) => object} opts.emit spine emitter (the caller's METERED emit)
171
184
  * @param {() => number} opts.remainingUsd the one wallet: what is left of the signed budget right now
172
185
  * @param {() => boolean} [opts.isUnpriced] has any round come back with a null cost? (F6) — the
@@ -306,12 +319,40 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
306
319
  * is, before anything is read at all. After the fix loop opens the ruling is gone, so once the mechanical
307
320
  * stages pass again the run PAUSES for a second review rather than converting the same
308
321
  * sentence forever.
322
+ * @param {{decision: string, text?: string|null, receivedAt?: string|null}|null} [opts.heldRuling]
323
+ * F102 — the decision the CHECKPOINT carries (`readResume`'s `restart.pendingDecision`):
324
+ * an answer a person already gave, on a leg that stopped before a single round was
325
+ * bought for it. It is applied exactly as a fresh one is, through the same seam, so
326
+ * the person is never asked the same question twice; the spine records that it came
327
+ * from a record rather than from a person, and `receivedAt` says when they said it.
328
+ *
329
+ * A leg handed BOTH is refused (`hitl-decision-red`, naming the held decision): two
330
+ * answers to one question is ambiguity, and a merge would pick a winner nobody chose.
331
+ * Absent is every ordinary run and every leg the operator answers directly.
332
+ * @param {number} [opts.priorSpentUsd=0] F103 — money the CHAIN spent before this leg,
333
+ * for the `engagement` record's money half ONLY. It bounds nothing here: the wallet
334
+ * is the ledger's one level up, and `remainingUsd` already nets this fold against the
335
+ * signed budget on every path (the signed budget is the chain's ceiling, and a rerun
336
+ * spends what remains of it — never a refill). Reporting, never governance.
337
+ * @param {boolean|null} [opts.reviewDoor] MODULE 8 — does this run END at the review
338
+ * door (PRD v1.71 §3)? `true`/`false` is the operator's explicit choice in either
339
+ * direction; absent is the CLASS default (`doorOpens`): always for soft-green,
340
+ * whose credit is held until a person accepts, and never for green-class unless
341
+ * asked. It changes what the run RECORDS and never what it returns — the door is
342
+ * a disposition, and the loop's verdict is not the door's to touch.
343
+ * @param {{text: string, fromRunid?: string|null, receivedAt?: string|null}|null} [opts.doorRerun]
344
+ * MODULE 8 — the signer took the RERUN door on a previous run: a FRESH ENGAGEMENT
345
+ * (§3.4) against the same signed spec, carrying their words. Two things follow, and
346
+ * only two: the already-green shortcut is refused (the tree passing the close is
347
+ * precisely the state the person rejected), and the words reach the PLANNER as new
348
+ * authoring. It is never a `humanRuling` — that answers a close's human STAGE, and
349
+ * a green-class job has none. Empty words are refused at the same seam every door is.
309
350
  * @returns {Promise<string>} 'green' | 'already-green' | 'escalated' | 'plan-red' |
310
351
  * 'check-red' | 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' |
311
352
  * 'branch-red' | 'cap-halt' | 'wall-halt' | 'provider-red' | 'interpreter-red' |
312
353
  * 'step-stalled' | 'hitl-pause' | 'hitl-decision-red' | `step-red:<id>`
313
354
  */
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 }: {
355
+ export function runPlan(job: any, { workdir, provider, nativeProvider, providerFor, judgeProvider, emit, remainingUsd, isUnpriced, spendComplete, capRuns, strikeLimit, closeTimeoutMs, maxStepRounds, layerRoot, scoutRounds, bridge, now, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, priorSpentUsd, reviewDoor, doorRerun }: {
315
356
  workdir: string;
316
357
  provider: any;
317
358
  nativeProvider?: ((o: {
@@ -320,6 +361,7 @@ export function runPlan(job: any, { workdir, provider, nativeProvider, providerF
320
361
  maxTurns: number;
321
362
  hasTools: boolean;
322
363
  }) => any) | undefined;
364
+ judgeProvider?: any;
323
365
  emit: (type: string, data?: object) => object;
324
366
  remainingUsd: () => number;
325
367
  isUnpriced?: (() => boolean) | undefined;
@@ -348,6 +390,18 @@ export function runPlan(job: any, { workdir, provider, nativeProvider, providerF
348
390
  decision: string;
349
391
  text?: string;
350
392
  } | null | undefined;
393
+ heldRuling?: {
394
+ decision: string;
395
+ text?: string | null;
396
+ receivedAt?: string | null;
397
+ } | null | undefined;
398
+ priorSpentUsd?: number | undefined;
399
+ reviewDoor?: boolean | null | undefined;
400
+ doorRerun?: {
401
+ text: string;
402
+ fromRunid?: string | null;
403
+ receivedAt?: string | null;
404
+ } | null | undefined;
351
405
  }): Promise<string>;
352
406
  /**
353
407
  * The recorded bound-reason's ceiling in the WORKER-facing note. The reason is a