bareloop 0.23.0 → 0.25.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/validate.js CHANGED
@@ -147,9 +147,28 @@ 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
 
157
+ /** Human-readable name for each entry in `SECRET_PATTERNS`, SAME order, SAME
158
+ * length — for a caller that must name WHICH shape matched (a refusal
159
+ * message, an audit line) without ever printing the matched text itself. Kept
160
+ * beside the inventory it describes rather than hand-typed at each call site,
161
+ * for the same reason `scanSecrets`/`redactSecrets` are: a second, drifting
162
+ * copy is how a name and its pattern come apart. */
163
+ export const SECRET_PATTERN_NAMES = [
164
+ 'sk- prefixed API key',
165
+ 'GitHub personal access token (ghp_)',
166
+ 'GitHub fine-grained PAT (github_pat_)',
167
+ 'AWS access key ID (AKIA)',
168
+ 'Slack token (xox)',
169
+ 'Google/Gemini API key (AIza)',
170
+ ];
171
+
153
172
  /**
154
173
  * Scan a RAW text stream (a spine file, a close's output, a transcript) for
155
174
  * known secret shapes and return the literal matches. The ONE spelling of the
@@ -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";
@@ -29,3 +29,4 @@ export { runInterview, authorCloseForJob, assembleSpec, prepareSigning, refusalE
29
29
  export { PROMPT_REGISTERS, isPromptFile } from "./promptregisters.js";
30
30
  export { exportBundle, bundleHash, readBundle, resolveBundleSpec, checkEnvelope, bless, verifyBlessing, appendHistory, checkBundleDeps } from "./bundle.js";
31
31
  export { readCloseScripts, checkCloseAbsolutePaths, absolutePathLiteralsOf, hashCloseScriptBytes, checkCloseByteSignature, checkStageByteSignature, signCloseScripts } from "./close-integrity.js";
32
+ export { prepareSource, proveDestination, copyOut, readSourceManifest, frontDoorFromManifest } from "./source.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,
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`;
@@ -201,25 +300,35 @@ export function decide(facts: any, card: any, { artifactText }?: {
201
300
  reason: string | null;
202
301
  };
203
302
  /**
204
- * THE JUDGE MODEL, PINNED. Never agent-selectable and never a step knob: it is
205
- * the only tier with established injection resistance upstream (BA-20's
206
- * INJECTION_BATTERY is haiku-4.5 only), and §4.2's whole safety argument is
207
- * worth exactly as much as that evidence. A judge-model bump forces a full
208
- * 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).
209
317
  *
210
- * THAT IS ENFORCED, not asked for, and it is enforced in three places because a
211
- * rule with no wired detector is prose (F45):
212
- * 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`
213
322
  * writes it as `closeDecl.calibration.judgeModel`, inside the spec, so
214
- * `jobSpecHash` covers it by construction. Bump this constant and every
323
+ * `jobSpecHash` covers it by construction. Change the judge and every
215
324
  * signed judged spec hashes differently: the signature dies, and re-signing
216
325
  * is what re-runs the calibration gate.
217
326
  * 2. `validateCloseDecl` REQUIRES it on any stored set — a set nobody can
218
- * attribute to a tier is a floor nobody can tell apart from a bumped one.
219
- * 3. `runJudgedFloor` refuses to grade when the stored tier is not this one,
220
- * naming both models. It does not degrade in either direction: grading on
221
- * the new tier judges against a floor nobody calibrated, and the old tier is
222
- * 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.
223
332
  */
224
333
  export const JUDGE_MODEL: "claude-haiku-4-5";
225
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
@@ -17,6 +49,30 @@ export function doorOpens(job: any, reviewDoor?: boolean | null): boolean;
17
49
  * @returns {any[]}
18
50
  */
19
51
  export function mechanicalStages(stages: any[] | null | undefined): any[];
52
+ /**
53
+ * git, read-only, in a repository. Faults come back as `ok:false` with the
54
+ * stderr — a caller decides whether that is a stop.
55
+ *
56
+ * Exported (PRD item 33/M2, `src/source.js`) so the hidden-git seed a plain
57
+ * folder/file/URL job runs against goes through the SAME spawn primitive as
58
+ * every other git read in this file — a second `spawn('git', …)` for the
59
+ * source front door would be a second instrument with its own timeout and
60
+ * buffer ceiling to keep in sync with this one.
61
+ * @param {string} cwd @param {string[]} args
62
+ */
63
+ export function git(cwd: string, args: string[]): Promise<{
64
+ ok: boolean;
65
+ out: string;
66
+ err: string;
67
+ code: null;
68
+ fault: string;
69
+ } | {
70
+ ok: boolean;
71
+ out: string;
72
+ err: string;
73
+ code: number;
74
+ fault: "failed";
75
+ }>;
20
76
  /**
21
77
  * D8's seed, READ rather than typed: HEAD at run start. A close stores the
22
78
  * counting rule and never the number (D12), so which commit the run is measured
@@ -531,10 +587,16 @@ export type Red = {
531
587
  * runner — never authored, never defaulted, and absent on every ordinary run.
532
588
  * `judgeLoop` is the PAID seam the judged stage runs its locate call through, and
533
589
  * 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
590
+ * module owns no provider and never picks one. Absent is not a fall-back to some
591
+ * other model — the stage stops as a wiring gap, the same answer `runPlan` gives a
537
592
  * native job with no native factory.
593
+ * `judgeModel` is the seam's other half (PRD item 32.1): WHICH model that loop
594
+ * drives, resolved by the runner (`resolveJudge`, src/judged.js — the spec's signed
595
+ * `judge` override, else the job's own worker model) rather than pinned in this
596
+ * library. It is required whenever `judgeLoop` is, and for the same reason: a
597
+ * verdict whose grader cannot be named is a floor nobody can attribute, and the
598
+ * signed calibration record has nothing to compare against. The two travel
599
+ * together or the stage stops.
538
600
  * `onJudgeCost` is the METER, and it is the only reason a judged stage can be
539
601
  * funded honestly: this executor cannot reach the run's ledger, so every locate
540
602
  * call's cost is reported OUT through it, once per call, on every route including
@@ -566,6 +628,7 @@ export type Ctx = {
566
628
  judgeLoop?: (o: {
567
629
  system: string;
568
630
  }) => any;
631
+ judgeModel?: string;
569
632
  onJudgeCost?: (c: {
570
633
  stage: string | null;
571
634
  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;