bareloop 0.23.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/src/providers.js CHANGED
@@ -12,16 +12,18 @@
12
12
  // models; it is exactly the two provider identities the PROVIDERS menu
13
13
  // (`src/job.js`) admits.
14
14
  //
15
- // What this module is NOT: it never picks a judge (the judge stays pinned
16
- // to `anthropic-api`/`JUDGE_MODEL` regardless of the job's worker provider —
17
- // arbiter territory, PRD item 28), never prices a round (F113: rates
18
- // passthrough is dead), and never decides a budget. It only constructs the
19
- // provider objects `bare-agent`'s `Loop` calls.
15
+ // What this module is NOT: it never RESOLVES a judge identity itself (that
16
+ // stays arbiter territory, `resolveJudge`/`resolveJobJudge` in
17
+ // src/judged.js — the spec's signed `judge` override, else the job's own
18
+ // worker provider and model, PRD item 32) — it only CONSTRUCTS the judge
19
+ // provider object from an identity the caller already resolved
20
+ // (`buildRunnerProviders`, below); it never prices a round (F113: rates
21
+ // passthrough is dead), and never decides a budget.
20
22
 
21
23
  import { createRequire } from 'node:module';
22
24
 
23
25
  const require = createRequire(import.meta.url);
24
- const { AnthropicProvider, OpenAIProvider } = require('bare-agent/providers');
26
+ const { AnthropicProvider, OpenAIProvider, GeminiProvider } = require('bare-agent/providers');
25
27
 
26
28
  /**
27
29
  * Today's anthropic-api tier table — EXACTLY the `DEFAULT_TIER_MODELS` both
@@ -49,6 +51,38 @@ export const OPENAI_TIER_MODELS = Object.freeze({
49
51
  haiku: 'deepseek-chat',
50
52
  });
51
53
 
54
+ /**
55
+ * Gemini (PRD item 31.3, hamr 2026-09-09: "anthropic, openai, gemini drop
56
+ * ollama for now"). Two real tiers, unlike DeepSeek's single model: `flash` is
57
+ * the cheap tier by design, so `haiku` maps to it honestly rather than
58
+ * repeating one id.
59
+ *
60
+ * ADMITTED-PENDING-PROBE. The probe rule stands (PRD item 28 ruling (d),
61
+ * 2026-09-09): no endpoint is in the menu without its own clean paid probe,
62
+ * and `gemini-api` has ZERO runs. `anthropic-api` is the original (~167
63
+ * archived runs) and `openai-api` was probed green through the shipped runner
64
+ * (run mtu12vks, $1.64/$4, F157); gemini owes that fire and has not paid it.
65
+ * `PROBE_STATUS` below is the machine-readable form of that debt — nothing in
66
+ * here silently pretends the provider is proven.
67
+ */
68
+ export const GEMINI_TIER_MODELS = Object.freeze({
69
+ sonnet: 'gemini-2.5-pro',
70
+ haiku: 'gemini-2.5-flash',
71
+ });
72
+
73
+ /**
74
+ * Which admitted providers have paid for their own end-to-end probe, and which
75
+ * have not. Read by the runner to print a loud marker; never read to REFUSE —
76
+ * hamr admitted gemini to the menu, and a table that quietly withheld it would
77
+ * be a second, invisible ruling. An unprobed provider runs, and says so.
78
+ * @type {Readonly<Record<string, {probed: boolean, evidence: string}>>}
79
+ */
80
+ export const PROBE_STATUS = Object.freeze({
81
+ 'anthropic-api': Object.freeze({ probed: true, evidence: 'the original surface — ~167 archived runs' }),
82
+ 'openai-api': Object.freeze({ probed: true, evidence: 'run mtu12vks, green through scripts/run-u.mjs, $1.64/$4 (F157)' }),
83
+ 'gemini-api': Object.freeze({ probed: false, evidence: 'ZERO runs — admitted to the menu, owes its own paid probe (PRD item 31.3)' }),
84
+ });
85
+
52
86
  /**
53
87
  * Per-MODEL request-key gating (F149, live-measured 2026-09-09): DeepSeek
54
88
  * silently IGNORES the modern `max_completion_tokens` key — asked for 64
@@ -73,15 +107,44 @@ const OPENAI_MODEL_OPTIONS = Object.freeze({
73
107
  * @property {new (options: any) => any} ctor the bare-agent provider class
74
108
  * @property {string} envKey the environment variable this provider reads its key from
75
109
  * @property {Readonly<Record<string, string>>} tiers tier name -> model id
76
- * @property {(model: string|undefined) => Record<string, any>} paramsFor per-model request-key/param gating, applied on TOP of {exposeErrorBody:true, apiKey, model, baseUrl?}
110
+ * @property {string} endpointKey the CONSTRUCTOR OPTION NAME this provider reads its endpoint from — NOT assumed to be `baseUrl` (see below)
111
+ * @property {(model: string|undefined) => Record<string, any>} paramsFor per-model request-key/param gating, applied on TOP of {exposeErrorBody:true, apiKey, model, <endpointKey>?}
77
112
  */
78
113
 
114
+ // ── WHY `endpointKey` IS A FIELD AND NOT THE LITERAL `baseUrl` ──────────────
115
+ //
116
+ // hamr's framing for this item was "api + api shape + endpoint" (PRD 31.3), and
117
+ // the endpoint half is the one that does NOT generalize. Read from bare-agent's
118
+ // own constructors, not assumed:
119
+ //
120
+ // AnthropicProvider `baseUrl` (provider-anthropic.js:49)
121
+ // OpenAIProvider `baseUrl` (provider-openai.js:76)
122
+ // GeminiProvider `baseUrl` (provider-gemini.js:47)
123
+ // OllamaProvider `url` (provider-ollama.js:32) <- and NO apiKey at all
124
+ //
125
+ // Every one of these constructors defaults its endpoint when the option is
126
+ // absent and none of them validate unknown option names. So passing `baseUrl`
127
+ // to Ollama would not throw: it would be IGNORED, the provider would default to
128
+ // `http://localhost:11434`, and the run would quietly talk to the wrong machine.
129
+ // That is the F149 class exactly — DeepSeek silently ignoring
130
+ // `max_completion_tokens` and returning 665 tokens for a 64-token cap — where a
131
+ // silently-dropped option produces a plausible-looking run against a
132
+ // configuration nobody chose.
133
+ //
134
+ // Ollama is NOT admitted today (hamr, 2026-09-09: "drop ollama for now"), so
135
+ // this field currently reads `baseUrl` for all three entries and its value is
136
+ // never exercised against a second spelling. It is here anyway, because the
137
+ // alternative is a hardcoded `baseUrl` that is correct for exactly as long as
138
+ // the menu holds only http-shaped API providers, and the FIRST thing that
139
+ // changes when Ollama is admitted is the one line nobody would think to look at.
140
+
79
141
  /** @type {Readonly<Record<string, ProviderTableEntry>>} */
80
142
  const PROVIDER_TABLE = Object.freeze({
81
143
  'anthropic-api': Object.freeze({
82
144
  ctor: AnthropicProvider,
83
145
  envKey: 'ANTHROPIC_API_KEY',
84
146
  tiers: ANTHROPIC_TIER_MODELS,
147
+ endpointKey: 'baseUrl',
85
148
  // Anthropic has only ever had one request key for the output cap
86
149
  // (`max_tokens`) — nothing to gate here, ever (unlike OpenAI-shaped
87
150
  // backends, which fork on `max_tokens` vs `max_completion_tokens`).
@@ -91,8 +154,19 @@ const PROVIDER_TABLE = Object.freeze({
91
154
  ctor: OpenAIProvider,
92
155
  envKey: 'OPENAI_API_KEY',
93
156
  tiers: OPENAI_TIER_MODELS,
157
+ endpointKey: 'baseUrl',
94
158
  paramsFor: (model) => OPENAI_MODEL_OPTIONS[/** @type {string} */ (model)] ?? {},
95
159
  }),
160
+ 'gemini-api': Object.freeze({
161
+ ctor: GeminiProvider,
162
+ envKey: 'GEMINI_API_KEY',
163
+ tiers: GEMINI_TIER_MODELS,
164
+ endpointKey: 'baseUrl',
165
+ // Gemini carries no `max_tokens`/`max_completion_tokens` fork of its own —
166
+ // bare-agent's GeminiProvider maps the cap internally. Nothing to gate, and
167
+ // an empty object is the honest statement of that, not an omission.
168
+ paramsFor: () => ({}),
169
+ }),
96
170
  });
97
171
 
98
172
  /**
@@ -130,7 +204,13 @@ export function makeProvider(providerName, { apiKey, model, baseUrl } = {}) {
130
204
  exposeErrorBody: true,
131
205
  apiKey,
132
206
  model,
133
- ...(baseUrl !== undefined ? { baseUrl } : {}),
207
+ // the endpoint goes in under THIS PROVIDER'S OWN OPTION NAME, never a
208
+ // hardcoded `baseUrl` (PRD item 31.3). The spec field stays `baseUrl` for
209
+ // everyone — one name for the person writing the job — and the translation
210
+ // to the constructor's spelling happens here, once. A provider whose option
211
+ // is spelled differently would otherwise IGNORE the endpoint silently and
212
+ // run against its own default; see the note above the table.
213
+ ...(baseUrl !== undefined ? { [entry.endpointKey]: baseUrl } : {}),
134
214
  ...entry.paramsFor(model),
135
215
  });
136
216
  }
@@ -144,24 +224,33 @@ export function makeProvider(providerName, { apiKey, model, baseUrl } = {}) {
144
224
  * before this factory existed — that decision stays where each runner's own
145
225
  * `--model` flag/spec precedence lives, not duplicated in here).
146
226
  *
147
- * The JUDGE stays pinned to `anthropic-api`/`judgeModel` regardless of
148
- * `providerName` — arbiter territory (PRD item 28: "the judge stays PINNED
149
- * … regardless of the job's worker provider"). A signed `judge:{provider,
150
- * model}` field with its own calibration record is PRD item 28 part (2),
151
- * NOT built here; until it lands, the judge always needs its own
152
- * `judgeApiKey` (today, always `ANTHROPIC_API_KEY`), independent of which
153
- * key the worker used.
227
+ * THE JUDGE IS NO LONGER PINNED TO ANTHROPIC (PRD item 32.2, hamr 2026-09-10:
228
+ * *"what i care about is that judge becomes llm agnostic and not set to one
229
+ * model or provider"*). It is constructed through this same factory, from a
230
+ * `judgeProviderName`/`judgeModel` pair the CALLER resolved (`resolveJudge`,
231
+ * src/judged.js — the spec's signed `judge` override, else the job's own worker
232
+ * provider and model). Item 28's pin, and item 28 part (2)'s deferral of a
233
+ * signed `judge:{provider,model}` field, both close here.
234
+ *
235
+ * `judgeApiKey` stays a SEPARATE argument rather than being derived: the judge
236
+ * may legitimately be a different provider from the worker, and reading its key
237
+ * for it would put a second env-var reader in a module whose whole job is to
238
+ * have exactly one. The caller reads the resolved judge provider's own `envKey`
239
+ * and hands the key in — the same contract the worker key already has here.
154
240
  * @param {object} args
155
241
  * @param {string} args.providerName spec.provider (or the anthropic-api default)
156
242
  * @param {string|undefined} args.apiKey the WORKER's key, read from the provider's own `envKey`
157
243
  * @param {string} args.model the already-resolved worker model id
158
244
  * @param {Readonly<Record<string, string>>} args.tierModels the already-resolved tier -> model map (spec override folded in by the caller)
159
245
  * @param {string|undefined} [args.baseUrl] spec.baseUrl, if any
160
- * @param {string|undefined} args.judgeApiKey the judge's own key (today: `ANTHROPIC_API_KEY`)
161
- * @param {string} args.judgeModel `JUDGE_MODEL`
246
+ * @param {string|undefined} args.judgeApiKey the judge's own key, read by the caller from the RESOLVED judge provider's `envKey`
247
+ * @param {string} args.judgeModel the resolved judge model id
248
+ * @param {string} [args.judgeProviderName] the resolved judge provider name; defaults to `providerName` (the job's own worker provider), which is `resolveJudge`'s own default
249
+ * @param {string|undefined} [args.judgeBaseUrl] the judge provider's endpoint, when it takes one
162
250
  */
163
251
  export function buildRunnerProviders({
164
252
  providerName, apiKey, model, tierModels, baseUrl, judgeApiKey, judgeModel,
253
+ judgeProviderName = providerName, judgeBaseUrl = undefined,
165
254
  }) {
166
255
  const provider = makeProvider(providerName, { apiKey, model, baseUrl });
167
256
  /** @type {Record<string, any>} */
@@ -171,6 +260,42 @@ export function buildRunnerProviders({
171
260
  ? provider
172
261
  : makeProvider(providerName, { apiKey, model: tierModels[tier], baseUrl })
173
262
  ));
174
- const judgeProvider = makeProvider('anthropic-api', { apiKey: judgeApiKey, model: judgeModel });
263
+ // the judge goes through the SAME table as the worker — one place names a
264
+ // provider identity, which is the entire reason this factory exists. Reusing
265
+ // the worker instance when the identity matches exactly is not an optimisation
266
+ // for its own sake: a second instance against the same endpoint/model is a
267
+ // second prompt-cache prefix, and cache writes are ~41% of the bill.
268
+ const judgeProvider = (judgeProviderName === providerName && judgeModel === model && judgeApiKey === apiKey
269
+ && judgeBaseUrl === baseUrl)
270
+ ? provider
271
+ : makeProvider(judgeProviderName, { apiKey: judgeApiKey, model: judgeModel, baseUrl: judgeBaseUrl });
175
272
  return { provider, providerFor, judgeProvider };
176
273
  }
274
+
275
+ /**
276
+ * The launch-time warning lines for an ADMITTED-PENDING-PROBE provider, or
277
+ * `null` when the provider has paid for its own end-to-end probe (and for an
278
+ * off-table name — an unknown provider is `resolveProvider`'s named throw to
279
+ * report, never this function's warning to soften).
280
+ *
281
+ * This lives here, next to `PROBE_STATUS`, rather than inline in the runner,
282
+ * for one reason: inline it was UNTESTABLE. `scripts/run-u.mjs` reaches this
283
+ * point only with a real job row (a machine-local patient path) and a real
284
+ * signed spec, so nothing in the suite could reach the branch, and a warning
285
+ * nothing executes drifts exactly the way the green-only prose did (item 31.2).
286
+ *
287
+ * It never REFUSES, and it never returns a shape a caller can mistake for a
288
+ * refusal: hamr admitted `gemini-api` to the menu, and a probe table that
289
+ * quietly withheld it would be a second ruling nobody made.
290
+ * @param {string} providerName
291
+ * @returns {string[]|null} lines to print, in order, or null when there is nothing to say
292
+ */
293
+ export function probeWarningLines(providerName) {
294
+ const probe = PROBE_STATUS[/** @type {keyof typeof PROBE_STATUS} */ (providerName)];
295
+ if (!probe || probe.probed !== false) return null;
296
+ return [
297
+ `⚠ UNPROVEN PROVIDER — "${providerName}" is on the menu but has never been probed end to end.`,
298
+ ` ${probe.evidence}`,
299
+ ' It will run. Nothing has shown that it CAN run this job to a close-rendered verdict.',
300
+ ];
301
+ }
package/src/run.js CHANGED
@@ -95,9 +95,14 @@ async function primitiveSmoke(workdir) {
95
95
  * metered claude-json text provider for the toolless drafter); ignored on every Loop-driven provider
96
96
  * @param {(type: string, data?: object) => object} opts.emit spine emitter
97
97
  * @param {(tier: string) => any} [opts.providerFor] P: per-step model-tier provider factory (forwarded to the plan flow)
98
+ * @param {string|null} [opts.judgeModel] SOFTGREEN — WHICH model the judge provider drives
99
+ * (PRD item 32.1), resolved by the CALLER (`resolveJudge`, src/judged.js) and passed
100
+ * through untouched. Travels with `judgeProvider`; absent, a judged stage stops as a
101
+ * wiring gap rather than grading under an identity nobody named.
98
102
  * @param {any} [opts.judgeProvider] SOFTGREEN — the provider a JUDGED close stage runs its
99
- * locate call through, wired by the operator and PINNED to `JUDGE_MODEL` (src/judged.js:
100
- * the only tier with established injection resistance upstream). Forwarded to the plan
103
+ * locate call through, built by the CALLER from the RESOLVED judge identity (PRD item
104
+ * 32.1: `resolveJudge`/`resolveJobJudge`, src/judged.js — the spec's signed `judge`
105
+ * override, else the job's own worker provider and model), never pinned. Forwarded to the plan
101
106
  * flow. It is its own seam and not `provider`/`providerFor`, because the judge tier is
102
107
  * never a step knob and never agent-selectable; absent, a judged stage instrument-stops as
103
108
  * a wiring gap rather than grading on whatever binding happened to be at hand. Its spend
@@ -237,7 +242,7 @@ async function primitiveSmoke(workdir) {
237
242
  * deliberately NOT in the resumable/checkpoint set (`src/reuse.js`) — an
238
243
  * unknown in-flight state is not a known-safe resume point.
239
244
  */
240
- export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider = null, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, closeDir = null, layerRoot = false, readShim = false, scout = true, bridge = null, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null, heldRuling = null, reviewDoor = null, doorRerun = null, resumable = true }) {
245
+ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider = null, judgeModel = null, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, closeDir = null, layerRoot = false, readShim = false, scout = true, bridge = null, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null, heldRuling = null, reviewDoor = null, doorRerun = null, resumable = true }) {
241
246
  // THE READ SHIM's ARM, resolved at the door — the FIRST thing this entry does,
242
247
  // before the ledger, before the approval gate, before a byte of the spec is read.
243
248
  // An unrecognised spelling throws here at zero cost instead of being coerced by
@@ -487,7 +492,7 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
487
492
  // accounts it natively (F12) and the job-end money contract is unchanged.
488
493
  {
489
494
  const outcome = await runPlan(job, {
490
- workdir, provider, nativeProvider, providerFor, judgeProvider, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, closeDir, layerRoot, readShim, scout, bridge, priorWallMs: chainWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun, priorSpentUsd: chainFoldUsd, resumable,
495
+ workdir, provider, nativeProvider, providerFor, judgeProvider, judgeModel, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, closeDir, layerRoot, readShim, scout, bridge, priorWallMs: chainWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun, priorSpentUsd: chainFoldUsd, resumable,
491
496
  remainingUsd: () => Math.min(shellCapUsd, job.budgetUsd - spentUsd),
492
497
  isUnpriced: () => unpriced, // F6: let the plan flow bail in-flight, not just after it returns
493
498
  spendComplete, // …and let its money-halt readout say whether the remaining it quotes is exact
package/src/validate.js CHANGED
@@ -147,6 +147,10 @@ export const SECRET_PATTERNS = [
147
147
  /(?<![A-Za-z0-9_-])github_pat_[A-Za-z0-9_]{20,}/,
148
148
  /(?<![A-Za-z0-9_-])AKIA[0-9A-Z]{16}/,
149
149
  /(?<![A-Za-z0-9_-])xox[bap]-[A-Za-z0-9-]{10,}/,
150
+ // Google/Gemini API key: `AIza` + 35 chars of [0-9A-Za-z_-] (39 total, fixed
151
+ // length by Google's own issuance format). Admitted with the gemini-api
152
+ // provider (PRD item 31.3, F160) — was missing on this branch until then.
153
+ /(?<![A-Za-z0-9_-])AIza[0-9A-Za-z_-]{35}/,
150
154
  ];
151
155
  const SECRET_RE = new RegExp(SECRET_PATTERNS.map((r) => r.source).join('|'));
152
156
 
@@ -328,6 +328,36 @@ export const VERDICT_CLASSES: readonly string[];
328
328
  export const LOCKED_CLASSES: readonly string[];
329
329
  /** the classes v1 actually builds */
330
330
  export const LIVE_CLASSES: readonly string[];
331
+ /** OFF THE MENU, but still built — a THIRD state, distinct from both lists above
332
+ * (PRD item 31.1, hamr 2026-09-09: *"retire hitl from list, keep its code"*, and
333
+ * *"if it's not on menu, you can't reach it, from menu"*).
334
+ *
335
+ * `hitl` was retired by ruling at v1.71 and the class moved to fwdloop (PRD item
336
+ * 29), but no constant was ever updated, so `runInterview` still returned
337
+ * `ok:true` for a hitl pick — an authorable job for a class this product no
338
+ * longer closes.
339
+ *
340
+ * Why this is NOT `LOCKED_CLASSES`. Locked means INEXPRESSIBLE: no guard battery
341
+ * exists, so `validateCloseDecl` cannot check the close and must refuse
342
+ * (`src/declaredclose.js:306`). hitl's battery, question set, composition rule and
343
+ * runtime all exist and are tested — locking it would make every one of them
344
+ * unreachable, which is not "keeping the code", it is abandoning it in place with
345
+ * its tests deleted. So the split is:
346
+ *
347
+ * LOCKED_CLASSES — no battery. Cannot be validated, cannot run. (empty today)
348
+ * UNLISTED_CLASSES — battery exists, validates and RUNS. Just never OFFERED.
349
+ * LIVE_CLASSES — everything the system can validate and run (locked removed).
350
+ *
351
+ * Read by the AUTHORING surface only (`runInterview`, `authorClose`'s precheck).
352
+ * The spec validator, the close validator and the runtime deliberately do NOT
353
+ * read it: a hitl spec that already exists still validates and still runs, which
354
+ * is what keeps the code alive and under test. Nothing can AUTHOR a new one.
355
+ * @type {readonly string[]} */
356
+ export const UNLISTED_CLASSES: readonly string[];
357
+ /** what the authoring interview actually OFFERS: live, minus unlisted. This is
358
+ * "the menu" in hamr's sentence, and it is the only list an authoring refusal
359
+ * should ever name. */
360
+ export const MENU_CLASSES: readonly string[];
331
361
  /**
332
362
  * The whole vocabulary, as DATA. `shape` and `asserts` are the strings the
333
363
  * authoring prompt renders (M3) — the prompt and this validator must judge the
@@ -81,7 +81,7 @@ export function refuseLockedKind(kind: string): Refusal;
81
81
  * @param {{answers: Record<string|number, any>, repoPath?: string|null, lang: string,
82
82
  * verdictType?: string|null,
83
83
  * questions?: Record<string|number, string>|null, generate?: Function, provider?: any,
84
- * seedRef?: string|null, scout?: any, listing?: any, ceilingUsd?: number|null,
84
+ * seedRef?: string|null, scout?: any, listing?: any, ceilingUsd?: number|null, judgeModel?: string|null,
85
85
  * onPhase?: (phase: string, data?: any) => void,
86
86
  * onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void,
87
87
  * seedFn?: Function, scoutFn?: Function, listingFn?: Function,
@@ -91,7 +91,7 @@ export function refuseLockedKind(kind: string): Refusal;
91
91
  * closeDecl: any, seedRef: string|null, authoring: any, judged: any, reds: Red[],
92
92
  * stop: string|null, cost: any, interview: any}>}
93
93
  */
94
- export function authorCloseForJob({ answers, repoPath, lang, verdictType, questions, generate, provider, seedRef, scout, listing, ceilingUsd, onPhase, onCall, seedFn, scoutFn, listingFn, authorFn, authorOpts, signerFix, proposeFn, compileOpts, }: {
94
+ export function authorCloseForJob({ answers, repoPath, lang, verdictType, questions, generate, provider, seedRef, scout, listing, ceilingUsd, judgeModel, onPhase, onCall, seedFn, scoutFn, listingFn, authorFn, authorOpts, signerFix, proposeFn, compileOpts, }: {
95
95
  answers: Record<string | number, any>;
96
96
  repoPath?: string | null;
97
97
  lang: string;
@@ -103,6 +103,7 @@ export function authorCloseForJob({ answers, repoPath, lang, verdictType, questi
103
103
  scout?: any;
104
104
  listing?: any;
105
105
  ceilingUsd?: number | null;
106
+ judgeModel?: string | null;
106
107
  onPhase?: (phase: string, data?: any) => void;
107
108
  onCall?: (call: {
108
109
  label: string;
@@ -207,12 +208,12 @@ export function assembleSpec(specDraft: any, { closeDecl, verdictType }: {
207
208
  *
208
209
  * @param {{spec: any, workdir: string, seedRef?: string|null, shellCapUsd?: number,
209
210
  * timeoutMs?: number, seedFn?: Function, listingFn?: Function, seedReadFn?: Function,
210
- * judgeLoop?: Function|null, onJudgeCost?: ((c: any) => void)|null, calibrateFn?: Function,
211
- * ceilingUsd?: number|null, priorCalls?: any[]}} o
211
+ * judgeLoop?: Function|null, judgeModel?: string|null, onJudgeCost?: ((c: any) => void)|null,
212
+ * calibrateFn?: Function, ceilingUsd?: number|null, priorCalls?: any[]}} o
212
213
  * @returns {Promise<{ok: boolean, specHash: string|null, seedRef: string|null,
213
214
  * gates: any, work: any[], guards: any[], stops: any[], reds: Red[], refusal: Refusal|null}>}
214
215
  */
215
- export function prepareSigning({ spec, workdir, seedRef, shellCapUsd, timeoutMs, seedFn, listingFn, seedReadFn, judgeLoop, onJudgeCost, calibrateFn, ceilingUsd, priorCalls, }: {
216
+ export function prepareSigning({ spec, workdir, seedRef, shellCapUsd, timeoutMs, seedFn, listingFn, seedReadFn, judgeLoop, judgeModel, onJudgeCost, calibrateFn, ceilingUsd, priorCalls, }: {
216
217
  spec: any;
217
218
  workdir: string;
218
219
  seedRef?: string | null;
@@ -222,6 +223,7 @@ export function prepareSigning({ spec, workdir, seedRef, shellCapUsd, timeoutMs,
222
223
  listingFn?: Function;
223
224
  seedReadFn?: Function;
224
225
  judgeLoop?: Function | null;
226
+ judgeModel?: string | null;
225
227
  onJudgeCost?: ((c: any) => void) | null;
226
228
  calibrateFn?: Function;
227
229
  ceilingUsd?: number | null;
@@ -277,4 +279,6 @@ import { requiredAnswersFor } from './authorflow.js';
277
279
  import { VERDICT_CLASSES } from './authoring.js';
278
280
  import { LOCKED_CLASSES } from './authoring.js';
279
281
  import { LIVE_CLASSES } from './authoring.js';
280
- export { QUESTION_SETS, questionsFor, requiredAnswersFor, VERDICT_CLASSES, LOCKED_CLASSES, LIVE_CLASSES };
282
+ import { UNLISTED_CLASSES } from './authoring.js';
283
+ import { MENU_CLASSES } from './authoring.js';
284
+ export { QUESTION_SETS, questionsFor, requiredAnswersFor, VERDICT_CLASSES, LOCKED_CLASSES, LIVE_CLASSES, UNLISTED_CLASSES, MENU_CLASSES };
@@ -80,7 +80,13 @@ export function factsResist(facts: any, gold: any, artifactText: string): {
80
80
  * graded one: `ok` is false, the cases nobody bought are absent rather than recorded
81
81
  * as failures, and `casualty` stays null — the transport was fine, the operator's own
82
82
  * governance stopped the gate.
83
- * @param {{cases: any, card: any, judgeLoop: Function|null,
83
+ * `judgeModel` is REQUIRED and has no default (PRD item 32.1). It is the identity
84
+ * the caller resolved (`resolveJudge`, src/judged.js) and it is what the graded set
85
+ * is STAMPED with — `setHash` folds it in, so a set certified by one judge cannot be
86
+ * mistaken for the same set certified by another. A default here would be the silent
87
+ * fall-back the whole judge-identity change exists to remove: a gate that grades
88
+ * under a model nobody named certifies a floor nobody can attribute.
89
+ * @param {{cases: any, card: any, judgeLoop: Function|null, judgeModel: string,
84
90
  * onCost?: (c: any) => void, attempts?: number, battery?: any[], batteryCard?: any,
85
91
  * capStop?: () => 'cap-halt'|'pricing-red'|null}} o
86
92
  * @returns {Promise<{ok: boolean, stop: string|null, casualty: any,
@@ -90,10 +96,11 @@ export function factsResist(facts: any, gold: any, artifactText: string): {
90
96
  * cardHash: string|null, casesHash: string|null, setHash: string|null,
91
97
  * costUsd: number|null, knownUsd: number, spendComplete: boolean, calls: any[]}>}
92
98
  */
93
- export function runCalibration({ cases, card, judgeLoop, onCost, attempts, battery, batteryCard, capStop, }: {
99
+ export function runCalibration({ cases, card, judgeLoop, judgeModel, onCost, attempts, battery, batteryCard, capStop, }: {
94
100
  cases: any;
95
101
  card: any;
96
102
  judgeLoop: Function | null;
103
+ judgeModel: string;
97
104
  onCost?: (c: any) => void;
98
105
  attempts?: number;
99
106
  battery?: any[];
@@ -261,15 +261,16 @@ export function signJudgedArtifacts({ proposal, fix }: {
261
261
  * - THE CASES land beside the stages as `calibration.cases` — a close-level
262
262
  * artifact, because the set calibrates the RULER and not one stage's params,
263
263
  * and because module 5's gate reads it before any stage runs.
264
- * - THE JUDGE MODEL lands beside them as `calibration.judgeModel`, and it is the
265
- * PIN (`JUDGE_MODEL`) rather than anything a caller may name. A calibration set
266
- * is only worth the tier that graded it — §4.2's whole safety argument is worth
267
- * exactly as much as BA-20's injection evidence, which is haiku-4.5's alone — so
268
- * the set has to carry which tier certified it or nothing downstream can tell a
269
- * signed floor from a bumped one. Storing it HERE, inside the spec, is what makes
270
- * a bump flip `jobSpecHash`: the signature dies, the signer re-signs, and a
271
- * re-sign is a re-run of the calibration gate. That is the docstring on
272
- * `JUDGE_MODEL` finally given a detector instead of a promise (F45).
264
+ * - THE JUDGE MODEL lands beside them as `calibration.judgeModel`. Since PRD item
265
+ * 32.1 that is the RESOLVED judge identity the caller hands in (`resolveJudge`,
266
+ * src/judged.js — the spec's signed `judge` override, else the job's own worker
267
+ * model), not a library pin. A calibration set is only worth the judge that
268
+ * graded it, so the set has to carry which judge certified it or nothing
269
+ * downstream can tell a signed floor from a moved one. Storing it HERE, inside
270
+ * the spec, is what makes a judge change flip `jobSpecHash`: the signature dies,
271
+ * the signer re-signs, and a re-sign is a re-run of the calibration gate. It is
272
+ * REQUIRED, with no default — a set stamped with a model nobody named is exactly
273
+ * the unattributable floor this field exists to prevent (F45).
273
274
  *
274
275
  * A card with NOWHERE TO LAND THROWS. It is a caller's own broken input — a
275
276
  * signed rubric folded into a close with no judged ruler — and there is no safe
@@ -278,12 +279,13 @@ export function signJudgedArtifacts({ proposal, fix }: {
278
279
  * The input is never mutated: a closeDecl is a signed artefact and rewriting a
279
280
  * caller's copy in place is how two readers come to disagree about what was
280
281
  * signed.
281
- * @param {any} closeDecl @param {{card: any, cases: any[]}} o
282
+ * @param {any} closeDecl @param {{card: any, cases: any[], judgeModel: string}} o
282
283
  * @returns {any} a new closeDecl
283
284
  */
284
- export function foldJudgedArtifacts(closeDecl: any, { card, cases }: {
285
+ export function foldJudgedArtifacts(closeDecl: any, { card, cases, judgeModel }: {
285
286
  card: any;
286
287
  cases: any[];
288
+ judgeModel: string;
287
289
  }): any;
288
290
  /**
289
291
  * ONE compile call: the person's Q6 and Q7 in, a proposal out.
package/types/cli.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * code, so `bin/bareloop.mjs` can set `process.exitCode` and let node flush
4
4
  * queued stdout on its own (F-something: `process.exit()` can discard it).
5
5
  * @param {string[]} argv
6
- * @param {{ env?: Record<string,string|undefined>, stdout?: any, stderr?: any, cwd?: string, provider?: any, providerFor?: any, judgeProvider?: any, now?: () => number, stdin?: any }} deps
6
+ * @param {{ env?: Record<string,string|undefined>, stdout?: any, stderr?: any, cwd?: string, provider?: any, providerFor?: any, judgeProvider?: any, judgeModel?: string|null, now?: () => number, stdin?: any }} deps
7
7
  * @returns {Promise<number>}
8
8
  */
9
9
  export function main(argv: string[], deps?: {
@@ -14,6 +14,7 @@ export function main(argv: string[], deps?: {
14
14
  provider?: any;
15
15
  providerFor?: any;
16
16
  judgeProvider?: any;
17
+ judgeModel?: string | null;
17
18
  now?: () => number;
18
19
  stdin?: any;
19
20
  }): Promise<number>;
@@ -110,6 +110,7 @@ export function guardNames(closeDecl: any, verdictType: string): string[];
110
110
  * seedTrees?: any, gapCap?: number, maxBuffer?: number, baselineMode?: 'auto'|'worktree',
111
111
  * humanRuling?: {decision: string, text?: string|null}|null,
112
112
  * judgeLoop?: ((o: {system: string}) => any)|null,
113
+ * judgeModel?: string|null,
113
114
  * onJudgeCost?: ((c: any) => void)|null,
114
115
  * callBounds?: (() => {timeoutMs?: number, deadlineMs?: number})|null}} [opts]
115
116
  * @returns {Promise<any>}
@@ -129,6 +130,7 @@ export function runDeclaredStages(stages: any[], redact?: (s: string) => string,
129
130
  judgeLoop?: ((o: {
130
131
  system: string;
131
132
  }) => any) | null;
133
+ judgeModel?: string | null;
132
134
  onJudgeCost?: ((c: any) => void) | null;
133
135
  callBounds?: (() => {
134
136
  timeoutMs?: number;
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
@@ -210,7 +210,28 @@ export const CADENCE_UNITS: readonly string[];
210
210
  /** PRD item 28 (2026-09-06/09-09): `openai-api` — an OpenAI-shaped provider,
211
211
  * constructed via `src/providers.js`'s factory. hamr's ruling (PRD 30.7):
212
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. */
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. */
214
235
  export const PROVIDERS: readonly string[];
215
236
  /** V3 environment label: declared keys only — every field is a lineage-key
216
237
  * candidate at N3. `provider` is part of the key by definition (top-level,