bareloop 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +247 -0
- package/README.md +1 -1
- package/bareloop.context.md +113 -29
- package/package.json +2 -2
- package/src/authorflow.js +24 -9
- package/src/authoring.js +32 -0
- package/src/authorjob.js +108 -36
- package/src/authorscout.js +83 -9
- package/src/calibrate.js +43 -12
- package/src/cardauthor.js +23 -16
- package/src/cli.js +73 -21
- package/src/declaredclose.js +25 -11
- package/src/index.js +8 -5
- package/src/job.js +92 -2
- package/src/judged.js +240 -19
- package/src/kinds.js +89 -12
- package/src/planrun.js +194 -41
- package/src/providers.js +301 -0
- package/src/ratelimit.js +116 -0
- package/src/reuse.js +9 -1
- package/src/run.js +68 -9
- package/src/transport.js +29 -12
- package/src/validate.js +4 -0
- package/types/authoring.d.ts +30 -0
- package/types/authorjob.d.ts +10 -6
- package/types/authorscout.d.ts +27 -2
- 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 +8 -1
- package/types/index.d.ts +1 -1
- package/types/job.d.ts +25 -0
- package/types/judged.d.ts +130 -16
- package/types/kinds.d.ts +51 -3
- package/types/planrun.d.ts +10 -2
- package/types/providers.d.ts +166 -0
- package/types/ratelimit.d.ts +59 -0
- package/types/run.d.ts +17 -3
- package/types/transport.d.ts +10 -4
package/src/calibrate.js
CHANGED
|
@@ -46,7 +46,9 @@
|
|
|
46
46
|
// calibration becomes a refusal, exactly as D9's other three gates work.
|
|
47
47
|
// * IT PICKS NO NUMBER. The size is `CALIBRATION_SIZE` (hamr's), the floor is
|
|
48
48
|
// all-of-them (hamr's), the ladder is `JUDGE_ATTEMPTS` (operator-confirmed),
|
|
49
|
-
// and the model is
|
|
49
|
+
// and the model is the run's RESOLVED judge identity, handed in (PRD item
|
|
50
|
+
// 32.1) rather than pinned in this library. Nothing here invents a threshold
|
|
51
|
+
// and nothing here picks a judge.
|
|
50
52
|
// * IT OWNS NO PROVIDER. The judge seam arrives as `judgeLoop`; an absent one
|
|
51
53
|
// is a WIRING GAP that stops the gate, never a silent skip — a judged close
|
|
52
54
|
// signed off a calibration nobody ran is the exact state this gate exists to
|
|
@@ -58,11 +60,12 @@
|
|
|
58
60
|
|
|
59
61
|
import { createHash } from 'node:crypto';
|
|
60
62
|
import {
|
|
61
|
-
|
|
63
|
+
LOCATE_LABEL, LOCATE_AXES, CALIBRATION_SIZE,
|
|
62
64
|
runLocate, decide, expectedOf, validateCard, validateCalibrationSet,
|
|
63
65
|
} from './judged.js';
|
|
64
66
|
import { JUDGE_ATTEMPTS } from './kinds.js';
|
|
65
67
|
import { tallyCalls } from './text.js';
|
|
68
|
+
import { PROVIDER_TIMEOUT_MS } from './clock.js';
|
|
66
69
|
|
|
67
70
|
/** @typedef {{code: string, path: string, detail: string, [k: string]: any}} Red */
|
|
68
71
|
|
|
@@ -308,11 +311,11 @@ export function factsResist(facts, gold, artifactText) {
|
|
|
308
311
|
* when the model is malforming, which is the worst moment for a paid ladder to be
|
|
309
312
|
* unbounded. A stop comes back as its own field rather than as a `red`: a ceiling
|
|
310
313
|
* is the OPERATOR's governance, never the transport's casualty.
|
|
311
|
-
* @param {{artifactText: string, card: any, judgeLoop: Function, attempts: number,
|
|
314
|
+
* @param {{artifactText: string, card: any, judgeLoop: Function, judgeModel: string, attempts: number,
|
|
312
315
|
* meter: (c: any) => void, id: string, kind: string,
|
|
313
316
|
* capStop: () => 'cap-halt'|'pricing-red'|null}} o
|
|
314
317
|
*/
|
|
315
|
-
async function pipeOnce({ artifactText, card, judgeLoop, attempts, meter, id, kind, capStop }) {
|
|
318
|
+
async function pipeOnce({ artifactText, card, judgeLoop, judgeModel, attempts, meter, id, kind, capStop }) {
|
|
316
319
|
/** @type {any} */
|
|
317
320
|
let last = null;
|
|
318
321
|
/** @type {any[]} */
|
|
@@ -330,8 +333,17 @@ async function pipeOnce({ artifactText, card, judgeLoop, attempts, meter, id, ki
|
|
|
330
333
|
card,
|
|
331
334
|
loopFactory: /** @type {any} */ (judgeLoop),
|
|
332
335
|
attempt,
|
|
336
|
+
// F152/PRD 30.4 — this module's own header already names the hazard ("a
|
|
337
|
+
// dead socket") as one of its three CASUALTY readings, but with no bound
|
|
338
|
+
// at all a truly silent judge endpoint never reaches that reading — it
|
|
339
|
+
// hangs the authoring process forever instead (measured). The gate runs
|
|
340
|
+
// at draft/sign time, which has no wall clock to derive a bound from (the
|
|
341
|
+
// authoring pipeline's only ceiling is money, `ceilingUsd`/`capStop`), so
|
|
342
|
+
// this reuses the same idle-timeout default the run path falls back to
|
|
343
|
+
// when ITS OWN wall is unbounded (`PROVIDER_TIMEOUT_MS`, src/clock.js).
|
|
344
|
+
callBounds: { timeoutMs: PROVIDER_TIMEOUT_MS },
|
|
333
345
|
onCost: (c) => meter({
|
|
334
|
-
id, kind, attempt, label: CALIBRATION_LABEL, model:
|
|
346
|
+
id, kind, attempt, label: CALIBRATION_LABEL, model: judgeModel,
|
|
335
347
|
costUsd: c.costUsd, unpricedRounds: c.unpricedRounds,
|
|
336
348
|
}),
|
|
337
349
|
});
|
|
@@ -371,7 +383,13 @@ async function pipeOnce({ artifactText, card, judgeLoop, attempts, meter, id, ki
|
|
|
371
383
|
* graded one: `ok` is false, the cases nobody bought are absent rather than recorded
|
|
372
384
|
* as failures, and `casualty` stays null — the transport was fine, the operator's own
|
|
373
385
|
* governance stopped the gate.
|
|
374
|
-
*
|
|
386
|
+
* `judgeModel` is REQUIRED and has no default (PRD item 32.1). It is the identity
|
|
387
|
+
* the caller resolved (`resolveJudge`, src/judged.js) and it is what the graded set
|
|
388
|
+
* is STAMPED with — `setHash` folds it in, so a set certified by one judge cannot be
|
|
389
|
+
* mistaken for the same set certified by another. A default here would be the silent
|
|
390
|
+
* fall-back the whole judge-identity change exists to remove: a gate that grades
|
|
391
|
+
* under a model nobody named certifies a floor nobody can attribute.
|
|
392
|
+
* @param {{cases: any, card: any, judgeLoop: Function|null, judgeModel: string,
|
|
375
393
|
* onCost?: (c: any) => void, attempts?: number, battery?: any[], batteryCard?: any,
|
|
376
394
|
* capStop?: () => 'cap-halt'|'pricing-red'|null}} o
|
|
377
395
|
* @returns {Promise<{ok: boolean, stop: string|null, casualty: any,
|
|
@@ -382,12 +400,25 @@ async function pipeOnce({ artifactText, card, judgeLoop, attempts, meter, id, ki
|
|
|
382
400
|
* costUsd: number|null, knownUsd: number, spendComplete: boolean, calls: any[]}>}
|
|
383
401
|
*/
|
|
384
402
|
export async function runCalibration({
|
|
385
|
-
cases, card, judgeLoop, onCost = () => {},
|
|
403
|
+
cases, card, judgeLoop, judgeModel, onCost = () => {},
|
|
386
404
|
attempts = JUDGE_ATTEMPTS,
|
|
387
405
|
battery = /** @type {any[]} */ (/** @type {unknown} */ (INJECTION_LOCATE_BATTERY)),
|
|
388
406
|
batteryCard = INJECTION_CARD,
|
|
389
407
|
capStop = () => null,
|
|
390
408
|
}) {
|
|
409
|
+
// The mandatory-identity throw guards a REAL judge seam graded under nobody's
|
|
410
|
+
// name — it does not apply when there is no seam at all. A caller wiring no
|
|
411
|
+
// `judgeLoop` (an adopter who has not connected a judge yet) legitimately hands
|
|
412
|
+
// in no `judgeModel` either, and that case is the WIRING-GAP `no-judge` stop
|
|
413
|
+
// below, returned as a red rather than thrown. Throwing here ahead of that
|
|
414
|
+
// check would make the wiring-gap stop unreachable through its own documented
|
|
415
|
+
// path — `prepareSigning` (src/authorjob.js) draws exactly this same line with
|
|
416
|
+
// its own `judgeLoop !== null` guard, and this function has to agree with it.
|
|
417
|
+
if (typeof judgeLoop === 'function' && (typeof judgeModel !== 'string' || judgeModel.trim() === '')) {
|
|
418
|
+
throw new Error('[calibrate] runCalibration needs the resolved judgeModel (PRD item 32.1): the graded set is '
|
|
419
|
+
+ 'STAMPED with the judge that certified it, and a set nobody can attribute is a floor nobody can tell apart '
|
|
420
|
+
+ 'from one graded by a different model. There is no library pin to fall back to.');
|
|
421
|
+
}
|
|
391
422
|
/** @type {any[]} */
|
|
392
423
|
const calls = [];
|
|
393
424
|
/** @param {any} c */
|
|
@@ -403,7 +434,7 @@ export async function runCalibration({
|
|
|
403
434
|
return /** @type {any} */ ({
|
|
404
435
|
ok: false, stop: null, casualty: null, graded: [], failures: [],
|
|
405
436
|
injection: { styles: [], allResisted: false, leaks: [] },
|
|
406
|
-
reds: [], judgeModel
|
|
437
|
+
reds: [], judgeModel,
|
|
407
438
|
cardHash: null, casesHash: null, setHash: null,
|
|
408
439
|
costUsd: t.costUsd, knownUsd: t.knownUsd, spendComplete: t.spendComplete, calls: calls.map((c) => ({ ...c })),
|
|
409
440
|
...extra,
|
|
@@ -454,7 +485,7 @@ export async function runCalibration({
|
|
|
454
485
|
code: 'no-judge-seam',
|
|
455
486
|
path: 'judgeLoop',
|
|
456
487
|
detail: `the calibration gate runs ${CALIBRATION_SIZE} real locate calls plus ${battery.length} injection `
|
|
457
|
-
+ `artifacts through ${
|
|
488
|
+
+ `artifacts through ${judgeModel}, and no judge seam was wired. An absent seam is an adopter wiring gap and `
|
|
458
489
|
+ 'never a fall-back: a gate that grades nothing would certify every close it was asked about',
|
|
459
490
|
}],
|
|
460
491
|
});
|
|
@@ -462,7 +493,7 @@ export async function runCalibration({
|
|
|
462
493
|
|
|
463
494
|
const cardHash = artifactHash(card);
|
|
464
495
|
const casesHash = artifactHash(cases);
|
|
465
|
-
const setHash = artifactHash({ card, cases, judgeModel
|
|
496
|
+
const setHash = artifactHash({ card, cases, judgeModel });
|
|
466
497
|
const stamped = { cardHash, casesHash, setHash };
|
|
467
498
|
|
|
468
499
|
// ── the signed ten ────────────────────────────────────────────────────────
|
|
@@ -470,7 +501,7 @@ export async function runCalibration({
|
|
|
470
501
|
const graded = [];
|
|
471
502
|
for (const c of cases) {
|
|
472
503
|
const r = await pipeOnce({
|
|
473
|
-
artifactText: String(c.artifact), card, judgeLoop: /** @type {Function} */ (judgeLoop),
|
|
504
|
+
artifactText: String(c.artifact), card, judgeLoop: /** @type {Function} */ (judgeLoop), judgeModel,
|
|
474
505
|
attempts, meter, id: String(c.id), kind: 'case', capStop,
|
|
475
506
|
});
|
|
476
507
|
// the ceiling, ahead of the casualty branch: a call nobody funded is not a call
|
|
@@ -517,7 +548,7 @@ export async function runCalibration({
|
|
|
517
548
|
const styles = [];
|
|
518
549
|
for (const b of battery) {
|
|
519
550
|
const r = await pipeOnce({
|
|
520
|
-
artifactText: b.artifact, card: batteryCard, judgeLoop: /** @type {Function} */ (judgeLoop),
|
|
551
|
+
artifactText: b.artifact, card: batteryCard, judgeLoop: /** @type {Function} */ (judgeLoop), judgeModel,
|
|
521
552
|
attempts, meter, id: b.id, kind: 'injection', capStop,
|
|
522
553
|
});
|
|
523
554
|
if (r.budget) {
|
package/src/cardauthor.js
CHANGED
|
@@ -60,10 +60,10 @@
|
|
|
60
60
|
// pipe produces are one shape read twice (module 5 compares them mechanically).
|
|
61
61
|
|
|
62
62
|
import {
|
|
63
|
-
JUDGE_RULE_IDS, JUDGE_RULES, CALIBRATION_SIZE, CASE_VERDICTS, validateJudgedArtifacts,
|
|
63
|
+
JUDGE_RULE_IDS, JUDGE_RULES, CALIBRATION_SIZE, CASE_VERDICTS, validateJudgedArtifacts,
|
|
64
64
|
} from './judged.js';
|
|
65
65
|
import { askStructured, makeCostBook } from './authorflow.js';
|
|
66
|
-
import {
|
|
66
|
+
import { judgedStages } from './kinds.js';
|
|
67
67
|
import { isObj, redactSecrets } from './validate.js';
|
|
68
68
|
|
|
69
69
|
/** @typedef {{code: string, path: string, detail: string, [k: string]: any}} Red */
|
|
@@ -363,15 +363,16 @@ export function signJudgedArtifacts({ proposal, fix = null }) {
|
|
|
363
363
|
* - THE CASES land beside the stages as `calibration.cases` — a close-level
|
|
364
364
|
* artifact, because the set calibrates the RULER and not one stage's params,
|
|
365
365
|
* and because module 5's gate reads it before any stage runs.
|
|
366
|
-
* - THE JUDGE MODEL lands beside them as `calibration.judgeModel
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
* the set has to carry which
|
|
371
|
-
* signed floor from a
|
|
372
|
-
* a
|
|
373
|
-
* re-sign is a re-run of the calibration gate.
|
|
374
|
-
*
|
|
366
|
+
* - THE JUDGE MODEL lands beside them as `calibration.judgeModel`. Since PRD item
|
|
367
|
+
* 32.1 that is the RESOLVED judge identity the caller hands in (`resolveJudge`,
|
|
368
|
+
* src/judged.js — the spec's signed `judge` override, else the job's own worker
|
|
369
|
+
* model), not a library pin. A calibration set is only worth the judge that
|
|
370
|
+
* graded it, so the set has to carry which judge certified it or nothing
|
|
371
|
+
* downstream can tell a signed floor from a moved one. Storing it HERE, inside
|
|
372
|
+
* the spec, is what makes a judge change flip `jobSpecHash`: the signature dies,
|
|
373
|
+
* the signer re-signs, and a re-sign is a re-run of the calibration gate. It is
|
|
374
|
+
* REQUIRED, with no default — a set stamped with a model nobody named is exactly
|
|
375
|
+
* the unattributable floor this field exists to prevent (F45).
|
|
375
376
|
*
|
|
376
377
|
* A card with NOWHERE TO LAND THROWS. It is a caller's own broken input — a
|
|
377
378
|
* signed rubric folded into a close with no judged ruler — and there is no safe
|
|
@@ -380,20 +381,26 @@ export function signJudgedArtifacts({ proposal, fix = null }) {
|
|
|
380
381
|
* The input is never mutated: a closeDecl is a signed artefact and rewriting a
|
|
381
382
|
* caller's copy in place is how two readers come to disagree about what was
|
|
382
383
|
* signed.
|
|
383
|
-
* @param {any} closeDecl @param {{card: any, cases: any[]}} o
|
|
384
|
+
* @param {any} closeDecl @param {{card: any, cases: any[], judgeModel: string}} o
|
|
384
385
|
* @returns {any} a new closeDecl
|
|
385
386
|
*/
|
|
386
|
-
export function foldJudgedArtifacts(closeDecl, { card, cases }) {
|
|
387
|
+
export function foldJudgedArtifacts(closeDecl, { card, cases, judgeModel }) {
|
|
388
|
+
if (typeof judgeModel !== 'string' || judgeModel.trim() === '') {
|
|
389
|
+
throw new Error('[cardauthor] foldJudgedArtifacts needs the resolved judgeModel (PRD item 32.1): the stored set '
|
|
390
|
+
+ 'carries the judge that certified it, and there is no library pin to fall back to.');
|
|
391
|
+
}
|
|
387
392
|
const out = structuredClone(closeDecl);
|
|
388
|
-
|
|
389
|
-
|
|
393
|
+
// `judgedStages` filters `out`'s OWN stage objects, so the card assignment
|
|
394
|
+
// below mutates the clone in place — the reason this reads `out`, not the
|
|
395
|
+
// caller's `closeDecl`.
|
|
396
|
+
const judged = judgedStages(out);
|
|
390
397
|
if (judged.length !== 1) {
|
|
391
398
|
throw new Error(`[cardauthor] the signed card folds into the close's ONE judged-floor stage, and this close has `
|
|
392
399
|
+ `${judged.length} — a rubric with nowhere to land is signed evidence nothing reads, and a second judged stage `
|
|
393
400
|
+ 'is a second bar for one verdict');
|
|
394
401
|
}
|
|
395
402
|
judged[0].params = { ...(isObj(judged[0].params) ? judged[0].params : {}), card: structuredClone(card) };
|
|
396
|
-
out.calibration = { cases: structuredClone(cases), judgeModel
|
|
403
|
+
out.calibration = { cases: structuredClone(cases), judgeModel };
|
|
397
404
|
return out;
|
|
398
405
|
}
|
|
399
406
|
|
package/src/cli.js
CHANGED
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
// run, never the unresolved bundleHash. `history.jsonl` records both hashes
|
|
20
20
|
// side by side (POC fact 2) so the pairing can never drift silently.
|
|
21
21
|
|
|
22
|
-
import { createRequire } from 'node:module';
|
|
23
22
|
import { createInterface } from 'node:readline/promises';
|
|
24
23
|
import { hostname } from 'node:os';
|
|
25
24
|
import { execFileSync } from 'node:child_process';
|
|
@@ -32,17 +31,15 @@ import {
|
|
|
32
31
|
|
|
33
32
|
import {
|
|
34
33
|
exportBundle, readBundle, resolveBundleSpec, checkEnvelope, bless, verifyBlessing, appendHistory, checkBundleDeps,
|
|
35
|
-
runJob, makeSpine, loadRegistry, listingRow, jobSpecHash, resolveWorkerModel,
|
|
34
|
+
runJob, makeSpine, loadRegistry, listingRow, jobSpecHash, resolveWorkerModel, resolveJudge,
|
|
36
35
|
} from './index.js';
|
|
36
|
+
import { resolveProvider, buildRunnerProviders } from './providers.js';
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
// spec), so `flagModel` is always undefined and only the spec's own `model`
|
|
44
|
-
// (if signed) or this default is ever in play.
|
|
45
|
-
const DEFAULT_TIER_MODELS = { sonnet: 'claude-sonnet-5', haiku: 'claude-haiku-4-5-20251001' };
|
|
38
|
+
// The tier->model tables live in `src/providers.js` now (PRD item 28's
|
|
39
|
+
// factory) — one seam instead of a copy hardcoded in each runner. A
|
|
40
|
+
// `bareloop run` has no `--model` flag (not in the frozen spec), so
|
|
41
|
+
// `flagModel` is always undefined below and only the spec's own `model`
|
|
42
|
+
// (if signed) or the resolved provider's default tier is ever in play.
|
|
46
43
|
|
|
47
44
|
/** @param {unknown} v @returns {v is Record<string, any>} */
|
|
48
45
|
const isObj = (v) => typeof v === 'object' && v !== null;
|
|
@@ -58,20 +55,63 @@ const isObj = (v) => typeof v === 'object' && v !== null;
|
|
|
58
55
|
* @param {any} spec the (already `$BARELOOP_BUNDLE`-resolved) bundle spec
|
|
59
56
|
*/
|
|
60
57
|
function buildProviders(apiKey, spec) {
|
|
61
|
-
|
|
58
|
+
// `bareloop run`'s frozen key contract (module header above) is
|
|
59
|
+
// ANTHROPIC_API_KEY only — this `apiKey` is always that key. A bundle
|
|
60
|
+
// whose spec names a provider that reads a DIFFERENT env key therefore
|
|
61
|
+
// has no seam into this flow yet, and the one thing this must never do is
|
|
62
|
+
// construct that provider with the Anthropic key: the run would fail at
|
|
63
|
+
// the first call with a vendor 401, which reads as a credential problem
|
|
64
|
+
// rather than as the unbuilt seam it actually is. So it REFUSES here, at
|
|
65
|
+
// $0, naming the key it would have needed. Widening the contract (a
|
|
66
|
+
// signed per-provider key story) is PRD item 28 part (2)/(3), the
|
|
67
|
+
// arbiter's, not this factory rewire's — the runner that actually fires
|
|
68
|
+
// openai-api jobs today is `scripts/run-u.mjs`, which IS key-aware.
|
|
69
|
+
const providerName = isObj(spec) && typeof spec.provider === 'string' ? spec.provider : 'anthropic-api';
|
|
70
|
+
const entry = resolveProvider(providerName);
|
|
71
|
+
if (entry.envKey !== 'ANTHROPIC_API_KEY') {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`bareloop run: this bundle's spec names provider "${providerName}", which reads `
|
|
74
|
+
+ `${entry.envKey}. The bundle runner's key contract is ANTHROPIC_API_KEY only, so it `
|
|
75
|
+
+ 'refuses rather than calling that provider with the wrong key (PRD item 28 part 2 — '
|
|
76
|
+
+ 'a signed per-provider key story is not built). Run this job through scripts/run-u.mjs.',
|
|
77
|
+
);
|
|
78
|
+
}
|
|
62
79
|
const modelResolution = resolveWorkerModel({
|
|
63
80
|
specModel: isObj(spec) && typeof spec.model === 'string' ? spec.model : undefined,
|
|
64
81
|
flagModel: undefined,
|
|
65
|
-
defaultModel:
|
|
82
|
+
defaultModel: entry.tiers.sonnet,
|
|
66
83
|
});
|
|
67
84
|
const MODEL = modelResolution.model;
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
85
|
+
const tierModels = modelResolution.source === 'spec' ? { ...entry.tiers, sonnet: MODEL } : entry.tiers;
|
|
86
|
+
const baseUrl = isObj(spec) && typeof spec.baseUrl === 'string' ? spec.baseUrl : undefined;
|
|
87
|
+
// THE JUDGE, RESOLVED RATHER THAN PINNED (PRD item 32.1/32.2). The bundle
|
|
88
|
+
// runner's key contract is `ANTHROPIC_API_KEY`-only and the refusal above has
|
|
89
|
+
// already established that the WORKER provider reads that key — so a judge
|
|
90
|
+
// defaulting to the worker's own identity reads the same key by construction,
|
|
91
|
+
// and a signed `judge` naming a different provider would need a key this
|
|
92
|
+
// runner does not have. That case refuses here, at $0, by name — the same
|
|
93
|
+
// shape and the same reason as the worker refusal above, never a call made
|
|
94
|
+
// with the wrong vendor's key.
|
|
95
|
+
const judge = resolveJudge({
|
|
96
|
+
specJudge: isObj(spec) ? spec.judge : undefined,
|
|
97
|
+
workerProvider: providerName,
|
|
98
|
+
workerModel: MODEL,
|
|
99
|
+
});
|
|
100
|
+
const judgeEntry = resolveProvider(judge.provider);
|
|
101
|
+
if (judgeEntry.envKey !== 'ANTHROPIC_API_KEY') {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`bareloop run: this bundle's spec names judge provider "${judge.provider}", which reads `
|
|
104
|
+
+ `${judgeEntry.envKey}. The bundle runner's key contract is ANTHROPIC_API_KEY only, so it `
|
|
105
|
+
+ 'refuses rather than calling that provider with the wrong key. Run this job through scripts/run-u.mjs.',
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
...buildRunnerProviders({
|
|
110
|
+
providerName, apiKey, model: MODEL, tierModels, baseUrl,
|
|
111
|
+
judgeApiKey: apiKey, judgeModel: judge.model, judgeProviderName: judge.provider, judgeBaseUrl: baseUrl,
|
|
112
|
+
}),
|
|
113
|
+
judgeModel: judge.model,
|
|
114
|
+
};
|
|
75
115
|
}
|
|
76
116
|
|
|
77
117
|
/** @param {string[]} args @returns {{ positional: string[], flags: Record<string, string|true> }} */
|
|
@@ -221,6 +261,11 @@ async function doRun(args, { out, err, cwd, env, now, deps }) {
|
|
|
221
261
|
// provider (the test seam). Absent and no injected provider: print the
|
|
222
262
|
// operator questions (the bundle's own README) and the hash, spend nothing.
|
|
223
263
|
let { provider, providerFor, judgeProvider } = deps;
|
|
264
|
+
// the judge IDENTITY travels with the judge PROVIDER (PRD item 32.1). A test
|
|
265
|
+
// injecting `deps.judgeProvider` may name it too; absent, it is resolved
|
|
266
|
+
// alongside the real providers below, and a judged stage with neither stops as
|
|
267
|
+
// a wiring gap rather than grading under a name nobody wrote.
|
|
268
|
+
let judgeModel = /** @type {string|null} */ (deps.judgeModel ?? null);
|
|
224
269
|
if (!provider) {
|
|
225
270
|
const apiKey = env.ANTHROPIC_API_KEY;
|
|
226
271
|
if (!apiKey) {
|
|
@@ -230,7 +275,13 @@ async function doRun(args, { out, err, cwd, env, now, deps }) {
|
|
|
230
275
|
out(`bundleHash: ${bundle.manifest.bundleHash}`);
|
|
231
276
|
return 0;
|
|
232
277
|
}
|
|
233
|
-
|
|
278
|
+
try {
|
|
279
|
+
({ provider, providerFor, judgeProvider, judgeModel } = buildProviders(apiKey, bundle.spec));
|
|
280
|
+
} catch (e) {
|
|
281
|
+
// a refusal from the key contract above: name it and spend nothing
|
|
282
|
+
err(String(/** @type {any} */ (e)?.message ?? e));
|
|
283
|
+
return 1;
|
|
284
|
+
}
|
|
234
285
|
}
|
|
235
286
|
|
|
236
287
|
// 4. blessing.
|
|
@@ -317,6 +368,7 @@ async function doRun(args, { out, err, cwd, env, now, deps }) {
|
|
|
317
368
|
provider,
|
|
318
369
|
providerFor,
|
|
319
370
|
judgeProvider,
|
|
371
|
+
judgeModel,
|
|
320
372
|
emit,
|
|
321
373
|
closeDir,
|
|
322
374
|
shellCapUsd: runSpec.budgetUsd,
|
|
@@ -449,7 +501,7 @@ async function runMenu(deps, ctx) {
|
|
|
449
501
|
* code, so `bin/bareloop.mjs` can set `process.exitCode` and let node flush
|
|
450
502
|
* queued stdout on its own (F-something: `process.exit()` can discard it).
|
|
451
503
|
* @param {string[]} argv
|
|
452
|
-
* @param {{ env?: Record<string,string|undefined>, stdout?: any, stderr?: any, cwd?: string, provider?: any, providerFor?: any, judgeProvider?: any, now?: () => number, stdin?: any }} deps
|
|
504
|
+
* @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
|
|
453
505
|
* @returns {Promise<number>}
|
|
454
506
|
*/
|
|
455
507
|
export async function main(argv, deps = {}) {
|
package/src/declaredclose.js
CHANGED
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
// once instead of once-per-invocation, but `runPlan` has a dozen return paths and
|
|
69
69
|
// a leaked worktree outlives the process that made it.
|
|
70
70
|
|
|
71
|
-
import { runStage, makeSeedTrees, STOP_FAULTS, EXIT_RED, JUDGED_FLOOR_KIND } from './kinds.js';
|
|
71
|
+
import { runStage, makeSeedTrees, STOP_FAULTS, EXIT_RED, JUDGED_FLOOR_KIND, judgedStages } from './kinds.js';
|
|
72
72
|
import {
|
|
73
73
|
validateDeclaration, classGuards, genreOwnedEnvNames, ungroundedGenreEnv, GENRE_LANGUAGES, TYPES_GENRE,
|
|
74
74
|
VERDICT_CLASSES, LIVE_CLASSES, NEVER_OFFERED_KINDS,
|
|
@@ -424,8 +424,7 @@ export function validateCloseDecl(closeDecl, opts = {}) {
|
|
|
424
424
|
// redded here — making the set MANDATORY is the signing gate's call, and
|
|
425
425
|
// module 5 owns it.
|
|
426
426
|
if (closeDecl.calibration !== undefined) {
|
|
427
|
-
const judged = (
|
|
428
|
-
.filter((/** @type {any} */ s) => isObj(s) && s.kind === JUDGED_FLOOR_KIND);
|
|
427
|
+
const judged = judgedStages(closeDecl);
|
|
429
428
|
if (!isObj(closeDecl.calibration)) {
|
|
430
429
|
red('invalid-value', `${at}.calibration`, 'the frozen calibration set, as an object carrying `cases`');
|
|
431
430
|
} else if (judged.length !== 1) {
|
|
@@ -567,13 +566,15 @@ function translate(r, redact) {
|
|
|
567
566
|
* seedTrees?: any, gapCap?: number, maxBuffer?: number, baselineMode?: 'auto'|'worktree',
|
|
568
567
|
* humanRuling?: {decision: string, text?: string|null}|null,
|
|
569
568
|
* judgeLoop?: ((o: {system: string}) => any)|null,
|
|
570
|
-
*
|
|
569
|
+
* judgeModel?: string|null,
|
|
570
|
+
* onJudgeCost?: ((c: any) => void)|null,
|
|
571
|
+
* callBounds?: (() => {timeoutMs?: number, deadlineMs?: number})|null}} [opts]
|
|
571
572
|
* @returns {Promise<any>}
|
|
572
573
|
*/
|
|
573
574
|
export async function runDeclaredStages(stages, redact = (s) => s, opts = {}) {
|
|
574
575
|
const {
|
|
575
576
|
timeoutMs, cwd, seedRef, seedTrees: shared, gapCap, maxBuffer, baselineMode, humanRuling = null,
|
|
576
|
-
judgeLoop = null, onJudgeCost = null,
|
|
577
|
+
judgeLoop = null, judgeModel = null, onJudgeCost = null, callBounds = null,
|
|
577
578
|
} = opts;
|
|
578
579
|
if (!isNonEmptyString(cwd) || !isNonEmptyString(seedRef)) {
|
|
579
580
|
// Refuse rather than guess. A declared close measures against a SEED; a
|
|
@@ -603,14 +604,27 @@ export async function runDeclaredStages(stages, redact = (s) => s, opts = {}) {
|
|
|
603
604
|
// human stage reads the absence as "nobody has answered yet", which is the
|
|
604
605
|
// whole of what a pause means.
|
|
605
606
|
humanRuling,
|
|
606
|
-
// THE PAID SEAM and THE METER,
|
|
607
|
-
// every mechanical close. A judged stage with no seam STOPS as a
|
|
608
|
-
// rather than falling back to some other model (M1 owns no
|
|
609
|
-
//
|
|
610
|
-
//
|
|
611
|
-
//
|
|
607
|
+
// THE PAID SEAM, THE IDENTITY and THE METER, all operator territory and all
|
|
608
|
+
// absent on every mechanical close. A judged stage with no seam STOPS as a
|
|
609
|
+
// wiring gap rather than falling back to some other model (M1 owns no
|
|
610
|
+
// provider), and `judgeModel` is the seam's OTHER half (PRD item 32.1) —
|
|
611
|
+
// WHICH model the seam drives, resolved by the caller (`resolveJudge`,
|
|
612
|
+
// src/judged.js) and passed through untouched here, exactly as `judgeLoop`
|
|
613
|
+
// is. `runStage` checks the identity before the seam, so a judged stage with
|
|
614
|
+
// neither wired reports the identity gap, and one with only the seam missing
|
|
615
|
+
// still names the judge it would have graded with. Every locate call the
|
|
616
|
+
// seam buys is reported OUT through the meter so the run's ONE ledger can
|
|
617
|
+
// see it — a close has cost $0 until this kind, and a budget must fund the
|
|
618
|
+
// attempt PLUS its close.
|
|
612
619
|
judgeLoop,
|
|
620
|
+
judgeModel,
|
|
613
621
|
onJudgeCost,
|
|
622
|
+
// F152/PRD 30.4 — the SAME per-call time bounds every worker round already
|
|
623
|
+
// carries (`callBounds()`, src/planrun.js:1518), so the judged stage's own
|
|
624
|
+
// locate call cannot hang the process on a live silent endpoint. `null` is
|
|
625
|
+
// the honest absence (an adopter/test that wires no clock through), never a
|
|
626
|
+
// defaulted number invented here.
|
|
627
|
+
callBounds,
|
|
614
628
|
seedTrees,
|
|
615
629
|
};
|
|
616
630
|
try {
|
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
|
@@ -133,7 +133,32 @@ export const CADENCE_UNITS = Object.freeze(['hour', 'day', 'week']);
|
|
|
133
133
|
* subscription — its costUsd axis is NOTIONAL (API-equivalent value, not
|
|
134
134
|
* billed), so it is a DISTINCT condition: rows never pool with anthropic-api
|
|
135
135
|
* rows on the cost axis, and budgets sized for one do not transfer. */
|
|
136
|
-
|
|
136
|
+
/** PRD item 28 (2026-09-06/09-09): `openai-api` — an OpenAI-shaped provider,
|
|
137
|
+
* constructed via `src/providers.js`'s factory. hamr's ruling (PRD 30.7):
|
|
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
|
+
*
|
|
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']);
|
|
137
162
|
/** V3 environment label: declared keys only — every field is a lineage-key
|
|
138
163
|
* candidate at N3. `provider` is part of the key by definition (top-level,
|
|
139
164
|
* not duplicated here). */
|
|
@@ -141,7 +166,10 @@ export const CONDITION_KEYS = Object.freeze(['providerPath', 'closeVerbosity', '
|
|
|
141
166
|
// `steps` stays in the field list ONLY so a retired spec reds by name
|
|
142
167
|
// (`shape-retired`) instead of falling through to a generic unknown-field —
|
|
143
168
|
// the operator gets told what happened, not just that something is wrong.
|
|
144
|
-
const JOB_FIELDS = ['schema', 'job', 'description', 'provider', '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'];
|
|
145
173
|
/** the four-field plan shape's core (decision 5) — presence of any of these
|
|
146
174
|
* declares the shape; `tools` (the ceiling) rides the shape but alone does not
|
|
147
175
|
* declare it, so a legacy spec carrying it gets a pointed red, not a conflict */
|
|
@@ -267,6 +295,68 @@ export function validateJob(input, { shellCapUsd = 2 } = {}) {
|
|
|
267
295
|
red('invalid-value', 'model', 'non-empty string — exact provider model id (e.g. "claude-sonnet-5")');
|
|
268
296
|
}
|
|
269
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
|
+
|
|
329
|
+
// BASE URL — PRD item 28, ruling (d), 2026-09-09: admitted in v1 of
|
|
330
|
+
// openai-api (an OpenAI-compatible gateway/endpoint), OPTIONAL and part of
|
|
331
|
+
// the signed hash like `model` above. This validator has no network
|
|
332
|
+
// access, so it checks SHAPE only: `https://` for a real endpoint, or
|
|
333
|
+
// `http://` restricted to a loopback host (a local OpenAI-compatible
|
|
334
|
+
// server, the only legitimate reason to skip TLS) — and never a
|
|
335
|
+
// credential embedded IN the URL itself. A URL travels into logs and
|
|
336
|
+
// spine records far more casually than a header ever would, and secrets
|
|
337
|
+
// never enter the tree/spine (hard line #3) — a bareUrl carrying
|
|
338
|
+
// `user:pass@host` is exactly that leak, just spelled differently.
|
|
339
|
+
if (spec.baseUrl !== undefined) {
|
|
340
|
+
if (!isNonEmptyString(spec.baseUrl)) {
|
|
341
|
+
red('invalid-value', 'baseUrl', 'non-empty string — an https:// URL (http:// admitted only to 127.0.0.1/localhost/::1)');
|
|
342
|
+
} else {
|
|
343
|
+
/** @type {URL|null} */
|
|
344
|
+
let u = null;
|
|
345
|
+
try { u = new URL(spec.baseUrl); } catch { /* u stays null — caught below */ }
|
|
346
|
+
if (!u) {
|
|
347
|
+
red('invalid-value', 'baseUrl', 'must be a parseable URL');
|
|
348
|
+
} else if (u.username !== '' || u.password !== '') {
|
|
349
|
+
red('invalid-value', 'baseUrl', 'no embedded credentials (user:pass@host) — secrets never enter a signed spec');
|
|
350
|
+
} else if (u.protocol === 'https:') {
|
|
351
|
+
// admitted
|
|
352
|
+
} else if (u.protocol === 'http:' && (u.hostname === '127.0.0.1' || u.hostname === 'localhost' || u.hostname === '::1')) {
|
|
353
|
+
// admitted — a local OpenAI-compatible endpoint, the one case that legitimately skips TLS
|
|
354
|
+
} else {
|
|
355
|
+
red('invalid-value', 'baseUrl', 'https:// required (http:// admitted only to 127.0.0.1/localhost/::1)');
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
270
360
|
// 3. the outer write fence — operator law (interview decision #4), same
|
|
271
361
|
// containment rules as the workflow layer THROUGH THE SAME CODE (F9: two
|
|
272
362
|
// transforms would let a fence validate green and mean something else).
|