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.
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)
@@ -397,6 +401,143 @@ export function runReuse(opts: {
397
401
  runJob?: typeof shippedRunJob | undefined;
398
402
  now?: (() => number) | undefined;
399
403
  }): Promise<any>;
404
+ /**
405
+ * WHAT A GREEN WRITES, in ONE place. Lifted verbatim out of `runReuse`'s own
406
+ * closure when the U runner needed it too (2B) — two spellings of "what a green
407
+ * writes" would be the two-transforms class applied to the ledger itself, which is
408
+ * the failure `greenParts` exists one level down to prevent.
409
+ *
410
+ * Two legs, and the split is which caller already holds an entry:
411
+ * - A BRIDGE IN HAND (the reuse leg) APPENDS. No shape check is needed: `runJob`'s
412
+ * load gate already proved at the door, before a token, that this entry is the
413
+ * same kind of recipe as this job (a mismatch returns `recipe-stale` and never
414
+ * gets here).
415
+ * - NO BRIDGE (the cold leg, and every U run) mints — unless a name already holds
416
+ * greens, in which case it APPENDS, because a cold run of a job that already has
417
+ * a bridge is another version of that workflow and overwriting it would destroy
418
+ * the green that minted it.
419
+ *
420
+ * But the name is the JOB's slug, and a job's CLOSE can be re-signed under that same
421
+ * slug. Appending across two close shapes does two wrong things at once: it counts a
422
+ * green another close rendered toward this entry's distinct-patient status (a false
423
+ * `proven`), and it hands the load gate — which matches on exactly these stage names
424
+ * — a plan that satisfied a different verification. So the shape is checked with the
425
+ * gate's own predicate, and a mismatch neither appends NOR discards the green: it
426
+ * forks to a deterministic derived name, with the fork ON the record.
427
+ *
428
+ * THE HOLD IS NOT DECIDED HERE. `quarantined` comes off `greenParts` reading the
429
+ * class the run was signed under (`record.verdictType`, module 6), so a caller that
430
+ * forgets to pass it mints an UNHELD green rather than a held one — which is why
431
+ * both callers pass the spec's own field and never a literal.
432
+ *
433
+ * KNOWN LIMIT, named rather than discovered later: `appendGreen` carries the entry's
434
+ * LABEL (`goal`, `closeStageNames`, `toolsUsed`) forward untouched, so a new version
435
+ * whose plan uses a verb the founding green did not is not reflected in `toolsUsed`.
436
+ * The load gate reads that field, so the gate's verb check goes slightly stale across
437
+ * versions. It is not a safety hole — the gate only ever gets more permissive there,
438
+ * and the TWEAKED plan still passes the full `validatePlan` against the job's signed
439
+ * menu at draft time, which is the check that actually decides. Fixing it properly
440
+ * means `appendGreen` taking a label update, which is module 1's contract and hamr's
441
+ * call, not something to change from the caller.
442
+ *
443
+ * Returns the WRITE RECORD both runners report — never throws, and a refusal is a
444
+ * record with `action: 'none'` and named reds rather than an absence a reader has to
445
+ * branch on.
446
+ * @param {{registryDir: string, bridge?: any|null, meta: any, record: any}} o
447
+ * @returns {{name: string|null, action: string, file: string|null, reds: any[], [k: string]: any}}
448
+ */
449
+ export function writeGreenRow({ registryDir, bridge, meta, record }: {
450
+ registryDir: string;
451
+ bridge?: any | null;
452
+ meta: any;
453
+ record: any;
454
+ }): {
455
+ name: string | null;
456
+ action: string;
457
+ file: string | null;
458
+ reds: any[];
459
+ [k: string]: any;
460
+ };
461
+ /**
462
+ * THE U RUNNER'S TERMINAL WRITE (2B) — does the run that just ended earn a registry
463
+ * row, and if so, write it.
464
+ *
465
+ * `scripts/run-u.mjs` has always kept standalone bridge FILES and never a registry,
466
+ * so its review door could describe a held learning credit it had no row to release:
467
+ * `--decide accept` reached `recordDoor`, found no green row for the runid and died
468
+ * `no-row-for-run`. This is the row, and STORAGE IS ALL IT IS — selection, promotion
469
+ * and reuse execution stay parked (layer-3-reuse). Nothing here reads a row back.
470
+ *
471
+ * FOUR REFUSALS, each one a guard that already existed somewhere and is kept:
472
+ * - NO REGISTRY, no row. The registry path is operator-supplied and never
473
+ * conjured; without one the door says, in words, that nothing was released.
474
+ * - ALREADY-GREEN mints nothing. The close was satisfied before this run did
475
+ * anything, so nothing here earned a version — the reuse-credit leak block, from
476
+ * the writing side rather than the door's.
477
+ * - A GREEN WITH NO PLAN mints nothing. The artifact that inherits is the one that
478
+ * RAN, and there is none (R1).
479
+ * - A CLASS WHOSE CREDIT IS NOT HELD mints nothing HERE. That is `green` today, and
480
+ * it is main's behaviour kept rather than a new rule: this runner has never
481
+ * written a registry row for a green-class run, and widening what enters the
482
+ * registry is a reuse-rung question, not a side effect of fixing a door. The
483
+ * predicate is `quarantinesCredit` — the ONE spelling of "is this class held" —
484
+ * so the row this runner mints and the row a door releases can never disagree
485
+ * about which classes they are talking about.
486
+ *
487
+ * The row is born HELD for exactly the reason any judged green is: `greenParts` reads
488
+ * `verdictType` off the record. Nothing here sets the flag.
489
+ * @param {{registryDir: string|null|undefined, job: any, name: string|null,
490
+ * outcome: string, plan: any, record: any}} o
491
+ * @returns {{minted: boolean, reason: string|null, write: any|null}}
492
+ */
493
+ export function writeRunGreenRow({ registryDir, job, name, outcome, plan, record }: {
494
+ registryDir: string | null | undefined;
495
+ job: any;
496
+ name: string | null;
497
+ outcome: string;
498
+ plan: any;
499
+ record: any;
500
+ }): {
501
+ minted: boolean;
502
+ reason: string | null;
503
+ write: any | null;
504
+ };
505
+ /**
506
+ * Apply a signer's door decision to the workflow a run wrote its row against:
507
+ * record the disposition, and — on `accept` over a HELD judged green — release the
508
+ * learning credit that green has been carrying unspent (PRD v1.71 §3).
509
+ *
510
+ * This is the ONE seam between a person's answer and the box. It is deliberately
511
+ * separate from `runReuse`: the door opens AFTER the run has ended (a green run is
512
+ * over, non-blocking, and the person may answer minutes or days later), so the
513
+ * decision cannot ride the run's own return path. The runner calls this with the
514
+ * runid it printed at the door.
515
+ *
516
+ * Never throws and never conjures: a missing registry, a missing entry, a
517
+ * malformed one and a runid with no green row all come back as named reds. The
518
+ * loop's verdict is untouched in every branch — this writes a disposition, never a
519
+ * result.
520
+ *
521
+ * @param {object} opts
522
+ * @param {string} opts.registryDir the operator-supplied registry directory
523
+ * @param {string} opts.name the workflow the run wrote to
524
+ * @param {string} opts.runid the RUN's id, exactly as the bridge row records it
525
+ * @param {string} opts.decision one of the three doors
526
+ * @param {string|null} [opts.at] when the person answered
527
+ * @returns {{ok: boolean, reds: any[], released: boolean, file: string|null}}
528
+ */
529
+ export function applyDoorDecision({ registryDir, name, runid, decision, at }: {
530
+ registryDir: string;
531
+ name: string;
532
+ runid: string;
533
+ decision: string;
534
+ at?: string | null | undefined;
535
+ }): {
536
+ ok: boolean;
537
+ reds: any[];
538
+ released: boolean;
539
+ file: string | null;
540
+ };
400
541
  /** @typedef {{code: string, path: string, detail?: string}} Red */
401
542
  /**
402
543
  * 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
@@ -102,13 +109,26 @@
102
109
  * RESUMES a paused run and forwarded verbatim to the plan flow. Absent on every
103
110
  * ordinary run; never authored and never defaulted (a defaulted answer to "is this
104
111
  * done?" is the rubber-stamp the class exists to prevent, 2026-08-12 §4).
112
+ * @param {{decision: string, text?: string|null, receivedAt?: string|null}|null} [opts.heldRuling]
113
+ * F102 — the answer the CHECKPOINT holds (`readResume`'s `restart.pendingDecision`):
114
+ * a decision a person already gave on a leg that stopped before a single round was
115
+ * bought for it. Forwarded verbatim; the plan flow applies it exactly as a fresh one
116
+ * and refuses a leg handed both. It is also what decides whether the TIME fold this
117
+ * run declares is the chain's or this engagement's (F103).
118
+ * @param {boolean|null} [opts.reviewDoor] MODULE 8 — does this run END at the review
119
+ * door (PRD v1.71 §3)? Forwarded verbatim to the plan flow, which owns the class
120
+ * default. It changes what the run RECORDS and never what it returns: the door is a
121
+ * disposition and the loop's verdict is not the door's to touch.
122
+ * @param {{text: string, fromRunid?: string|null, receivedAt?: string|null}|null} [opts.doorRerun]
123
+ * MODULE 8 — this leg IS the fresh engagement a signer commissioned at a previous
124
+ * run's review door, carrying their words. Forwarded verbatim.
105
125
  * @returns {Promise<string>} outcome: 'green' | 'already-green' | 'escalated' |
106
126
  * 'unapproved-spec' | 'job-red' | 'smoke-red' | 'plan-red' | 'check-red' |
107
127
  * 'close-red' | 'close-unsupported' | 'recipe-stale' | 'branch-red' | 'pricing-red' | 'provider-red' |
108
128
  * 'interpreter-red' | 'cap-halt' | 'wall-halt' | 'step-stalled' |
109
129
  * 'hitl-pause' | 'hitl-decision-red' | `step-red:<id>`
110
130
  */
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 }: {
131
+ export function runJob(rawSpec: object | string, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider, emit, capRuns, strikeLimit, shellCapUsd, closeTimeoutMs, layerRoot, bridge, priorSpentUsd, priorSpendComplete, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun }: {
112
132
  approvals: unknown;
113
133
  workdir: string;
114
134
  provider: any;
@@ -120,6 +140,7 @@ export function runJob(rawSpec: object | string, { approvals, workdir, provider,
120
140
  }) => any) | undefined;
121
141
  emit: (type: string, data?: object) => object;
122
142
  providerFor?: ((tier: string) => any) | undefined;
143
+ judgeProvider?: any;
123
144
  capRuns?: number | undefined;
124
145
  strikeLimit?: number | undefined;
125
146
  shellCapUsd?: number | undefined;
@@ -143,7 +164,34 @@ export function runJob(rawSpec: object | string, { approvals, workdir, provider,
143
164
  decision: string;
144
165
  text?: string;
145
166
  } | null | undefined;
167
+ heldRuling?: {
168
+ decision: string;
169
+ text?: string | null;
170
+ receivedAt?: string | null;
171
+ } | null | undefined;
172
+ reviewDoor?: boolean | null | undefined;
173
+ doorRerun?: {
174
+ text: string;
175
+ fromRunid?: string | null;
176
+ receivedAt?: string | null;
177
+ } | null | undefined;
146
178
  }): Promise<string>;
179
+ /** @typedef {{code: string, path: string, detail?: string}} Red */
180
+ /**
181
+ * THE ROUND RECORDS THE ONE LEDGER SUMS — as data, because two of them now exist
182
+ * and a hand-spelled condition per record is how a paid call comes to be free.
183
+ *
184
+ * `worker-round` is the attempt's own spend, metered per ROUND rather than per
185
+ * attempt-return (F12: a halted attempt's spend was invisible by 300×).
186
+ * `judge-round` is the CLOSE's, and it is the first spend a close has ever had:
187
+ * the judged floor (softgreen) buys a pinned locate call per artifact, and the
188
+ * budget-identity rule is that a budget funds the attempt PLUS its close. Both
189
+ * carry the honest `costUsd` — a null lands as `unpriced` and halts, never as $0.
190
+ *
191
+ * What is deliberately NOT here: `worker-result` / `worker-plan`, which are
192
+ * attempt TOTALS of the same rounds and would double-count.
193
+ */
194
+ export const ACCOUNTED_ROUND_TYPES: readonly string[];
147
195
  export type Red = {
148
196
  code: string;
149
197
  path: string;