bareloop 0.22.0 → 0.24.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/index.d.ts CHANGED
@@ -15,7 +15,7 @@ export { workBranchName, prepareWorkBranch, WORK_BRANCH_PREFIX, WORK_BRANCH_RE }
15
15
  export { createReadShim, wrapReadTool, readShimArm, readShimStrategy, READ_SHIM_CAP, READ_SHIM_STRATEGY, READ_SHIM_DIFF_STRATEGY, READ_SHIM_ARMS } from "./readshim.js";
16
16
  export { runDeclaredClose, runStage, seedRead, seedAtHead, seedListing, changedSet, makeSeedTrees, LIVE_KINDS, STOP_FAULTS, JUDGED_MARKER, EXIT_GREEN, EXIT_RED, EXIT_STOP, HUMAN_DECISIONS, SEED_EXEMPT_KINDS, JUDGED_FLOOR_KIND, normalizeHumanRuling, resolveHumanRuling, JUDGE_ATTEMPTS, MAX_JUDGED_PATHS, REVIEW_DOOR, DOOR_OPEN_OUTCOMES, REVIEW_DOOR_CLASSES, doorOpens, mechanicalStages } from "./kinds.js";
17
17
  export { answerReviewDoor, doorRecordOf, doorAgeGate } from "./reviewdoor.js";
18
- export { JUDGE_MODEL, JUDGE_MAX_TOKENS, JUDGE_RULES, JUDGE_RULE_IDS, LOCATE_AXES, LOCATE_LABEL, validateCard, validateFacts, locatePrompt, runLocate, decide, defaultJudgeLoop, CALIBRATION_SIZE, CASE_VERDICTS, validateCalibrationSet, validateJudgedArtifacts, expectedOf } from "./judged.js";
18
+ export { JUDGE_MODEL, resolveJudge, JUDGE_MAX_TOKENS, JUDGE_RULES, JUDGE_RULE_IDS, LOCATE_AXES, LOCATE_LABEL, validateCard, validateFacts, locatePrompt, runLocate, decide, defaultJudgeLoop, CALIBRATION_SIZE, CASE_VERDICTS, validateCalibrationSet, validateJudgedArtifacts, expectedOf } from "./judged.js";
19
19
  export { proposeJudgedArtifacts, signJudgedArtifacts, foldJudgedArtifacts, proposalSchema, proposalTool, cardCasesPrompt, PROPOSAL_TOOL_NAME, PROPOSAL_LABEL, MAX_PROPOSAL_RETRIES, COMPILE_SYSTEM } from "./cardauthor.js";
20
20
  export { runCalibration, compareExpectation, factsResist, artifactHash, INJECTION_LOCATE_BATTERY, INJECTION_CARD, CALIBRATION_LABEL, CASUALTY_AXES } from "./calibrate.js";
21
21
  export { validateDeclaration, normalizeDeclaration, KIND_CATALOGUE, CATALOGUE_KINDS, CATALOGUE_LIVE_KINDS, LOCKED_KINDS, TYPES_GENRE, TYPES_GENRE_TEMPLATE, GENRE_LANGUAGES, genreEnv, genreOwnedEnvNames, genreInstruments, DIRECTIONS, BASELINES, MAX_STAGES, VERDICT_CLASSES, LOCKED_CLASSES, LIVE_CLASSES, CLASS_BATTERIES, classGuards, classMenu, closeCeiling, NEVER_OFFERED_KINDS } from "./authoring.js";
package/types/job.d.ts CHANGED
@@ -207,6 +207,31 @@ export const CADENCE_UNITS: readonly string[];
207
207
  * subscription — its costUsd axis is NOTIONAL (API-equivalent value, not
208
208
  * billed), so it is a DISTINCT condition: rows never pool with anthropic-api
209
209
  * rows on the cost axis, and budgets sized for one do not transfer. */
210
+ /** PRD item 28 (2026-09-06/09-09): `openai-api` — an OpenAI-shaped provider,
211
+ * constructed via `src/providers.js`'s factory. hamr's ruling (PRD 30.7):
212
+ * `deepseek-chat` is THE secondary provider (one, not a menu of half-tested
213
+ * models); the factory's tier table is where that lives, not this menu.
214
+ *
215
+ * PRD item 31.3 (hamr, 2026-09-09: *"anthropic, openai, gemini drop ollama for
216
+ * now"*): `gemini-api` joins. That is bareloop matching bare-agent's own
217
+ * adapter surface rather than curating a shorter list — bare-agent exports four
218
+ * real HTTP backends (Anthropic, OpenAI, Gemini, Ollama) plus CLIPipe and the
219
+ * FallbackProvider composite; `provider-http.js`, `provider-stop-reason.js`,
220
+ * `provider-temperature.js` and `provider-usage.js` are shared HELPERS, not
221
+ * backends, and were miscounted as providers once — the export list, never the
222
+ * filenames, is the source of truth.
223
+ *
224
+ * Ollama is deliberately NOT here. Beyond hamr's word, it is the one entry
225
+ * whose economics differ in kind: it takes no API key and bills nothing, so
226
+ * every round would price at $0 through machinery that treats $0 as a real
227
+ * price. An unpriced round reported as free is the exact honesty violation the
228
+ * `?? 0` rule exists to stop, and admitting it needs that question answered
229
+ * first, not a menu entry.
230
+ *
231
+ * `gemini-api` is ADMITTED-PENDING-PROBE: the probe rule stands (item 28
232
+ * ruling (d)) and gemini has zero runs. See `PROBE_STATUS` in
233
+ * `src/providers.js` — a spec may name it, and the runner says out loud that
234
+ * nothing has proven it. */
210
235
  export const PROVIDERS: readonly string[];
211
236
  /** V3 environment label: declared keys only — every field is a lineage-key
212
237
  * candidate at N3. `provider` is part of the key by definition (top-level,
package/types/judged.d.ts CHANGED
@@ -1,3 +1,102 @@
1
+ /**
2
+ * RESOLVE WHICH MODEL GRADES THIS JOB (PRD item 32.1).
3
+ *
4
+ * hamr, 2026-09-10: *"what i care about is that judge becomes llm agnostic and
5
+ * not set to one model or provider"* — and the shape he named: *"defaults to
6
+ * whatever llm is used and can be overriden"*.
7
+ *
8
+ * So, in precedence order:
9
+ * 1. `spec.judge` — an explicit, SIGNED `{provider, model}`. This is how a run
10
+ * pins a judge deliberately different from its worker (an independent
11
+ * reader, or a judge whose calibration a job wants to keep while the worker
12
+ * moves). It is a spec field the HUMAN signs, part of `jobSpecHash` like
13
+ * every other; the drafting agent cannot express it, because naming your
14
+ * own examiner is arbiter territory.
15
+ * 2. otherwise the job's OWN worker provider and model. A DeepSeek job judges
16
+ * on DeepSeek and needs no second account — which is the whole point: after
17
+ * item 31.4 a green job runs on its own key alone, and this is what lets a
18
+ * JUDGED one do the same.
19
+ *
20
+ * There is deliberately NO third fallback to `JUDGE_MODEL`. A caller that cannot
21
+ * say which model will grade has not wired a judge, and the grading path stops
22
+ * as a wiring gap rather than grading under an identity nobody chose — the same
23
+ * answer an absent `judgeLoop` already gets. A silent default here would be the
24
+ * exact silent-ignore class that `endpointKey` and F149 exist to prevent, except
25
+ * pointed at the arbiter.
26
+ *
27
+ * SELF-GRADING is licensed by measurement, not by this function: the judge
28
+ * renders no verdict (it locates facts; `decide()` is deterministic), and the
29
+ * calibration gate — 10 cases, a 10-of-10 floor, no partial credit, cases that
30
+ * MUST fail — is exactly the instrument that catches a judge which cannot see
31
+ * failures. If a worker-family judge cannot pass its own calibration, that is
32
+ * the finding, and the gate refuses to sign.
33
+ *
34
+ * @param {{specJudge?: any, workerProvider?: string, workerModel?: string}} o
35
+ * @returns {{provider: string, model: string}}
36
+ */
37
+ export function resolveJudge({ specJudge, workerProvider, workerModel }?: {
38
+ specJudge?: any;
39
+ workerProvider?: string;
40
+ workerModel?: string;
41
+ }): {
42
+ provider: string;
43
+ model: string;
44
+ };
45
+ /**
46
+ * `resolveJudge` needs a worker MODEL already resolved, and an authoring
47
+ * pipeline has to compute one identity at TWO different moments — once at
48
+ * compose time, to STAMP `closeDecl.calibration.judgeModel` into the spec it
49
+ * is about to sign, and again at the calibration gate that grades against
50
+ * that stamp — from a value that is not yet a validated spec (the operator's
51
+ * draft) the first time and is one (the assembled spec) the second. Two
52
+ * hand-spelled compositions of `resolveWorkerModel` + `resolveJudge` is
53
+ * exactly the kind of pair that drifts (PRD F45's "a rule with no detector is
54
+ * prose" applies to a rule spelled TWICE just as much as to one spelled
55
+ * nowhere) — this is the one spelling both calls make, and it mirrors the
56
+ * SAME composition `scripts/run-u.mjs` applies at launch time: the named
57
+ * provider's own `tiers.sonnet` defaults an absent model, and a signed
58
+ * `judge` override wins over the worker default.
59
+ *
60
+ * `source` is read for `provider`/`model`/`judge` only, so a draft and the
61
+ * spec `assembleSpec` folds it into resolve identically — `assembleSpec`
62
+ * spreads the draft into the spec untouched on those three fields — and any
63
+ * draft that goes on to validate and sign therefore stamps and grades under
64
+ * the exact same identity `scripts/run-u.mjs` will resolve at run time.
65
+ *
66
+ * `source.provider` absent, or unresolvable, or `source.judge` malformed all
67
+ * THROW here exactly as `resolveProvider`/`resolveJudge` already do on their
68
+ * own — a caller composing this from a pre-validation draft is expected to
69
+ * catch that and use a placeholder, because a draft that cannot resolve here
70
+ * is a draft `validateJob` reds a few lines later regardless, before anything
71
+ * gets signed under the placeholder.
72
+ *
73
+ * `resolveWorkerModelFn` is taken as a PARAMETER rather than imported — this
74
+ * module cannot import `./job.js` directly: `job.js` imports
75
+ * `./declaredclose.js`, which imports `resolveJudge` from THIS module, and a
76
+ * `judged.js` → `job.js` edge would close that into a cycle. `resolveProvider`
77
+ * has no such conflict (`./providers.js` imports nothing local) and is
78
+ * imported directly.
79
+ *
80
+ * @param {{provider?: string, model?: string, judge?: any}} source draft or spec
81
+ * @param {string} fallbackProviderName used only when `source.provider` is absent
82
+ * @param {(o: {specModel?: string, flagModel?: string, defaultModel: string}) => {model: string, source: string}} resolveWorkerModelFn `resolveWorkerModel` from `./job.js`
83
+ * @returns {{provider: string, model: string}}
84
+ */
85
+ export function resolveJobJudge(source: {
86
+ provider?: string;
87
+ model?: string;
88
+ judge?: any;
89
+ }, fallbackProviderName: string, resolveWorkerModelFn: (o: {
90
+ specModel?: string;
91
+ flagModel?: string;
92
+ defaultModel: string;
93
+ }) => {
94
+ model: string;
95
+ source: string;
96
+ }): {
97
+ provider: string;
98
+ model: string;
99
+ };
1
100
  /**
2
101
  * The rubric card: Q6's answer compiled into enumerated items, signed by the
3
102
  * person and enumerated in the spec hash. `rule` selects from `JUDGE_RULES`;
@@ -130,12 +229,13 @@ export function locatePrompt(card: any): string;
130
229
  * On every route out, red or clean, `onCost` fires exactly once with the honest
131
230
  * read: a paid call that leaves no meter record is F12 wearing a judge's coat.
132
231
  * @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
232
+ * maxTokens?: number, attempt?: number, onCost?: (c: {costUsd: number|null, unpricedRounds: number}) => void,
233
+ * callBounds?: {timeoutMs?: number, deadlineMs?: number}}} o
134
234
  * @returns {Promise<{ok: boolean, facts: any|null, red: {axis: string, detail: string}|null,
135
235
  * costUsd: number|null, unpricedRounds: number, truncated: boolean, parseError: boolean,
136
236
  * raw: ReturnType<typeof scrubRaw>}>}
137
237
  */
138
- export function runLocate({ artifactText, card, loopFactory, maxTokens, attempt, onCost }: {
238
+ export function runLocate({ artifactText, card, loopFactory, maxTokens, attempt, onCost, callBounds, }: {
139
239
  artifactText: string;
140
240
  card: any;
141
241
  loopFactory: (o: {
@@ -148,6 +248,10 @@ export function runLocate({ artifactText, card, loopFactory, maxTokens, attempt,
148
248
  costUsd: number | null;
149
249
  unpricedRounds: number;
150
250
  }) => void;
251
+ callBounds?: {
252
+ timeoutMs?: number;
253
+ deadlineMs?: number;
254
+ };
151
255
  }): Promise<{
152
256
  ok: boolean;
153
257
  facts: any | null;
@@ -196,25 +300,35 @@ export function decide(facts: any, card: any, { artifactText }?: {
196
300
  reason: string | null;
197
301
  };
198
302
  /**
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.
303
+ * THE JUDGE'S HISTORICAL PIN. Until PRD item 32 (2026-09-10) this constant WAS
304
+ * the judge: every grading path imported it, so bareloop could run a worker on
305
+ * any provider and still judge only on Claude.
306
+ *
307
+ * It is no longer read by any grading path. The judge identity is now RESOLVED
308
+ * per job (`resolveJudge` below) and travels as data — `ctx.judgeModel` into
309
+ * `runJudgedFloor`, an explicit argument into the calibration gate, and
310
+ * `closeDecl.calibration.judgeModel` into the signed spec, exactly as before.
311
+ * Keeping the export is deliberate: it is part of the public API
312
+ * (`src/index.js`) and an adopter importing it should not break on a minor.
313
+ * What it means now is "the judge bareloop pinned before item 32", which is
314
+ * still the right DEFAULT for an anthropic-api job and still the only tier with
315
+ * established injection resistance upstream (BA-20's `INJECTION_BATTERY` is
316
+ * haiku-4.5 only).
204
317
  *
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`
318
+ * THE ENFORCEMENT IS UNCHANGED, and still lives in three wired places because a
319
+ * rule with no detector is prose (F45) — only the value being compared moved
320
+ * from this constant to the resolved identity:
321
+ * 1. the identity is STORED with the set it graded — `foldJudgedArtifacts`
208
322
  * writes it as `closeDecl.calibration.judgeModel`, inside the spec, so
209
- * `jobSpecHash` covers it by construction. Bump this constant and every
323
+ * `jobSpecHash` covers it by construction. Change the judge and every
210
324
  * signed judged spec hashes differently: the signature dies, and re-signing
211
325
  * is what re-runs the calibration gate.
212
326
  * 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.
327
+ * attribute to a judge is a floor nobody can tell apart from a moved one.
328
+ * 3. `runJudgedFloor` refuses to grade when the stored identity is not the one
329
+ * about to grade, naming both. It does not degrade in either direction:
330
+ * grading on the new judge judges against a floor nobody calibrated, and
331
+ * the old judge is not on offer.
218
332
  */
219
333
  export const JUDGE_MODEL: "claude-haiku-4-5";
220
334
  /**
package/types/kinds.d.ts CHANGED
@@ -1,3 +1,35 @@
1
+ /**
2
+ * The judged stages of a close declaration, and whether it has any — the ONE
3
+ * spelling of "does this close ask a model to render anything?".
4
+ *
5
+ * The comment above `JUDGED_FLOOR_KIND` says five literals is how the readers
6
+ * come to disagree, and by PRD item 31.4 there were SIX open-coded
7
+ * `stages.filter(s => s.kind === JUDGED_FLOOR_KIND)` across the tree
8
+ * (`src/authorjob.js` twice, `src/cardauthor.js`, `src/declaredclose.js`,
9
+ * `scripts/run-author.mjs`, `scripts/author-readout.mjs`). 31.4 needed a
10
+ * seventh, in the runner, to decide whether a judge KEY is demanded — and a
11
+ * seventh spelling deciding a REFUSAL is the drift `scripts/run-u.mjs`'s own
12
+ * judge-provider comment warns about ("a second reading of the declaration
13
+ * that can drift from the one the runner actually executes"). So there is one
14
+ * reading now, and every caller shares it.
15
+ *
16
+ * Defensive on shape ON PURPOSE: callers pass raw specs read off disk and
17
+ * half-built drafts, not validated declarations. An absent/!object `closeDecl`
18
+ * or `stages` is "no judged stages", never a throw — the close VALIDATOR is
19
+ * what rejects a malformed declaration, and this predicate answering a
20
+ * question about one is not the place to re-litigate it.
21
+ * @param {any} closeDecl a close declaration (or anything, including undefined)
22
+ * @returns {any[]} the judged-floor stages, in declaration order
23
+ */
24
+ export function judgedStages(closeDecl: any): any[];
25
+ /**
26
+ * Does this close declaration judge — i.e. will a judge model be CALLED?
27
+ * The question `scripts/run-u.mjs` asks before demanding a judge key (PRD item
28
+ * 31.4): a job that never judges must run on its own provider's key alone.
29
+ * @param {any} closeDecl
30
+ * @returns {boolean}
31
+ */
32
+ export function closeJudges(closeDecl: any): boolean;
1
33
  /**
2
34
  * Does this run end at a review door? The operator's flag WINS in both
3
35
  * directions and is never inferred: `true` opens it for any class, `false` shuts
@@ -531,14 +563,25 @@ export type Red = {
531
563
  * runner — never authored, never defaulted, and absent on every ordinary run.
532
564
  * `judgeLoop` is the PAID seam the judged stage runs its locate call through, and
533
565
  * 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
566
+ * module owns no provider and never picks one. Absent is not a fall-back to some
567
+ * other model — the stage stops as a wiring gap, the same answer `runPlan` gives a
537
568
  * native job with no native factory.
569
+ * `judgeModel` is the seam's other half (PRD item 32.1): WHICH model that loop
570
+ * drives, resolved by the runner (`resolveJudge`, src/judged.js — the spec's signed
571
+ * `judge` override, else the job's own worker model) rather than pinned in this
572
+ * library. It is required whenever `judgeLoop` is, and for the same reason: a
573
+ * verdict whose grader cannot be named is a floor nobody can attribute, and the
574
+ * signed calibration record has nothing to compare against. The two travel
575
+ * together or the stage stops.
538
576
  * `onJudgeCost` is the METER, and it is the only reason a judged stage can be
539
577
  * funded honestly: this executor cannot reach the run's ledger, so every locate
540
578
  * call's cost is reported OUT through it, once per call, on every route including
541
579
  * the red ones — a paid call that leaves no meter record is F12 in a judge's coat.
580
+ * `callBounds` is F152/PRD 30.4's per-call TIME bound, read fresh before every
581
+ * locate attempt — the same derivation the run's own worker rounds carry
582
+ * (`callBounds()`, src/planrun.js). Absent is the honest unbounded default:
583
+ * an adopter or test that wires no clock through behaves exactly as before
584
+ * this field existed.
542
585
  * `onStage` is a REPORTING seam and nothing else: the seed read spawns a real
543
586
  * toolchain per stage (a suite is the slow one) and a whole read is one opaque
544
587
  * await, so the shell gets told as each stage lands. It decides nothing, it is
@@ -561,6 +604,7 @@ export type Ctx = {
561
604
  judgeLoop?: (o: {
562
605
  system: string;
563
606
  }) => any;
607
+ judgeModel?: string;
564
608
  onJudgeCost?: (c: {
565
609
  stage: string | null;
566
610
  kind: string | null;
@@ -571,6 +615,10 @@ export type Ctx = {
571
615
  costUsd: number | null;
572
616
  unpricedRounds: number;
573
617
  }) => void;
618
+ callBounds?: () => {
619
+ timeoutMs?: number;
620
+ deadlineMs?: number;
621
+ };
574
622
  onStage?: (row: {
575
623
  stage: string | null;
576
624
  kind: string | null;
@@ -196,8 +196,15 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
196
196
  * `false` → the drafter has no tools, so a native session would report NO cost — return a
197
197
  * metered claude-json TEXT provider (`--output-format json`, `parse:'claude-json'`) instead,
198
198
  * so its spend is never invisible. The Loop path (`anthropic-api`) never touches this.
199
+ * @param {string|null} [opts.judgeModel] softgreen — WHICH model that judge provider drives
200
+ * (PRD item 32.1). Resolved by the CALLER (`resolveJudge`, src/judged.js: the spec's signed
201
+ * `judge` override, else the job's own worker model), never pinned in this library. It
202
+ * travels with `judgeProvider` or the judged stage stops as a wiring gap: a verdict whose
203
+ * grader cannot be named is a floor nobody can attribute.
199
204
  * @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
205
+ * locate call through, built by the CALLER from the RESOLVED judge identity (PRD item
206
+ * 32.1: `resolveJudge`, src/judged.js — the spec's signed `judge` override, else the job's
207
+ * own worker provider and model), never pinned in this library. Separate
201
208
  * from `provider`/`providerFor` on purpose: the judge tier is not a step knob and never
202
209
  * agent-selectable, and the worker's binding is not a legal stand-in for it. Absent is not
203
210
  * a fall-back — a judged stage with no seam instrument-STOPS as a wiring gap, the same
@@ -410,7 +417,7 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
410
417
  * 'branch-red' | 'cap-halt' | 'wall-halt' | 'provider-red' | 'interpreter-red' |
411
418
  * 'step-stalled' | 'hitl-pause' | 'hitl-decision-red' | `step-red:<id>`
412
419
  */
413
- export function runPlan(job: any, { workdir, provider, nativeProvider, providerFor, judgeProvider, emit, remainingUsd, isUnpriced, spendComplete, capRuns, strikeLimit, closeTimeoutMs, closeDir, maxStepRounds, layerRoot, readShim, scout, scoutRounds, bridge, now, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, priorSpentUsd, reviewDoor, doorRerun, resumable }: {
420
+ export function runPlan(job: any, { workdir, provider, nativeProvider, providerFor, judgeProvider, judgeModel, emit, remainingUsd, isUnpriced, spendComplete, capRuns, strikeLimit, closeTimeoutMs, closeDir, maxStepRounds, layerRoot, readShim, scout, scoutRounds, bridge, now, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, priorSpentUsd, reviewDoor, doorRerun, resumable }: {
414
421
  workdir: string;
415
422
  provider: any;
416
423
  nativeProvider?: ((o: {
@@ -419,6 +426,7 @@ export function runPlan(job: any, { workdir, provider, nativeProvider, providerF
419
426
  maxTurns: number;
420
427
  hasTools: boolean;
421
428
  }) => any) | undefined;
429
+ judgeModel?: string | null | undefined;
422
430
  judgeProvider?: any;
423
431
  emit: (type: string, data?: object) => object;
424
432
  remainingUsd: () => number;
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Look up a provider's table entry. Throws a NAMED error on an unrecognized
3
+ * provider — never a silent default (the `resolveRates`-style-substring-
4
+ * match lesson: a forward hazard even at zero observed rounds, MEMORY.md).
5
+ * `src/job.js`'s `PROVIDERS` menu is the one place a spec's `provider`
6
+ * field is validated; this is the runtime construction seam it feeds.
7
+ * @param {string} providerName
8
+ * @returns {ProviderTableEntry}
9
+ */
10
+ export function resolveProvider(providerName: string): ProviderTableEntry;
11
+ /**
12
+ * Construct ONE provider instance for `providerName`/`model`, applying
13
+ * `exposeErrorBody: true` (F153 — every constructed provider, unconditionally)
14
+ * and the model's own request-key gating (`paramsFor`). `baseUrl` (PRD item
15
+ * 28, ruling (d), 2026-09-09: admitted in v1) is forwarded only when given —
16
+ * every provider constructor here defaults it on its own when absent.
17
+ * @param {string} providerName
18
+ * @param {{apiKey?: string, model?: string, baseUrl?: string}} [options]
19
+ */
20
+ export function makeProvider(providerName: string, { apiKey, model, baseUrl }?: {
21
+ apiKey?: string;
22
+ model?: string;
23
+ baseUrl?: string;
24
+ }): any;
25
+ /**
26
+ * Build the `{provider, providerFor, judgeProvider}` triple both runners
27
+ * need (`src/cli.js`'s `buildProviders`, `scripts/run-u.mjs`'s top-level
28
+ * wiring) — ONE seam instead of two copies of the same construction and
29
+ * tier-memoization logic. `model`/`tierModels` are already-RESOLVED inputs
30
+ * (the caller runs `resolveWorkerModel` itself, exactly as both runners did
31
+ * before this factory existed — that decision stays where each runner's own
32
+ * `--model` flag/spec precedence lives, not duplicated in here).
33
+ *
34
+ * THE JUDGE IS NO LONGER PINNED TO ANTHROPIC (PRD item 32.2, hamr 2026-09-10:
35
+ * *"what i care about is that judge becomes llm agnostic and not set to one
36
+ * model or provider"*). It is constructed through this same factory, from a
37
+ * `judgeProviderName`/`judgeModel` pair the CALLER resolved (`resolveJudge`,
38
+ * src/judged.js — the spec's signed `judge` override, else the job's own worker
39
+ * provider and model). Item 28's pin, and item 28 part (2)'s deferral of a
40
+ * signed `judge:{provider,model}` field, both close here.
41
+ *
42
+ * `judgeApiKey` stays a SEPARATE argument rather than being derived: the judge
43
+ * may legitimately be a different provider from the worker, and reading its key
44
+ * for it would put a second env-var reader in a module whose whole job is to
45
+ * have exactly one. The caller reads the resolved judge provider's own `envKey`
46
+ * and hands the key in — the same contract the worker key already has here.
47
+ * @param {object} args
48
+ * @param {string} args.providerName spec.provider (or the anthropic-api default)
49
+ * @param {string|undefined} args.apiKey the WORKER's key, read from the provider's own `envKey`
50
+ * @param {string} args.model the already-resolved worker model id
51
+ * @param {Readonly<Record<string, string>>} args.tierModels the already-resolved tier -> model map (spec override folded in by the caller)
52
+ * @param {string|undefined} [args.baseUrl] spec.baseUrl, if any
53
+ * @param {string|undefined} args.judgeApiKey the judge's own key, read by the caller from the RESOLVED judge provider's `envKey`
54
+ * @param {string} args.judgeModel the resolved judge model id
55
+ * @param {string} [args.judgeProviderName] the resolved judge provider name; defaults to `providerName` (the job's own worker provider), which is `resolveJudge`'s own default
56
+ * @param {string|undefined} [args.judgeBaseUrl] the judge provider's endpoint, when it takes one
57
+ */
58
+ export function buildRunnerProviders({ providerName, apiKey, model, tierModels, baseUrl, judgeApiKey, judgeModel, judgeProviderName, judgeBaseUrl, }: {
59
+ providerName: string;
60
+ apiKey: string | undefined;
61
+ model: string;
62
+ tierModels: Readonly<Record<string, string>>;
63
+ baseUrl?: string | undefined;
64
+ judgeApiKey: string | undefined;
65
+ judgeModel: string;
66
+ judgeProviderName?: string | undefined;
67
+ judgeBaseUrl?: string | undefined;
68
+ }): {
69
+ provider: any;
70
+ providerFor: (tier: string) => any;
71
+ judgeProvider: any;
72
+ };
73
+ /**
74
+ * The launch-time warning lines for an ADMITTED-PENDING-PROBE provider, or
75
+ * `null` when the provider has paid for its own end-to-end probe (and for an
76
+ * off-table name — an unknown provider is `resolveProvider`'s named throw to
77
+ * report, never this function's warning to soften).
78
+ *
79
+ * This lives here, next to `PROBE_STATUS`, rather than inline in the runner,
80
+ * for one reason: inline it was UNTESTABLE. `scripts/run-u.mjs` reaches this
81
+ * point only with a real job row (a machine-local patient path) and a real
82
+ * signed spec, so nothing in the suite could reach the branch, and a warning
83
+ * nothing executes drifts exactly the way the green-only prose did (item 31.2).
84
+ *
85
+ * It never REFUSES, and it never returns a shape a caller can mistake for a
86
+ * refusal: hamr admitted `gemini-api` to the menu, and a probe table that
87
+ * quietly withheld it would be a second ruling nobody made.
88
+ * @param {string} providerName
89
+ * @returns {string[]|null} lines to print, in order, or null when there is nothing to say
90
+ */
91
+ export function probeWarningLines(providerName: string): string[] | null;
92
+ /**
93
+ * Today's anthropic-api tier table — EXACTLY the `DEFAULT_TIER_MODELS` both
94
+ * runners hardcoded before this factory existed. Any change to these two
95
+ * values is a change to shipped anthropic-api behaviour, not a refactor.
96
+ */
97
+ export const ANTHROPIC_TIER_MODELS: Readonly<{
98
+ sonnet: "claude-sonnet-5";
99
+ haiku: "claude-haiku-4-5-20251001";
100
+ }>;
101
+ /**
102
+ * DeepSeek (F149/F150) is a SINGLE model, not a tiered family — hamr's
103
+ * ruling above: one secondary provider, not a menu. Both tiers map to the
104
+ * same model id ON PURPOSE: `resolveWorkerModel`/a runner's `providerFor
105
+ * (tier)` only ever LOOK UP a tier's model id (src/job.js:818-827,
106
+ * scripts/run-u.mjs's `providerFor`) — neither assumes the tiers name
107
+ * different models — so repeating the one id is the honest representation
108
+ * of a single-model provider, not a second model masquerading as `haiku`.
109
+ * If DeepSeek (or a future openai-api entrant) ever ships a genuinely
110
+ * cheaper tier model, this is where it would earn its own `haiku` id.
111
+ */
112
+ export const OPENAI_TIER_MODELS: Readonly<{
113
+ sonnet: "deepseek-chat";
114
+ haiku: "deepseek-chat";
115
+ }>;
116
+ /**
117
+ * Gemini (PRD item 31.3, hamr 2026-09-09: "anthropic, openai, gemini drop
118
+ * ollama for now"). Two real tiers, unlike DeepSeek's single model: `flash` is
119
+ * the cheap tier by design, so `haiku` maps to it honestly rather than
120
+ * repeating one id.
121
+ *
122
+ * ADMITTED-PENDING-PROBE. The probe rule stands (PRD item 28 ruling (d),
123
+ * 2026-09-09): no endpoint is in the menu without its own clean paid probe,
124
+ * and `gemini-api` has ZERO runs. `anthropic-api` is the original (~167
125
+ * archived runs) and `openai-api` was probed green through the shipped runner
126
+ * (run mtu12vks, $1.64/$4, F157); gemini owes that fire and has not paid it.
127
+ * `PROBE_STATUS` below is the machine-readable form of that debt — nothing in
128
+ * here silently pretends the provider is proven.
129
+ */
130
+ export const GEMINI_TIER_MODELS: Readonly<{
131
+ sonnet: "gemini-2.5-pro";
132
+ haiku: "gemini-2.5-flash";
133
+ }>;
134
+ /**
135
+ * Which admitted providers have paid for their own end-to-end probe, and which
136
+ * have not. Read by the runner to print a loud marker; never read to REFUSE —
137
+ * hamr admitted gemini to the menu, and a table that quietly withheld it would
138
+ * be a second, invisible ruling. An unprobed provider runs, and says so.
139
+ * @type {Readonly<Record<string, {probed: boolean, evidence: string}>>}
140
+ */
141
+ export const PROBE_STATUS: Readonly<Record<string, {
142
+ probed: boolean;
143
+ evidence: string;
144
+ }>>;
145
+ export type ProviderTableEntry = {
146
+ /**
147
+ * the bare-agent provider class
148
+ */
149
+ ctor: new (options: any) => any;
150
+ /**
151
+ * the environment variable this provider reads its key from
152
+ */
153
+ envKey: string;
154
+ /**
155
+ * tier name -> model id
156
+ */
157
+ tiers: Readonly<Record<string, string>>;
158
+ /**
159
+ * the CONSTRUCTOR OPTION NAME this provider reads its endpoint from — NOT assumed to be `baseUrl` (see below)
160
+ */
161
+ endpointKey: string;
162
+ /**
163
+ * per-model request-key/param gating, applied on TOP of {exposeErrorBody:true, apiKey, model, <endpointKey>?}
164
+ */
165
+ paramsFor: (model: string | undefined) => Record<string, any>;
166
+ };
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Is `err` an HTTP 429 (rate-limited) response? True ONLY for an explicit
3
+ * status of 429 on the error itself. Nothing else qualifies — not message
4
+ * text (a message merely containing "429" is not this), not `retryable`,
5
+ * not any other status. Symmetric with `isTransportFailure`'s own status
6
+ * check (src/transport.js), just reading the opposite branch of it.
7
+ * @param {any} err
8
+ * @returns {boolean}
9
+ */
10
+ export function isRateLimited(err: any): boolean;
11
+ /**
12
+ * The vendor's RAW stated wait, in milliseconds, parsed straight from the
13
+ * error message — no default, no margin, no cap. `null` when nothing in the
14
+ * message matches a known shape. Exported separately from `rateLimitWaitMs`
15
+ * so a caller that wants to report the vendor's own number on the spine
16
+ * (`statedMs`) never has to re-derive it from the margin-adjusted figure.
17
+ *
18
+ * bare-agent does not expose the vendor's `retry-after` /
19
+ * `x-ratelimit-reset-*` response headers on the thrown error (BA-26) — the
20
+ * delay exists only in the vendor's English message text, so this parses
21
+ * that text. Verified real shape (OpenAI, run `8ev2sdkn`, 2026-09-08):
22
+ * "Please try again in 11.996s." This also supports "try again in 1.5
23
+ * seconds", "try again in 20ms", "retry after 30s", and "retry after 30
24
+ * seconds" — case-insensitive.
25
+ * @param {any} err
26
+ * @returns {number|null}
27
+ */
28
+ export function statedWaitMs(err: any): number | null;
29
+ /**
30
+ * How long to wait before retrying a 429, in milliseconds — or `null`
31
+ * meaning "do not retry at all" (either the parsed wait exceeds
32
+ * `RATE_LIMIT_MAX_WAIT_MS`, in which case waiting that long is the
33
+ * operator's decision, not ours).
34
+ *
35
+ * A parsed value gets `PARSED_DELAY_MARGIN_MS` added (the vendor's stated
36
+ * number is the earliest moment its window resets, not a safe one to fire
37
+ * on), capped so the margin itself can never push a parsed value over
38
+ * `RATE_LIMIT_MAX_WAIT_MS`. When nothing parses, `RATE_LIMIT_DEFAULT_WAIT_MS`
39
+ * is returned as-is, with no margin added (there is nothing to add margin
40
+ * to — it is already a conservative guess, not an earliest-moment reading).
41
+ * @param {any} err
42
+ * @returns {number|null}
43
+ */
44
+ export function rateLimitWaitMs(err: any): number | null;
45
+ /** The fixed retry budget: ONE extra attempt after a 429. Never read from a
46
+ * job spec or CLI flag — raising it is arbiter territory, mirroring
47
+ * transport.js's TRANSPORT_RETRIES exactly (tighten-only doctrine). */
48
+ export const RATE_LIMIT_RETRIES: 1;
49
+ /** bare-agent's `Retry.call` (if ever used) counts ATTEMPTS, not retries;
50
+ * this module's own hand-rolled ladder (src/planrun.js) does the same for
51
+ * symmetry with TRANSPORT_MAX_ATTEMPTS. */
52
+ export const RATE_LIMIT_MAX_ATTEMPTS: number;
53
+ /** The hard cap on how long we will ever wait for a vendor-stated 429 delay.
54
+ * A vendor asking for longer than this is not honoured — we do not retry at
55
+ * all in that case. Waiting that long is the operator's decision, not ours
56
+ * (arbiter territory: nothing here escalates the wait past what hamr set). */
57
+ export const RATE_LIMIT_MAX_WAIT_MS: 60000;
58
+ /** Used only when the vendor's message carries no parseable delay at all. */
59
+ export const RATE_LIMIT_DEFAULT_WAIT_MS: 5000;
package/types/run.d.ts CHANGED
@@ -34,9 +34,14 @@
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 {string|null} [opts.judgeModel] SOFTGREEN — WHICH model the judge provider drives
38
+ * (PRD item 32.1), resolved by the CALLER (`resolveJudge`, src/judged.js) and passed
39
+ * through untouched. Travels with `judgeProvider`; absent, a judged stage stops as a
40
+ * wiring gap rather than grading under an identity nobody named.
37
41
  * @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
42
+ * locate call through, built by the CALLER from the RESOLVED judge identity (PRD item
43
+ * 32.1: `resolveJudge`/`resolveJobJudge`, src/judged.js — the spec's signed `judge`
44
+ * override, else the job's own worker provider and model), never pinned. Forwarded to the plan
40
45
  * flow. It is its own seam and not `provider`/`providerFor`, because the judge tier is
41
46
  * never a step knob and never agent-selectable; absent, a judged stage instrument-stops as
42
47
  * a wiring gap rather than grading on whatever binding happened to be at hand. Its spend
@@ -167,8 +172,16 @@
167
172
  * 'close-red' | 'close-unsupported' | 'recipe-stale' | 'branch-red' | 'pricing-red' | 'provider-red' |
168
173
  * 'interpreter-red' | 'cap-halt' | 'wall-halt' | 'step-stalled' |
169
174
  * 'hitl-pause' | 'hitl-decision-red' | `step-red:<id>`
175
+ * NOTE: `job-end` can ALSO land with outcome 'runner-drained' (F140/PRD item
176
+ * 28(c)) — the `beforeExit` backstop's own terminal, emitted from OUTSIDE this
177
+ * function's normal control flow when the process drains with no `job-end` at
178
+ * all (a provider promise that never settled). It never appears as this
179
+ * function's own RETURN value (nothing is left to receive it), only as the
180
+ * spine's terminal record. Non-zero exit, `spendComplete:false` always, and
181
+ * deliberately NOT in the resumable/checkpoint set (`src/reuse.js`) — an
182
+ * unknown in-flight state is not a known-safe resume point.
170
183
  */
171
- export function runJob(rawSpec: object | string, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider, emit, capRuns, strikeLimit, shellCapUsd, closeTimeoutMs, closeDir, layerRoot, readShim, scout, bridge, priorSpentUsd, priorSpendComplete, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun, resumable }: {
184
+ export function runJob(rawSpec: object | string, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider, judgeModel, emit, capRuns, strikeLimit, shellCapUsd, closeTimeoutMs, closeDir, layerRoot, readShim, scout, bridge, priorSpentUsd, priorSpendComplete, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun, resumable }: {
172
185
  approvals: unknown;
173
186
  workdir: string;
174
187
  provider: any;
@@ -180,6 +193,7 @@ export function runJob(rawSpec: object | string, { approvals, workdir, provider,
180
193
  }) => any) | undefined;
181
194
  emit: (type: string, data?: object) => object;
182
195
  providerFor?: ((tier: string) => any) | undefined;
196
+ judgeModel?: string | null | undefined;
183
197
  judgeProvider?: any;
184
198
  capRuns?: number | undefined;
185
199
  strikeLimit?: number | undefined;
@@ -2,11 +2,17 @@
2
2
  * Is `err` a transport-class failure — the request never produced an HTTP
3
3
  * response? True for: a TLS fault mid-record ("bad record mac" and kin), a
4
4
  * reset/broken/timed-out socket (ECONNRESET, EPIPE, ETIMEDOUT, ECONNREFUSED,
5
- * ENETUNREACH — by code on the error itself or on `err.cause`), and
6
- * `fetch failed` wrapping a network `cause`. False for anything carrying an
7
- * HTTP status (`err.status`/`err.statusCode`, 4xx/5xx/429) or an explicit
5
+ * ENETUNREACH — by code on the error itself or on `err.cause`), `fetch
6
+ * failed` wrapping a network `cause`, and — as of bare-agent 0.42.0 (BA-25)
7
+ * — a `ProviderError` whose `err.context.bound === 'transport'`: the
8
+ * response stream ended (`'aborted'`/`'error'`/premature `'close'`) after
9
+ * headers but before the body completed, which bare-agent itself rejects as
10
+ * retryable with no HTTP status. False for anything carrying an HTTP status
11
+ * (`err.status`/`err.statusCode`, 4xx/5xx/429) or an explicit
8
12
  * `err.retryable === false` — those are provider RESPONSES, not transport
9
- * casualties, and bare-agent's own policy governs them unchanged.
13
+ * casualties, and bare-agent's own policy governs them unchanged; both
14
+ * guards run BEFORE the `context.bound` check, so a status or an explicit
15
+ * `retryable:false` wins even if `bound` says 'transport'.
10
16
  * @param {any} err
11
17
  * @returns {boolean}
12
18
  */