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/CHANGELOG.md +122 -0
- package/bareloop.context.md +56 -19
- package/package.json +1 -1
- package/src/authorflow.js +17 -7
- package/src/authoring.js +32 -0
- package/src/authorjob.js +108 -36
- package/src/calibrate.js +33 -12
- package/src/cardauthor.js +23 -16
- package/src/cli.js +36 -5
- package/src/declaredclose.js +17 -10
- package/src/index.js +8 -5
- package/src/job.js +58 -3
- package/src/judged.js +132 -14
- package/src/kinds.js +77 -12
- package/src/planrun.js +14 -2
- package/src/providers.js +143 -18
- package/src/run.js +9 -4
- package/src/validate.js +4 -0
- package/types/authoring.d.ts +30 -0
- package/types/authorjob.d.ts +10 -6
- package/types/calibrate.d.ts +9 -2
- package/types/cardauthor.d.ts +13 -11
- package/types/cli.d.ts +2 -1
- package/types/declaredclose.d.ts +2 -0
- package/types/index.d.ts +1 -1
- package/types/job.d.ts +22 -1
- package/types/judged.d.ts +123 -14
- package/types/kinds.d.ts +42 -3
- package/types/planrun.d.ts +10 -2
- package/types/providers.d.ts +73 -11
- package/types/run.d.ts +9 -3
package/src/index.js
CHANGED
|
@@ -146,15 +146,18 @@ export {
|
|
|
146
146
|
// hitl checkpoint keeps. It never returns, writes or implies a verdict.
|
|
147
147
|
export { answerReviewDoor, doorRecordOf, doorAgeGate } from './reviewdoor.js';
|
|
148
148
|
// SOFTGREEN module 1 — the judged floor's core. The pieces an adopter (and an
|
|
149
|
-
// integrating UI) genuinely needs:
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
149
|
+
// integrating UI) genuinely needs: `resolveJudge` (PRD item 32.1 — WHICH model
|
|
150
|
+
// grades a job: the spec's signed `judge` override, else the job's own worker
|
|
151
|
+
// model, and no library fall-back), `JUDGE_MODEL` (the pre-item-32 pin, kept
|
|
152
|
+
// exported for adopters and still the right default for an anthropic-api job,
|
|
153
|
+
// but no longer read by any grading path), the rulebook a card SELECTS from,
|
|
154
|
+
// the card gate, and the two halves themselves so a calibration harness can
|
|
155
|
+
// grade the whole pipe without running a close.
|
|
153
156
|
// `defaultJudgeLoop` is the one spelling of how this repo drives a judge; the
|
|
154
157
|
// runner reaches it through `runPlan`'s `judgeProvider`, and it is exported so a
|
|
155
158
|
// caller building its own harness does not spell a second one.
|
|
156
159
|
export {
|
|
157
|
-
JUDGE_MODEL, JUDGE_MAX_TOKENS, JUDGE_RULES, JUDGE_RULE_IDS, LOCATE_AXES, LOCATE_LABEL,
|
|
160
|
+
JUDGE_MODEL, resolveJudge, JUDGE_MAX_TOKENS, JUDGE_RULES, JUDGE_RULE_IDS, LOCATE_AXES, LOCATE_LABEL,
|
|
158
161
|
validateCard, validateFacts, locatePrompt, runLocate, decide, defaultJudgeLoop,
|
|
159
162
|
// SOFTGREEN module 4 — what a legal SIGNED calibration set is. The SIZE is
|
|
160
163
|
// hamr's own ruling and a size change is a spec-level threshold change;
|
package/src/job.js
CHANGED
|
@@ -136,8 +136,29 @@ export const CADENCE_UNITS = Object.freeze(['hour', 'day', 'week']);
|
|
|
136
136
|
/** PRD item 28 (2026-09-06/09-09): `openai-api` — an OpenAI-shaped provider,
|
|
137
137
|
* constructed via `src/providers.js`'s factory. hamr's ruling (PRD 30.7):
|
|
138
138
|
* `deepseek-chat` is THE secondary provider (one, not a menu of half-tested
|
|
139
|
-
* models); the factory's tier table is where that lives, not this menu.
|
|
140
|
-
|
|
139
|
+
* models); the factory's tier table is where that lives, not this menu.
|
|
140
|
+
*
|
|
141
|
+
* PRD item 31.3 (hamr, 2026-09-09: *"anthropic, openai, gemini drop ollama for
|
|
142
|
+
* now"*): `gemini-api` joins. That is bareloop matching bare-agent's own
|
|
143
|
+
* adapter surface rather than curating a shorter list — bare-agent exports four
|
|
144
|
+
* real HTTP backends (Anthropic, OpenAI, Gemini, Ollama) plus CLIPipe and the
|
|
145
|
+
* FallbackProvider composite; `provider-http.js`, `provider-stop-reason.js`,
|
|
146
|
+
* `provider-temperature.js` and `provider-usage.js` are shared HELPERS, not
|
|
147
|
+
* backends, and were miscounted as providers once — the export list, never the
|
|
148
|
+
* filenames, is the source of truth.
|
|
149
|
+
*
|
|
150
|
+
* Ollama is deliberately NOT here. Beyond hamr's word, it is the one entry
|
|
151
|
+
* whose economics differ in kind: it takes no API key and bills nothing, so
|
|
152
|
+
* every round would price at $0 through machinery that treats $0 as a real
|
|
153
|
+
* price. An unpriced round reported as free is the exact honesty violation the
|
|
154
|
+
* `?? 0` rule exists to stop, and admitting it needs that question answered
|
|
155
|
+
* first, not a menu entry.
|
|
156
|
+
*
|
|
157
|
+
* `gemini-api` is ADMITTED-PENDING-PROBE: the probe rule stands (item 28
|
|
158
|
+
* ruling (d)) and gemini has zero runs. See `PROBE_STATUS` in
|
|
159
|
+
* `src/providers.js` — a spec may name it, and the runner says out loud that
|
|
160
|
+
* nothing has proven it. */
|
|
161
|
+
export const PROVIDERS = Object.freeze(['anthropic-api', 'openai-api', 'gemini-api', 'clipipe-subscription']);
|
|
141
162
|
/** V3 environment label: declared keys only — every field is a lineage-key
|
|
142
163
|
* candidate at N3. `provider` is part of the key by definition (top-level,
|
|
143
164
|
* not duplicated here). */
|
|
@@ -145,7 +166,10 @@ export const CONDITION_KEYS = Object.freeze(['providerPath', 'closeVerbosity', '
|
|
|
145
166
|
// `steps` stays in the field list ONLY so a retired spec reds by name
|
|
146
167
|
// (`shape-retired`) instead of falling through to a generic unknown-field —
|
|
147
168
|
// the operator gets told what happened, not just that something is wrong.
|
|
148
|
-
const JOB_FIELDS = ['schema', 'job', 'description', 'provider', 'baseUrl', 'conditions', 'cadence', 'budgetUsd', 'maxWallMs', 'closeTimeoutMs', 'model', 'writeScope', 'steps', 'escalation', 'goal', 'verdictType', 'close', 'closeDecl', 'checks', 'tools'];
|
|
169
|
+
const JOB_FIELDS = ['schema', 'job', 'description', 'provider', 'baseUrl', 'conditions', 'cadence', 'budgetUsd', 'maxWallMs', 'closeTimeoutMs', 'model', 'judge', 'writeScope', 'steps', 'escalation', 'goal', 'verdictType', 'close', 'closeDecl', 'checks', 'tools'];
|
|
170
|
+
/** the exact fields of a signed `judge` override — nested objects red unknown
|
|
171
|
+
* keys too, because no smuggling level exists in a signed spec */
|
|
172
|
+
const JUDGE_FIELDS = ['provider', 'model'];
|
|
149
173
|
/** the four-field plan shape's core (decision 5) — presence of any of these
|
|
150
174
|
* declares the shape; `tools` (the ceiling) rides the shape but alone does not
|
|
151
175
|
* declare it, so a legacy spec carrying it gets a pointed red, not a conflict */
|
|
@@ -271,6 +295,37 @@ export function validateJob(input, { shellCapUsd = 2 } = {}) {
|
|
|
271
295
|
red('invalid-value', 'model', 'non-empty string — exact provider model id (e.g. "claude-sonnet-5")');
|
|
272
296
|
}
|
|
273
297
|
|
|
298
|
+
// JUDGE — PRD item 32.1 (hamr, 2026-09-10: the judge "defaults to whatever
|
|
299
|
+
// llm is used and can be overriden"). OPTIONAL, and the OVERRIDE half: absent,
|
|
300
|
+
// the judge resolves to this job's own worker provider and model
|
|
301
|
+
// (`resolveJudge`, src/judged.js), which is what lets a DeepSeek or Gemini job
|
|
302
|
+
// judge without a second account. Present, it pins a judge deliberately
|
|
303
|
+
// different from the worker — an independent reader.
|
|
304
|
+
//
|
|
305
|
+
// It is signed like `model`/`baseUrl` above: `jobSpecHash` canonizes the whole
|
|
306
|
+
// resolved spec, so naming a judge is the OPERATOR signing off on who examines
|
|
307
|
+
// this work. The drafting agent cannot express it — naming your own examiner is
|
|
308
|
+
// arbiter territory, and the authoring schema has no such field.
|
|
309
|
+
//
|
|
310
|
+
// BOTH halves are required together, deliberately. A `judge` carrying only a
|
|
311
|
+
// provider would have to invent a model, and a judge identity nobody wrote is
|
|
312
|
+
// the state the calibration record exists to make impossible.
|
|
313
|
+
if (spec.judge !== undefined) {
|
|
314
|
+
if (!isObj(spec.judge)) {
|
|
315
|
+
red('invalid-value', 'judge', 'an object {provider, model} — the signed judge override; omit it to judge on this job\'s own worker model');
|
|
316
|
+
} else {
|
|
317
|
+
for (const key of Object.keys(spec.judge)) {
|
|
318
|
+
if (!JUDGE_FIELDS.includes(key)) red('unknown-field', `judge.${key}`, `declared keys only: ${JUDGE_FIELDS.join('|')}`);
|
|
319
|
+
}
|
|
320
|
+
if (!PROVIDERS.includes(spec.judge.provider)) {
|
|
321
|
+
red('invalid-value', 'judge.provider', `menu: ${PROVIDERS.join('|')} — the judge runs through the same provider factory the worker does, so an off-menu judge is a provider nothing can construct`);
|
|
322
|
+
}
|
|
323
|
+
if (!isNonEmptyString(spec.judge.model)) {
|
|
324
|
+
red('invalid-value', 'judge.model', 'non-empty string — exact provider model id; a judge with a provider and no model is an identity nobody signed');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
274
329
|
// BASE URL — PRD item 28, ruling (d), 2026-09-09: admitted in v1 of
|
|
275
330
|
// openai-api (an OpenAI-compatible gateway/endpoint), OPTIONAL and part of
|
|
276
331
|
// the signed hash like `model` above. This validator has no network
|
package/src/judged.js
CHANGED
|
@@ -65,32 +65,150 @@
|
|
|
65
65
|
|
|
66
66
|
import { createRequire } from 'node:module';
|
|
67
67
|
import { extractArtifact, priceOf, scrubRaw } from './text.js';
|
|
68
|
+
import { resolveProvider } from './providers.js';
|
|
68
69
|
|
|
69
70
|
const require = createRequire(import.meta.url);
|
|
70
71
|
|
|
71
72
|
/**
|
|
72
|
-
* THE JUDGE
|
|
73
|
-
* the
|
|
74
|
-
*
|
|
75
|
-
* worth exactly as much as that evidence. A judge-model bump forces a full
|
|
76
|
-
* recalibration — mandatory, per the model-bump dead-weight replay rule.
|
|
73
|
+
* THE JUDGE'S HISTORICAL PIN. Until PRD item 32 (2026-09-10) this constant WAS
|
|
74
|
+
* the judge: every grading path imported it, so bareloop could run a worker on
|
|
75
|
+
* any provider and still judge only on Claude.
|
|
77
76
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
77
|
+
* It is no longer read by any grading path. The judge identity is now RESOLVED
|
|
78
|
+
* per job (`resolveJudge` below) and travels as data — `ctx.judgeModel` into
|
|
79
|
+
* `runJudgedFloor`, an explicit argument into the calibration gate, and
|
|
80
|
+
* `closeDecl.calibration.judgeModel` into the signed spec, exactly as before.
|
|
81
|
+
* Keeping the export is deliberate: it is part of the public API
|
|
82
|
+
* (`src/index.js`) and an adopter importing it should not break on a minor.
|
|
83
|
+
* What it means now is "the judge bareloop pinned before item 32", which is
|
|
84
|
+
* still the right DEFAULT for an anthropic-api job and still the only tier with
|
|
85
|
+
* established injection resistance upstream (BA-20's `INJECTION_BATTERY` is
|
|
86
|
+
* haiku-4.5 only).
|
|
87
|
+
*
|
|
88
|
+
* THE ENFORCEMENT IS UNCHANGED, and still lives in three wired places because a
|
|
89
|
+
* rule with no detector is prose (F45) — only the value being compared moved
|
|
90
|
+
* from this constant to the resolved identity:
|
|
91
|
+
* 1. the identity is STORED with the set it graded — `foldJudgedArtifacts`
|
|
81
92
|
* writes it as `closeDecl.calibration.judgeModel`, inside the spec, so
|
|
82
|
-
* `jobSpecHash` covers it by construction.
|
|
93
|
+
* `jobSpecHash` covers it by construction. Change the judge and every
|
|
83
94
|
* signed judged spec hashes differently: the signature dies, and re-signing
|
|
84
95
|
* is what re-runs the calibration gate.
|
|
85
96
|
* 2. `validateCloseDecl` REQUIRES it on any stored set — a set nobody can
|
|
86
|
-
* attribute to a
|
|
87
|
-
* 3. `runJudgedFloor` refuses to grade when the stored
|
|
88
|
-
* naming both
|
|
89
|
-
* the new
|
|
90
|
-
* not on offer.
|
|
97
|
+
* attribute to a judge is a floor nobody can tell apart from a moved one.
|
|
98
|
+
* 3. `runJudgedFloor` refuses to grade when the stored identity is not the one
|
|
99
|
+
* about to grade, naming both. It does not degrade in either direction:
|
|
100
|
+
* grading on the new judge judges against a floor nobody calibrated, and
|
|
101
|
+
* the old judge is not on offer.
|
|
91
102
|
*/
|
|
92
103
|
export const JUDGE_MODEL = 'claude-haiku-4-5';
|
|
93
104
|
|
|
105
|
+
/**
|
|
106
|
+
* RESOLVE WHICH MODEL GRADES THIS JOB (PRD item 32.1).
|
|
107
|
+
*
|
|
108
|
+
* hamr, 2026-09-10: *"what i care about is that judge becomes llm agnostic and
|
|
109
|
+
* not set to one model or provider"* — and the shape he named: *"defaults to
|
|
110
|
+
* whatever llm is used and can be overriden"*.
|
|
111
|
+
*
|
|
112
|
+
* So, in precedence order:
|
|
113
|
+
* 1. `spec.judge` — an explicit, SIGNED `{provider, model}`. This is how a run
|
|
114
|
+
* pins a judge deliberately different from its worker (an independent
|
|
115
|
+
* reader, or a judge whose calibration a job wants to keep while the worker
|
|
116
|
+
* moves). It is a spec field the HUMAN signs, part of `jobSpecHash` like
|
|
117
|
+
* every other; the drafting agent cannot express it, because naming your
|
|
118
|
+
* own examiner is arbiter territory.
|
|
119
|
+
* 2. otherwise the job's OWN worker provider and model. A DeepSeek job judges
|
|
120
|
+
* on DeepSeek and needs no second account — which is the whole point: after
|
|
121
|
+
* item 31.4 a green job runs on its own key alone, and this is what lets a
|
|
122
|
+
* JUDGED one do the same.
|
|
123
|
+
*
|
|
124
|
+
* There is deliberately NO third fallback to `JUDGE_MODEL`. A caller that cannot
|
|
125
|
+
* say which model will grade has not wired a judge, and the grading path stops
|
|
126
|
+
* as a wiring gap rather than grading under an identity nobody chose — the same
|
|
127
|
+
* answer an absent `judgeLoop` already gets. A silent default here would be the
|
|
128
|
+
* exact silent-ignore class that `endpointKey` and F149 exist to prevent, except
|
|
129
|
+
* pointed at the arbiter.
|
|
130
|
+
*
|
|
131
|
+
* SELF-GRADING is licensed by measurement, not by this function: the judge
|
|
132
|
+
* renders no verdict (it locates facts; `decide()` is deterministic), and the
|
|
133
|
+
* calibration gate — 10 cases, a 10-of-10 floor, no partial credit, cases that
|
|
134
|
+
* MUST fail — is exactly the instrument that catches a judge which cannot see
|
|
135
|
+
* failures. If a worker-family judge cannot pass its own calibration, that is
|
|
136
|
+
* the finding, and the gate refuses to sign.
|
|
137
|
+
*
|
|
138
|
+
* @param {{specJudge?: any, workerProvider?: string, workerModel?: string}} o
|
|
139
|
+
* @returns {{provider: string, model: string}}
|
|
140
|
+
*/
|
|
141
|
+
export function resolveJudge({ specJudge, workerProvider, workerModel } = {}) {
|
|
142
|
+
const nonEmpty = (/** @type {any} */ v) => typeof v === 'string' && v.trim() !== '';
|
|
143
|
+
if (specJudge !== null && typeof specJudge === 'object'
|
|
144
|
+
&& nonEmpty(specJudge.provider) && nonEmpty(specJudge.model)) {
|
|
145
|
+
return { provider: specJudge.provider, model: specJudge.model };
|
|
146
|
+
}
|
|
147
|
+
if (specJudge !== undefined && specJudge !== null) {
|
|
148
|
+
// A malformed `judge` is never quietly ignored into the worker default: the
|
|
149
|
+
// spec VALIDATOR reds it (src/job.js), and reaching here with one means a
|
|
150
|
+
// caller skipped validation. Throwing beats grading under an identity the
|
|
151
|
+
// signer did not write.
|
|
152
|
+
throw new Error('[judged] spec.judge, when present, is {provider, model} with both non-empty — '
|
|
153
|
+
+ `got ${JSON.stringify(specJudge)}. A malformed judge is not a request to use the worker's.`);
|
|
154
|
+
}
|
|
155
|
+
if (!nonEmpty(workerProvider) || !nonEmpty(workerModel)) {
|
|
156
|
+
throw new Error('[judged] resolveJudge needs the job\'s worker provider and model to default the judge to them '
|
|
157
|
+
+ '(PRD item 32.1) — a caller that can name neither a judge nor a worker has not wired a judge, and there is '
|
|
158
|
+
+ 'no library-pinned fall-back any more.');
|
|
159
|
+
}
|
|
160
|
+
return { provider: /** @type {string} */ (workerProvider), model: /** @type {string} */ (workerModel) };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* `resolveJudge` needs a worker MODEL already resolved, and an authoring
|
|
165
|
+
* pipeline has to compute one identity at TWO different moments — once at
|
|
166
|
+
* compose time, to STAMP `closeDecl.calibration.judgeModel` into the spec it
|
|
167
|
+
* is about to sign, and again at the calibration gate that grades against
|
|
168
|
+
* that stamp — from a value that is not yet a validated spec (the operator's
|
|
169
|
+
* draft) the first time and is one (the assembled spec) the second. Two
|
|
170
|
+
* hand-spelled compositions of `resolveWorkerModel` + `resolveJudge` is
|
|
171
|
+
* exactly the kind of pair that drifts (PRD F45's "a rule with no detector is
|
|
172
|
+
* prose" applies to a rule spelled TWICE just as much as to one spelled
|
|
173
|
+
* nowhere) — this is the one spelling both calls make, and it mirrors the
|
|
174
|
+
* SAME composition `scripts/run-u.mjs` applies at launch time: the named
|
|
175
|
+
* provider's own `tiers.sonnet` defaults an absent model, and a signed
|
|
176
|
+
* `judge` override wins over the worker default.
|
|
177
|
+
*
|
|
178
|
+
* `source` is read for `provider`/`model`/`judge` only, so a draft and the
|
|
179
|
+
* spec `assembleSpec` folds it into resolve identically — `assembleSpec`
|
|
180
|
+
* spreads the draft into the spec untouched on those three fields — and any
|
|
181
|
+
* draft that goes on to validate and sign therefore stamps and grades under
|
|
182
|
+
* the exact same identity `scripts/run-u.mjs` will resolve at run time.
|
|
183
|
+
*
|
|
184
|
+
* `source.provider` absent, or unresolvable, or `source.judge` malformed all
|
|
185
|
+
* THROW here exactly as `resolveProvider`/`resolveJudge` already do on their
|
|
186
|
+
* own — a caller composing this from a pre-validation draft is expected to
|
|
187
|
+
* catch that and use a placeholder, because a draft that cannot resolve here
|
|
188
|
+
* is a draft `validateJob` reds a few lines later regardless, before anything
|
|
189
|
+
* gets signed under the placeholder.
|
|
190
|
+
*
|
|
191
|
+
* `resolveWorkerModelFn` is taken as a PARAMETER rather than imported — this
|
|
192
|
+
* module cannot import `./job.js` directly: `job.js` imports
|
|
193
|
+
* `./declaredclose.js`, which imports `resolveJudge` from THIS module, and a
|
|
194
|
+
* `judged.js` → `job.js` edge would close that into a cycle. `resolveProvider`
|
|
195
|
+
* has no such conflict (`./providers.js` imports nothing local) and is
|
|
196
|
+
* imported directly.
|
|
197
|
+
*
|
|
198
|
+
* @param {{provider?: string, model?: string, judge?: any}} source draft or spec
|
|
199
|
+
* @param {string} fallbackProviderName used only when `source.provider` is absent
|
|
200
|
+
* @param {(o: {specModel?: string, flagModel?: string, defaultModel: string}) => {model: string, source: string}} resolveWorkerModelFn `resolveWorkerModel` from `./job.js`
|
|
201
|
+
* @returns {{provider: string, model: string}}
|
|
202
|
+
*/
|
|
203
|
+
export function resolveJobJudge(source, fallbackProviderName, resolveWorkerModelFn) {
|
|
204
|
+
const providerName = source?.provider ?? fallbackProviderName;
|
|
205
|
+
const providerEntry = resolveProvider(providerName);
|
|
206
|
+
const { model } = resolveWorkerModelFn({
|
|
207
|
+
specModel: source?.model, flagModel: undefined, defaultModel: providerEntry.tiers.sonnet,
|
|
208
|
+
});
|
|
209
|
+
return resolveJudge({ specJudge: source?.judge, workerProvider: providerName, workerModel: model });
|
|
210
|
+
}
|
|
211
|
+
|
|
94
212
|
/**
|
|
95
213
|
* The locate call's token ceiling. The POC's two real artifacts came back well
|
|
96
214
|
* inside 2000; this doubles that for headroom on a longer artifact, and the
|
package/src/kinds.js
CHANGED
|
@@ -66,7 +66,7 @@ import { tmpdir } from 'node:os';
|
|
|
66
66
|
import { join, sep } from 'node:path';
|
|
67
67
|
import { globToPrefix, isNonEmptyString } from './validate.js';
|
|
68
68
|
import { CLOSE_ENV_DENY } from './ralph.js';
|
|
69
|
-
import { runLocate, decide, validateCard, LOCATE_AXES, LOCATE_LABEL
|
|
69
|
+
import { runLocate, decide, validateCard, LOCATE_AXES, LOCATE_LABEL } from './judged.js';
|
|
70
70
|
|
|
71
71
|
/** the close's two judgment exits — a stage's verdict, in the shape the
|
|
72
72
|
* hand-written closes already spoke (exit code is truth) */
|
|
@@ -173,6 +173,46 @@ export const MAX_JUDGED_PATHS = 8;
|
|
|
173
173
|
* decision on it, and five literals is how they come to disagree. */
|
|
174
174
|
export const JUDGED_FLOOR_KIND = 'judged-floor';
|
|
175
175
|
export const SEED_EXEMPT_KINDS = Object.freeze([JUDGED_FLOOR_KIND, 'human-confirms']);
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The judged stages of a close declaration, and whether it has any — the ONE
|
|
179
|
+
* spelling of "does this close ask a model to render anything?".
|
|
180
|
+
*
|
|
181
|
+
* The comment above `JUDGED_FLOOR_KIND` says five literals is how the readers
|
|
182
|
+
* come to disagree, and by PRD item 31.4 there were SIX open-coded
|
|
183
|
+
* `stages.filter(s => s.kind === JUDGED_FLOOR_KIND)` across the tree
|
|
184
|
+
* (`src/authorjob.js` twice, `src/cardauthor.js`, `src/declaredclose.js`,
|
|
185
|
+
* `scripts/run-author.mjs`, `scripts/author-readout.mjs`). 31.4 needed a
|
|
186
|
+
* seventh, in the runner, to decide whether a judge KEY is demanded — and a
|
|
187
|
+
* seventh spelling deciding a REFUSAL is the drift `scripts/run-u.mjs`'s own
|
|
188
|
+
* judge-provider comment warns about ("a second reading of the declaration
|
|
189
|
+
* that can drift from the one the runner actually executes"). So there is one
|
|
190
|
+
* reading now, and every caller shares it.
|
|
191
|
+
*
|
|
192
|
+
* Defensive on shape ON PURPOSE: callers pass raw specs read off disk and
|
|
193
|
+
* half-built drafts, not validated declarations. An absent/!object `closeDecl`
|
|
194
|
+
* or `stages` is "no judged stages", never a throw — the close VALIDATOR is
|
|
195
|
+
* what rejects a malformed declaration, and this predicate answering a
|
|
196
|
+
* question about one is not the place to re-litigate it.
|
|
197
|
+
* @param {any} closeDecl a close declaration (or anything, including undefined)
|
|
198
|
+
* @returns {any[]} the judged-floor stages, in declaration order
|
|
199
|
+
*/
|
|
200
|
+
export function judgedStages(closeDecl) {
|
|
201
|
+
const stages = closeDecl && typeof closeDecl === 'object' ? closeDecl.stages : undefined;
|
|
202
|
+
if (!Array.isArray(stages)) return [];
|
|
203
|
+
return stages.filter((s) => s !== null && typeof s === 'object' && s.kind === JUDGED_FLOOR_KIND);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Does this close declaration judge — i.e. will a judge model be CALLED?
|
|
208
|
+
* The question `scripts/run-u.mjs` asks before demanding a judge key (PRD item
|
|
209
|
+
* 31.4): a job that never judges must run on its own provider's key alone.
|
|
210
|
+
* @param {any} closeDecl
|
|
211
|
+
* @returns {boolean}
|
|
212
|
+
*/
|
|
213
|
+
export function closeJudges(closeDecl) {
|
|
214
|
+
return judgedStages(closeDecl).length > 0;
|
|
215
|
+
}
|
|
176
216
|
/** the one kind a PERSON renders — named once, because the runner keys three
|
|
177
217
|
* decisions on it (is there a door to answer, which stage paused, whose ruling
|
|
178
218
|
* this is) and three literals is how they come to disagree */
|
|
@@ -258,6 +298,7 @@ export function mechanicalStages(stages) {
|
|
|
258
298
|
* gapCap?: number, maxBuffer?: number, seedTrees?: SeedTrees,
|
|
259
299
|
* humanRuling?: {decision: string, text?: string|null}|null,
|
|
260
300
|
* judgeLoop?: (o: {system: string}) => any,
|
|
301
|
+
* judgeModel?: string,
|
|
261
302
|
* onJudgeCost?: (c: {stage: string|null, kind: string|null, path: string, attempt: number,
|
|
262
303
|
* label: string, model: string, costUsd: number|null, unpricedRounds: number}) => void,
|
|
263
304
|
* callBounds?: () => {timeoutMs?: number, deadlineMs?: number},
|
|
@@ -268,10 +309,16 @@ export function mechanicalStages(stages) {
|
|
|
268
309
|
* runner — never authored, never defaulted, and absent on every ordinary run.
|
|
269
310
|
* `judgeLoop` is the PAID seam the judged stage runs its locate call through, and
|
|
270
311
|
* it is operator territory for the same reason the worker's provider is: this
|
|
271
|
-
* module owns no provider and never picks one
|
|
272
|
-
*
|
|
273
|
-
* model — the stage stops as a wiring gap, the same answer `runPlan` gives a
|
|
312
|
+
* module owns no provider and never picks one. Absent is not a fall-back to some
|
|
313
|
+
* other model — the stage stops as a wiring gap, the same answer `runPlan` gives a
|
|
274
314
|
* native job with no native factory.
|
|
315
|
+
* `judgeModel` is the seam's other half (PRD item 32.1): WHICH model that loop
|
|
316
|
+
* drives, resolved by the runner (`resolveJudge`, src/judged.js — the spec's signed
|
|
317
|
+
* `judge` override, else the job's own worker model) rather than pinned in this
|
|
318
|
+
* library. It is required whenever `judgeLoop` is, and for the same reason: a
|
|
319
|
+
* verdict whose grader cannot be named is a floor nobody can attribute, and the
|
|
320
|
+
* signed calibration record has nothing to compare against. The two travel
|
|
321
|
+
* together or the stage stops.
|
|
275
322
|
* `onJudgeCost` is the METER, and it is the only reason a judged stage can be
|
|
276
323
|
* funded honestly: this executor cannot reach the run's ledger, so every locate
|
|
277
324
|
* call's cost is reported OUT through it, once per call, on every route including
|
|
@@ -1736,16 +1783,34 @@ async function runJudgedFloor(stage, ctx) {
|
|
|
1736
1783
|
// model on it (`validateCloseDecl`), so the runtime question is only ever WHICH
|
|
1737
1784
|
// model — and a stage descriptor handed straight to `runStage` by an adopter or a
|
|
1738
1785
|
// test is not a signed spec making a claim about a tier.
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1786
|
+
//
|
|
1787
|
+
// PRD item 32.1: WHICH model grades is no longer this module's constant — it is
|
|
1788
|
+
// `ctx.judgeModel`, the identity the run resolved and wired (`resolveJudge`,
|
|
1789
|
+
// src/judged.js: the spec's signed `judge` override, else the job's own worker
|
|
1790
|
+
// model). The IDENTITY is checked before the seam and before the stamp, because
|
|
1791
|
+
// an unwired identity makes both of the messages below unwritable: neither
|
|
1792
|
+
// "would grade with X" nor "the judge is X" can be said by a stage that cannot
|
|
1793
|
+
// name X. There is deliberately NO fall-back to `JUDGE_MODEL` here — a run that
|
|
1794
|
+
// cannot say which model grades has not wired a judge, and grading under an
|
|
1795
|
+
// identity nobody chose is exactly the silent-ignore class this repo refuses
|
|
1796
|
+
// everywhere else.
|
|
1797
|
+
if (!isNonEmptyString(ctx.judgeModel)) {
|
|
1798
|
+
return stopped(stage, ctx, `INSTRUMENT: stage "${stage.name}" is a judged stage and this run wired no judge IDENTITY `
|
|
1799
|
+
+ '(ctx.judgeModel) — an adopter wiring gap, never a silent fall-back to a library pin. A verdict whose grader '
|
|
1800
|
+
+ 'cannot be named is a floor nobody can attribute, and the calibration record has nothing to compare against',
|
|
1801
|
+
{}, STOP_FAULTS.FAILED);
|
|
1802
|
+
}
|
|
1803
|
+
const judgeModel = /** @type {string} */ (ctx.judgeModel);
|
|
1804
|
+
if (isNonEmptyString(stage.calibrationJudgeModel) && stage.calibrationJudgeModel !== judgeModel) {
|
|
1805
|
+
return stopped(stage, ctx, `INSTRUMENT: stage "${stage.name}" would grade with ${judgeModel}, and the calibration `
|
|
1806
|
+
+ `set signed with this close was graded by ${stage.calibrationJudgeModel} — the judge moved under the `
|
|
1742
1807
|
+ 'signature. A floor is worth exactly the judge that certified it, so this stops rather than grading against a '
|
|
1743
|
-
+ 'floor nobody calibrated: re-sign the spec with the new
|
|
1744
|
-
{ signedJudgeModel: stage.calibrationJudgeModel, judgeModel
|
|
1808
|
+
+ 'floor nobody calibrated: re-sign the spec with the new judge and re-run the calibration gate (recalibrate)',
|
|
1809
|
+
{ signedJudgeModel: stage.calibrationJudgeModel, judgeModel }, STOP_FAULTS.FAILED);
|
|
1745
1810
|
}
|
|
1746
1811
|
if (typeof ctx.judgeLoop !== 'function') {
|
|
1747
1812
|
return stopped(stage, ctx, `INSTRUMENT: stage "${stage.name}" is a judged stage and this run wired no judge seam `
|
|
1748
|
-
+ `(ctx.judgeLoop) — an adopter wiring gap, never a silent fall-back: the judge is ${
|
|
1813
|
+
+ `(ctx.judgeLoop) — an adopter wiring gap, never a silent fall-back: the judge is ${judgeModel} and a `
|
|
1749
1814
|
+ 'stage that quietly graded on whatever model was lying around would be a floor nobody calibrated',
|
|
1750
1815
|
{}, STOP_FAULTS.FAILED);
|
|
1751
1816
|
}
|
|
@@ -1797,7 +1862,7 @@ async function runJudgedFloor(stage, ctx) {
|
|
|
1797
1862
|
if (typeof ctx.onJudgeCost !== 'function') return;
|
|
1798
1863
|
ctx.onJudgeCost({
|
|
1799
1864
|
stage: stage?.name ?? null, kind: stage?.kind ?? null, path: rel, attempt,
|
|
1800
|
-
label: LOCATE_LABEL, model:
|
|
1865
|
+
label: LOCATE_LABEL, model: judgeModel, costUsd: c.costUsd, unpricedRounds: c.unpricedRounds,
|
|
1801
1866
|
});
|
|
1802
1867
|
},
|
|
1803
1868
|
});
|
|
@@ -1848,7 +1913,7 @@ async function runJudgedFloor(stage, ctx) {
|
|
|
1848
1913
|
exitCode: red ? EXIT_RED : EXIT_GREEN,
|
|
1849
1914
|
gapLines: gap.render(),
|
|
1850
1915
|
detail: {
|
|
1851
|
-
model:
|
|
1916
|
+
model: judgeModel,
|
|
1852
1917
|
card: p.card.items.map((/** @type {any} */ it) => it.rule),
|
|
1853
1918
|
paths,
|
|
1854
1919
|
perPath,
|
package/src/planrun.js
CHANGED
|
@@ -714,8 +714,15 @@ ${scoutBlob || '(no scout notes)'}`;
|
|
|
714
714
|
* `false` → the drafter has no tools, so a native session would report NO cost — return a
|
|
715
715
|
* metered claude-json TEXT provider (`--output-format json`, `parse:'claude-json'`) instead,
|
|
716
716
|
* so its spend is never invisible. The Loop path (`anthropic-api`) never touches this.
|
|
717
|
+
* @param {string|null} [opts.judgeModel] softgreen — WHICH model that judge provider drives
|
|
718
|
+
* (PRD item 32.1). Resolved by the CALLER (`resolveJudge`, src/judged.js: the spec's signed
|
|
719
|
+
* `judge` override, else the job's own worker model), never pinned in this library. It
|
|
720
|
+
* travels with `judgeProvider` or the judged stage stops as a wiring gap: a verdict whose
|
|
721
|
+
* grader cannot be named is a floor nobody can attribute.
|
|
717
722
|
* @param {any} [opts.judgeProvider] softgreen — the provider a JUDGED close stage runs its
|
|
718
|
-
* locate call through,
|
|
723
|
+
* locate call through, built by the CALLER from the RESOLVED judge identity (PRD item
|
|
724
|
+
* 32.1: `resolveJudge`, src/judged.js — the spec's signed `judge` override, else the job's
|
|
725
|
+
* own worker provider and model), never pinned in this library. Separate
|
|
719
726
|
* from `provider`/`providerFor` on purpose: the judge tier is not a step knob and never
|
|
720
727
|
* agent-selectable, and the worker's binding is not a legal stand-in for it. Absent is not
|
|
721
728
|
* a fall-back — a judged stage with no seam instrument-STOPS as a wiring gap, the same
|
|
@@ -928,7 +935,7 @@ ${scoutBlob || '(no scout notes)'}`;
|
|
|
928
935
|
* 'branch-red' | 'cap-halt' | 'wall-halt' | 'provider-red' | 'interpreter-red' |
|
|
929
936
|
* 'step-stalled' | 'hitl-pause' | 'hitl-decision-red' | `step-red:<id>`
|
|
930
937
|
*/
|
|
931
|
-
export async function runPlan(job, { workdir, provider, nativeProvider, providerFor, judgeProvider = null, emit, remainingUsd, isUnpriced = () => false, spendComplete = () => true, capRuns = 3, strikeLimit = STRIKE_LIMIT, closeTimeoutMs, closeDir = null, maxStepRounds = 40, layerRoot = false, readShim = false, scout = true, scoutRounds = SCOUT_ROUNDS, bridge = null, now, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null, heldRuling = null, priorSpentUsd = 0, reviewDoor = null, doorRerun = null, resumable = true }) {
|
|
938
|
+
export async function runPlan(job, { workdir, provider, nativeProvider, providerFor, judgeProvider = null, judgeModel = null, emit, remainingUsd, isUnpriced = () => false, spendComplete = () => true, capRuns = 3, strikeLimit = STRIKE_LIMIT, closeTimeoutMs, closeDir = null, maxStepRounds = 40, layerRoot = false, readShim = false, scout = true, scoutRounds = SCOUT_ROUNDS, bridge = null, now, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null, heldRuling = null, priorSpentUsd = 0, reviewDoor = null, doorRerun = null, resumable = true }) {
|
|
932
939
|
// MEMORY-CACHE: what the read shim (src/readshim.js) saved THIS run, summed across
|
|
933
940
|
// every mkWorker's own shim instance (scout, drafter, each step's worker, the fix
|
|
934
941
|
// worker) — one accumulator closed over by all of them, because the shim's ledger
|
|
@@ -1633,6 +1640,11 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1633
1640
|
// an attempt's (a budget funds the attempt PLUS its close), and a null cost trips
|
|
1634
1641
|
// the same F6 pricing halt rather than being laundered into $0.
|
|
1635
1642
|
judgeLoop: judgeProvider ? (/** @type {{system: string}} */ o) => defaultJudgeLoop({ provider: judgeProvider, system: o.system }) : null,
|
|
1643
|
+
// the seam's other half (PRD item 32.1) — WHICH model that loop drives. Passed
|
|
1644
|
+
// through untouched: this module resolves no judge, exactly as it constructs no
|
|
1645
|
+
// provider. `runJudgedFloor` stops as a wiring gap when it is absent rather than
|
|
1646
|
+
// falling back to a library pin, so a null here is a stop and never a default.
|
|
1647
|
+
...(judgeModel === null ? {} : { judgeModel }),
|
|
1636
1648
|
// F152/PRD 30.4 — the judge's locate call carried NO time bound at all: a
|
|
1637
1649
|
// live endpoint that accepts and never answers hung the process forever
|
|
1638
1650
|
// (measured), because an open socket is an active handle and no backstop
|