bareloop 0.40.0 → 0.42.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +96 -1
  2. package/README.md +13 -21
  3. package/bareloop.context.md +141 -128
  4. package/package.json +5 -4
  5. package/src/authorflow.js +346 -38
  6. package/src/authoring.js +92 -7
  7. package/src/authorjob.js +82 -20
  8. package/src/authorreadout.js +89 -5
  9. package/src/authorrun.js +39 -30
  10. package/src/authorscout.js +112 -4
  11. package/src/bundle.js +7 -4
  12. package/src/bundlerun.js +4 -1
  13. package/src/cli.js +1 -1
  14. package/src/config.js +129 -29
  15. package/src/declaredclose.js +23 -12
  16. package/src/draftspend.js +4 -4
  17. package/src/facts.js +317 -0
  18. package/src/factwords.js +129 -0
  19. package/src/index.js +2 -4
  20. package/src/inputlook.js +301 -0
  21. package/src/interviewrun.js +17 -25
  22. package/src/job.js +15 -8
  23. package/src/jobblock.js +7 -2
  24. package/src/judged.js +11 -43
  25. package/src/keysfile.js +15 -7
  26. package/src/kinds.js +86 -5
  27. package/src/labelscan.js +170 -0
  28. package/src/modellist.js +84 -0
  29. package/src/monthly.js +23 -43
  30. package/src/pagefetch.js +125 -0
  31. package/src/panel/authorroutes.js +34 -16
  32. package/src/panel/authorsession.js +161 -80
  33. package/src/panel/index.html +553 -152
  34. package/src/panel/jobplan.js +34 -11
  35. package/src/panel/runroutes.js +3 -0
  36. package/src/panel/server.js +54 -18
  37. package/src/panel/settingsroutes.js +232 -173
  38. package/src/planrun.js +5 -4
  39. package/src/providerrows.js +226 -141
  40. package/src/providers.js +58 -332
  41. package/src/providershapes.js +18 -0
  42. package/src/replay.js +1 -1
  43. package/src/rubricstep.js +804 -0
  44. package/src/source.js +166 -1
  45. package/src/thinking.js +130 -0
  46. package/src/u-readout.js +1 -1
  47. package/src/userrun.js +64 -75
  48. package/types/authorflow.d.ts +272 -18
  49. package/types/authoring.d.ts +29 -1
  50. package/types/authorjob.d.ts +9 -7
  51. package/types/authorreadout.d.ts +27 -2
  52. package/types/authorscout.d.ts +72 -2
  53. package/types/config.d.ts +23 -6
  54. package/types/declaredclose.d.ts +4 -2
  55. package/types/draftspend.d.ts +8 -8
  56. package/types/facts.d.ts +71 -0
  57. package/types/factwords.d.ts +42 -0
  58. package/types/index.d.ts +1 -1
  59. package/types/inputlook.d.ts +238 -0
  60. package/types/job.d.ts +5 -5
  61. package/types/jobblock.d.ts +8 -1
  62. package/types/judged.d.ts +5 -36
  63. package/types/keysfile.d.ts +15 -4
  64. package/types/labelscan.d.ts +38 -0
  65. package/types/modellist.d.ts +50 -0
  66. package/types/monthly.d.ts +4 -3
  67. package/types/pagefetch.d.ts +71 -0
  68. package/types/panel/authorsession.d.ts +20 -11
  69. package/types/panel/jobplan.d.ts +12 -4
  70. package/types/panel/server.d.ts +18 -6
  71. package/types/providerrows.d.ts +192 -99
  72. package/types/providers.d.ts +34 -159
  73. package/types/providershapes.d.ts +10 -0
  74. package/types/rubricstep.d.ts +286 -0
  75. package/types/source.d.ts +94 -0
  76. package/types/thinking.d.ts +90 -0
  77. package/types/userrun.d.ts +17 -1
package/src/userrun.js CHANGED
@@ -25,7 +25,6 @@
25
25
  // authored is preserved from the spine as a reusable artifact, and the next run of
26
26
  // this shape reuses and fine-tunes it rather than starting cold.
27
27
  import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, rmSync, readdirSync, openSync, readSync, closeSync, fstatSync, unlinkSync } from 'node:fs';
28
- import { createRequire } from 'node:module';
29
28
  import { fileURLToPath } from 'node:url';
30
29
  import { execFileSync, spawn } from 'node:child_process';
31
30
  import { join, resolve, dirname, basename } from 'node:path';
@@ -43,7 +42,7 @@ import { makeSpine } from './spine.js';
43
42
  // signed into the job spec — a job is a repeatable SHAPE, the source/
44
43
  // destination a per-run value, exactly like `bareloop run <bundle> --repo
45
44
  // <path>` varies the repo without touching the bundle's signature).
46
- import { readSourceManifest, frontDoorFromManifest, proveDestination, copyOut } from './source.js';
45
+ import { readSourceManifest, frontDoorFromManifest, proveDestination, copyOut, refreezeUrlInputs } from './source.js';
47
46
  import { scanSecrets, redactSecrets } from './validate.js';
48
47
  import { runBehaviour, formatBehaviour } from './behaviour.js';
49
48
  // the three doors' SEMANTICS live in the library; this script surfaces them
@@ -83,14 +82,11 @@ import { parseJsonl } from './replayio.js';
83
82
  import { claimRun, monthlyRefusalText, legSpend, chainSpend } from './monthly.js';
84
83
  import { ConfigError } from './config.js';
85
84
  import { commandFor } from './invoke.js';
86
- import { applyConfiguredKey, judgeRatesFor, keyNameFor, priceReadout, ratesFor, rowsForHome } from './providerrows.js';
85
+ import { judgeRatesFor, keyNameFor, keyValueOf, priceReadout, ratesFor, ratesOfLine, resolveWorker, judgeRowFor, rowsForHome, REFUSE_NAME_MODEL } from './providerrows.js';
87
86
  // the banner's wall arithmetic, extracted so it is reachable by a test (F83): the
88
87
  // end-of-run readout sits past the approval gate, so nothing could ever drive it here
89
88
  import { wallLine, doomedResume, deathAtOf, evidencePackage, doorLines, resumeAtLines, reviewDoorPackage, runDoorLines, tokensLine, doorTimingRedLines } from './u-readout.js';
90
89
 
91
- const require = createRequire(import.meta.url);
92
- const { AnthropicProvider } = require('bare-agent/providers');
93
-
94
90
  const JOBS = {
95
91
  'aurora-spawner': {
96
92
  spec: 'aurora-u-spawner-types.json',
@@ -174,10 +170,10 @@ const JOBS = {
174
170
  },
175
171
  // PRD item 28 / 30.9 — the openai-api probe through THIS runner (the shipped
176
172
  // one), not poc-run-param.mjs. Same job shape, same seed, its own patient copy.
177
- 'bareguard-types-deepseek': {
178
- spec: 'bareguard-u-types-deepseek.json',
179
- workdir: '/home/hamr/PycharmProjects/bareloop-patients/bareguard-u-deepseek',
180
- spine: 'bareguard-u-deepseek-bareloop',
173
+ 'bareguard-types-gateway': {
174
+ spec: 'bareguard-u-types-gateway.json',
175
+ workdir: '/home/hamr/PycharmProjects/bareloop-patients/bareguard-u-gateway',
176
+ spine: 'bareguard-u-gateway-bareloop',
181
177
  seed: '2ae8fcd37041c186524a6eb5e953b9752cd602fa',
182
178
  },
183
179
  // N4's hitl PROVING job (build plan §Proving; hamr's ruling, 2026-08-13). The job
@@ -239,6 +235,7 @@ class ExitSignal extends Error {
239
235
  * @property {any} [provider] the TEST SEAM: supplying this skips the real-key
240
236
  * check entirely, the same shape `src/cli.js`'s `main(argv, deps)` uses.
241
237
  * @property {(tier: string) => any} [providerFor]
238
+ * @property {(url: string) => Promise<{ok: true, text: string, url: string}|{ok: false, error: string}>} [fetchUrl] PART 1c - a test seam: how a web input is read at run start (production: barebrowse)
242
239
  * @property {any} [judgeProvider]
243
240
  * @property {string} [runlistHome] F196 — a test seam for `appendRun`'s
244
241
  * (src/runlist.js) `home` override, the same shape `deps.provider` already
@@ -273,7 +270,8 @@ class ExitSignal extends Error {
273
270
  * @property {string} [into] the source door's own directory (`source.json`, and the spine directory beside it).
274
271
  * Default `dirname(workdir)` — the copied-tree layout, where the tree is `<into>/tree`. A repo job on a WORKTREE
275
272
  * (P6 item 1) has its tree inside the person's repo, so its `into` is passed explicitly.
276
- * @property {string|null} [model] the raw `--model` tier name (`sonnet`/`haiku`), or null for the default
273
+ * @property {string|null} [model] the raw `--model <id>` (a model id of a ticked Settings line), or null
274
+ * @property {string|null} [providerKey] the raw `--provider <KEY>` (a key's env name), or null
277
275
  * @property {string|null} [readShim] the raw `--read-shim` arm name, or null for the default
278
276
  * @property {string|null} [scout] the raw `--scout` value (`on`/`off`), or null for the default
279
277
  * @property {string|null} [draftSpentUsd] the raw `--draft-spent-usd` value (hamr's
@@ -306,6 +304,7 @@ class ExitSignal extends Error {
306
304
  * @property {string} spineName
307
305
  * @property {string} [into] see `RunOpts.into`
308
306
  * @property {string|null} model
307
+ * @property {string|null} providerKey
309
308
  * @property {string|null} readShim
310
309
  * @property {string|null} scout
311
310
  * @property {string|null} draftSpentUsd
@@ -389,25 +388,13 @@ async function execute(ctx) {
389
388
  /** @type {{workdir: string, seed: string, spine: string}} */
390
389
  const target = { workdir: ctx.workdir, seed: ctx.seed, spine: ctx.spineName };
391
390
 
392
- // --model picks the DEFAULT worker tier (runner territory — the spec names no model, so
393
- // the signed hash is unaffected). Tier names, not model ids. This map is the OPERATOR's
394
- // and is deliberately WIDER than the planner's menu: since 2026-08-06 `STEP_MODELS` is
395
- // sonnet-only (the haiku attribution probe — see src/plan.js), so a PLAN can no longer
396
- // say haiku while `--model haiku` still can. haiku takes no output_config.effort
397
- // (provider-gated, battery rule) — nothing to gate yet since neither tier sets effort.
398
- // PRD v1.36 floor: the drafter/default tier is sonnet MINIMUM — measured, a haiku
399
- // drafter died plan-red twice on the same rejection (ms7gne7s). `--model haiku` runs
400
- // BELOW the floor: an explicit operator probe whose rows are probes, never battery
401
- // evidence.
402
- const DEFAULT_TIER_MODELS = { sonnet: 'claude-sonnet-5', haiku: 'claude-haiku-4-5-20251001' };
403
- // `modelFlagArg` is null when --model was never passed at all — distinct from a
404
- // flag that happens to resolve to the default tier's own id, because the SPEC
405
- // (below, once loaded) needs to know whether a flag was actually given, not just
406
- // what it resolved to (build-list #3: spec wins, a mismatched flag is refused).
391
+ // --model <id> names the Settings line a run works on when the SPEC names no model (an older spec, signed before a job carried its
392
+ // own); a spec's own `model` still wins and a different --model is refused. --provider <KEY> narrows to one key when two keys tick the
393
+ // same id. Runner territory: the spec names neither, so the signed hash is unaffected. There are no tier aliases and no built-in model.
394
+ // `modelFlagArg` is null when --model was never passed — distinct from a flag that happens to equal the spec's model, because the SPEC
395
+ // (below, once loaded) needs to know whether a flag was actually given (build-list #3: spec wins, a mismatched flag is refused).
407
396
  const modelFlagArg = ctx.model ?? null;
408
- const tierArg = modelFlagArg ?? 'sonnet';
409
- const FLAG_TIER_MODEL = DEFAULT_TIER_MODELS[/** @type {keyof typeof DEFAULT_TIER_MODELS} */ (tierArg)];
410
- if (!FLAG_TIER_MODEL) { err(`unknown --model "${tierArg}" — one of: ${Object.keys(DEFAULT_TIER_MODELS).join(', ')}`); throw new ExitSignal(2); }
397
+ const providerFlagArg = ctx.providerKey ?? null;
411
398
  // --read-shim picks the READ SHIM's ARM (src/readshim.js, Phase 2 A0/A1/A2/A3). Runner
412
399
  // territory exactly like --model: the spec names no shim, so the signed hash is
413
400
  // unaffected. Phase 2 itself never flipped this default — its own "what this
@@ -545,11 +532,12 @@ async function execute(ctx) {
545
532
  try {
546
533
  modelResolution = resolveWorkerModel({
547
534
  specModel: spec.model,
548
- flagModel: modelFlagArg === null ? undefined : FLAG_TIER_MODEL,
549
- defaultModel: /** @type {NonNullable<typeof providerEntry>} */ (providerEntry).tiers.sonnet,
535
+ flagModel: modelFlagArg === null ? undefined : modelFlagArg,
536
+ // a scripted provider handed in by a test IS the run: it names its own model; a real run has no default
537
+ suppliedModel: deps.provider ? String(/** @type {any} */ (deps.provider).model ?? 'scripted') : undefined,
550
538
  });
551
539
  } catch (e) {
552
- die(/** @type {Error} */ (e).message);
540
+ die(spec.model === undefined && modelFlagArg === null ? REFUSE_NAME_MODEL : /** @type {Error} */ (e).message);
553
541
  }
554
542
  const MODEL = modelResolution.model;
555
543
  const MODEL_LABEL = modelResolution.source === 'spec' ? `${MODEL} (spec)` : MODEL;
@@ -1022,16 +1010,20 @@ async function execute(ctx) {
1022
1010
  // spine, then handed down as data (`runJob`'s `rates`) — nothing deep in the run re-reads config. A bad
1023
1011
  // price refuses here; it never falls back to the built-in guess. No price set = `null` = no `rates` key
1024
1012
  // passed anywhere, byte-identical to before. The judge's own row is resolved below, beside its key.
1025
- const keyCfg = cfgOn ? rowsForHome(cfgHome) : [];
1013
+ const keyCfg = cfgOn ? rowsForHome(cfgHome, env) : [];
1026
1014
  /** @type {ReturnType<typeof ratesFor>} */
1027
1015
  let workerPrice = null;
1028
- try {
1029
- workerPrice = ratesFor(spec.provider, spec.baseUrl, keyCfg, spec.model);
1030
- } catch (e) {
1031
- if (!(e instanceof ConfigError)) throw e;
1032
- err(`${e.message} — refusing to start rather than guess a price. Nothing spent.`);
1033
- throw new ExitSignal(2);
1034
- }
1016
+ // WHICH SETTINGS LINE THIS RUN WORKS ON (P8): the row is the key, the Base URL and the old-max_tokens tick; the line is the model
1017
+ // and its price. A scripted provider handed in by a test IS the run (no row). A refusal here is $0, before anything else.
1018
+ const W = deps.provider ? null : resolveWorker(keyCfg, spec, { model: modelFlagArg, provider: providerFlagArg });
1019
+ const refuseWorker = () => { if (W && !W.ok) { err(W.say.includes('Nothing was spent') ? W.say : `${W.say} Nothing was spent.`); throw new ExitSignal(2); } };
1020
+ // the preview cannot name its key without a line: refuse it here. A SIGNED run meets its other gates first (pause door, wall) and
1021
+ // is refused at the key check below — still before anything is spent
1022
+ if ((ctx.approve ?? null) === null) refuseWorker();
1023
+ /** the endpoint the worker talks to: the chosen row's own Base URL (a spec signed before addresses were required carries none) */
1024
+ const baseUrl = W && W.ok ? W.baseUrl : (typeof spec.baseUrl === 'string' ? spec.baseUrl : undefined);
1025
+ const wLine = W && W.ok ? ratesOfLine(W.line) : null;
1026
+ workerPrice = W ? (wLine && W.ok ? { ...wLine, envName: W.row.envName } : null) : ratesFor(spec.provider, spec.baseUrl, keyCfg, spec.model);
1035
1027
 
1036
1028
  if ((ctx.approve ?? null) !== specHash) {
1037
1029
  out(dead ? 'U — RESUME, continuing a halted run, REAL dollars' : 'U — user-mode e2e, ONE run, REAL dollars');
@@ -1192,7 +1184,7 @@ async function execute(ctx) {
1192
1184
  // right key only by accident of the runner's own generic error message,
1193
1185
  // never from this hint. `keyNameFor` is the same resolved name
1194
1186
  // the real key check at launch reads (the person's row variable, else the provider's built-in one).
1195
- const invoke = (/** @type {string} */ tail) => ` ${keyNameFor(spec.provider, spec.baseUrl, keyCfg, spec.model).name}=... ${INVOKE}${dead ? ` --resume ${RESUME}` : ''}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL}${REGISTRY_TAIL}${tail} --approve ${PRINT_APPROVE}`;
1187
+ const invoke = (/** @type {string} */ tail) => ` ${(W && W.ok ? W.row.envName : null) ?? keyNameFor(spec.provider, spec.baseUrl, keyCfg, spec.model).name ?? '<KEY>'}=... ${INVOKE}${dead ? ` --resume ${RESUME}` : ''}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL}${REGISTRY_TAIL}${tail} --approve ${PRINT_APPROVE}`;
1196
1188
  /** the door the operator has already picked, as flags — hoisted out of the else
1197
1189
  * below so the inhibitor line at the bottom can print the WHOLE command rather
1198
1190
  * than a shape the operator has to assemble. Empty on an ordinary run and on the
@@ -1446,13 +1438,15 @@ async function execute(ctx) {
1446
1438
  let judgePrice = null;
1447
1439
  if (JUDGES) {
1448
1440
  try {
1449
- judgePrice = judgeRatesFor(judge, spec.provider, spec.baseUrl, workerPrice, keyCfg);
1441
+ judgePrice = judgeRatesFor(judge, spec.provider, baseUrl, workerPrice, keyCfg);
1450
1442
  } catch (e) {
1451
1443
  if (!(e instanceof ConfigError)) throw e;
1452
1444
  err(`${e.message} — refusing to start rather than guess a price. Nothing spent.`);
1453
1445
  throw new ExitSignal(2);
1454
1446
  }
1455
1447
  }
1448
+ /** the row the judge's key (and endpoint) comes from: the worker's own row when the judge shares its provider, else the judge provider's row */
1449
+ const judgeRow = judgeRowFor(keyCfg, judge, spec.provider, W && W.ok ? W.row : null);
1456
1450
  // undefined, not null: both come straight off `env[...]` lookups below, which
1457
1451
  // (like process.env) never produce null for a missing key.
1458
1452
  /** @type {string|undefined} */
@@ -1468,13 +1462,10 @@ async function execute(ctx) {
1468
1462
  // row = the built-in. Names only — values come from the
1469
1463
  // env / keys file. The judge follows the worker's choice when it is the same provider
1470
1464
  // (the same rule that gives it the worker's baseUrl below).
1471
- const workerEnv = applyConfiguredKey(env, spec.provider, spec.baseUrl, keyCfg, spec.model);
1472
- const judgeEnv = judge.provider === spec.provider ? workerEnv : applyConfiguredKey(env, judge.provider, undefined, keyCfg, judge.model);
1473
- /** the variable NAME this worker's key is read from (the person's pick, else built-in) — for messages only */
1474
- const workerKeyName = keyNameFor(spec.provider, spec.baseUrl, keyCfg, spec.model).name;
1475
- // A green anthropic-api job's behaviour is byte-identical to before: its
1476
- // worker key IS `ANTHROPIC_API_KEY`, demanded as ever.
1477
- workerApiKey = workerEnv[providerEntry.envKey];
1465
+ refuseWorker();
1466
+ const workerKeyName = /** @type {NonNullable<typeof W> & {ok: true}} */ (W).row.envName;
1467
+ // A job's worker key is its Settings row's own variable — there is no built-in name.
1468
+ workerApiKey = keyValueOf(env, workerKeyName);
1478
1469
  if (!workerApiKey) { err(`${workerKeyName} not set (secrets load from the environment — never the tree)`); throw new ExitSignal(2); }
1479
1470
  // F181 — a key that reads as "set" above can still carry a line break/
1480
1471
  // control char/stray whitespace (a two-line secret-store entry, e.g.) and
@@ -1487,10 +1478,10 @@ async function execute(ctx) {
1487
1478
  // override matters most in the case this item exists for: a job whose worker and
1488
1479
  // judge are the SAME provider but different accounts, and the anthropic case
1489
1480
  // where one variable used to do two jobs.
1490
- judgeApiKey = env.JUDGE_API_KEY ?? judgeEnv[judgeEntry.envKey];
1481
+ judgeApiKey = env.JUDGE_API_KEY ?? keyValueOf(env, judgeRow?.envName);
1491
1482
  if (JUDGES && !judgeApiKey) {
1492
1483
  err(`This job's close JUDGES (${judgedStages(spec.closeDecl).length} judged stage(s)) and its judge is ${judge.model} on ${judge.provider}${spec.judge ? ' (signed override)' : " (this job's own worker model — no `judge` override signed)"}.`);
1493
- err(`Neither JUDGE_API_KEY nor ${judgeEntry.envKey} is set — secrets load from the environment, never the tree.`);
1484
+ err(`Neither JUDGE_API_KEY nor ${judgeRow?.envName ?? `a key row for ${judge.provider}`} is set — secrets load from the environment, never the tree.`);
1494
1485
  err('Refused at $0, BEFORE the worker spends anything: a judged run that discovers this at the close has paid for a verdict it cannot render.');
1495
1486
  throw new ExitSignal(2);
1496
1487
  }
@@ -1500,7 +1491,7 @@ async function execute(ctx) {
1500
1491
  if (JUDGES) {
1501
1492
  const judgeKeyProblem = apiKeyProblem(/** @type {string} */ (judgeApiKey));
1502
1493
  if (judgeKeyProblem) {
1503
- const judgeEnvName = env.JUDGE_API_KEY ? 'JUDGE_API_KEY' : judgeEntry.envKey;
1494
+ const judgeEnvName = env.JUDGE_API_KEY ? 'JUDGE_API_KEY' : (judgeRow?.envName ?? judge.provider);
1504
1495
  err(`${judgeEnvName} ${judgeKeyProblem} — refusing rather than crashing mid-call (never trimmed or repaired; fix the value at its source)`);
1505
1496
  throw new ExitSignal(2);
1506
1497
  }
@@ -1640,7 +1631,7 @@ async function execute(ctx) {
1640
1631
  // OpenAI-compatible gateway/endpoint. Undefined for anthropic-api jobs and
1641
1632
  // for an openai-api job that names none (bare-agent's OpenAIProvider then
1642
1633
  // defaults to api.openai.com/v1 on its own).
1643
- const baseUrl = typeof spec.baseUrl === 'string' ? spec.baseUrl : undefined;
1634
+ // (`baseUrl` — the worker row's Base URL — was resolved above with the Settings line)
1644
1635
  // TEST SEAM — same shape as the bundle door (src/bundlerun.js): a caller that supplies
1645
1636
  // `deps.provider` skips the real-key check above entirely and this
1646
1637
  // construction never runs (a scripted provider IS the run).
@@ -1648,26 +1639,11 @@ async function execute(ctx) {
1648
1639
  let providerFor = deps.providerFor;
1649
1640
  let judgeProvider = deps.judgeProvider ?? null;
1650
1641
  if (!provider) {
1651
- provider = makeProvider(spec.provider, { apiKey: /** @type {string} */ (workerApiKey), model: MODEL, baseUrl });
1652
- // P: the per-step model-tier factory. The TIER menu a PLAN may name is signed in the
1653
- // plan schema (STEP_MODELS — sonnet-only since the 2026-08-06 haiku attribution probe);
1654
- // the tier->model mapping is the RUNNER's territory, here, and keeps haiku for the
1655
- // operator's own --model knob. haiku takes no output_config.effort (provider-gated,
1656
- // battery rule) - nothing to gate yet since neither tier sets effort params.
1657
- // `sonnet` maps to the RESOLVED worker model (MODEL — spec/flag/default), not the
1658
- // bare provider-table id, so a spec-named model reaches every plan step's own
1659
- // provider (STEP_MODELS is sonnet-only), not just the top-level `provider` above.
1660
- // ONLY when the SPEC named it: a `--model haiku` probe keeps its old reach (the
1661
- // top-level provider), never silently widening into every step's tier. The tier
1662
- // table itself comes from `providerEntry` (src/providers.js) — for openai-api
1663
- // today that means BOTH tiers resolve to the same `deepseek-flash` id (hamr's
1664
- // ruling, PRD 30.7: one secondary provider, not a menu; swapped from the
1665
- // retired `deepseek-chat`, F171), so a `--model haiku` probe against an
1666
- // openai-api job harmlessly re-resolves to the same model.
1667
- const TIER_MODELS = modelResolution.source === 'spec' ? { ...providerEntry.tiers, sonnet: MODEL } : providerEntry.tiers;
1668
- /** @type {Record<string, any>} */
1669
- const tierCache = {};
1670
- providerFor = (/** @type {string} */ tier) => (tierCache[tier] ??= TIER_MODELS[/** @type {keyof typeof TIER_MODELS} */ (tier)] === MODEL ? provider : makeProvider(spec.provider, { apiKey: /** @type {string} */ (workerApiKey), model: TIER_MODELS[/** @type {keyof typeof TIER_MODELS} */ (tier)], baseUrl }));
1642
+ const legacyMaxTokens = W && W.ok ? W.row.legacyMaxTokens : false;
1643
+ provider = makeProvider(spec.provider, { apiKey: /** @type {string} */ (workerApiKey), model: MODEL, baseUrl, legacyMaxTokens });
1644
+ // P: the per-step model factory. The ONLY tier a plan may name is the schema token `sonnet` (STEP_MODELS, src/plan.js — a
1645
+ // documented schema token, not a model id); it means THIS job's worker model, so every step's provider is the one above.
1646
+ providerFor = () => provider;
1671
1647
  // SOFTGREEN — the JUDGED stage's own provider. Since PRD item 32.1 its identity is
1672
1648
  // RESOLVED (`judge` above), not pinned: the spec's signed `judge` override, else this
1673
1649
  // job's own worker model. It is still never a step knob and never agent-selectable —
@@ -1684,7 +1660,7 @@ async function execute(ctx) {
1684
1660
  // every live softgreen run would have done: run-author wired this seam and this
1685
1661
  // runner never did.
1686
1662
  judgeProvider = judgeApiKey
1687
- ? makeProvider(judge.provider, { apiKey: /** @type {string} */ (judgeApiKey), model: judge.model, baseUrl: judge.provider === spec.provider ? baseUrl : undefined })
1663
+ ? makeProvider(judge.provider, { apiKey: /** @type {string} */ (judgeApiKey), model: judge.model, baseUrl: judge.provider === spec.provider ? baseUrl : (judgeRow && judgeRow.baseUrl.trim() !== '' ? judgeRow.baseUrl.trim() : undefined), legacyMaxTokens: judgeRow ? judgeRow.legacyMaxTokens : false })
1688
1664
  : null;
1689
1665
  }
1690
1666
 
@@ -2433,6 +2409,7 @@ function buildCtx(mode, opts) {
2433
2409
  spineName: /** @type {string} */ (opts.spineName),
2434
2410
  ...(opts.into === undefined ? {} : { into: opts.into }),
2435
2411
  model: opts.model ?? null,
2412
+ providerKey: opts.providerKey ?? null,
2436
2413
  readShim: opts.readShim ?? null,
2437
2414
  scout: opts.scout ?? null,
2438
2415
  draftSpentUsd: opts.draftSpentUsd ?? null,
@@ -2616,13 +2593,24 @@ export async function main(argv, deps = {}) {
2616
2593
  + 'with exactly one source-*/ sibling.');
2617
2594
  }
2618
2595
  const into = candidates[0];
2619
- const manifestRead = await readSourceManifest(into);
2596
+ let manifestRead = await readSourceManifest(into);
2620
2597
  if (manifestRead.stop !== null) {
2621
2598
  die(`--spec ${specPath}: ${manifestRead.code} — ${manifestRead.stop}`);
2622
2599
  }
2623
2600
  if (!manifestRead.present || !manifestRead.manifest) {
2624
2601
  die(`--spec ${specPath}: ${into} carries no source.json — not a prepared copy this runner can trust`);
2625
2602
  }
2603
+ // PART 1c (RF22): a FRESH run of a rubric job with web inputs fetches each address again and makes that text the run's frozen
2604
+ // input BEFORE the worker starts (the drafting copy was only for the look). A resume / door / decision keeps the copy its
2605
+ // first start froze. A refusal is plain words, $0.
2606
+ if (argFlag('resume') === null && argFlag('door') === null && argFlag('decide') === null) {
2607
+ const fresh = await refreezeUrlInputs({ into, ...(deps.fetchUrl ? { fetchUrl: deps.fetchUrl } : {}) });
2608
+ if (!fresh.ok) die(`--spec ${specPath}: ${fresh.error}`);
2609
+ if (fresh.refreshed.length > 0) {
2610
+ manifestRead = await readSourceManifest(into);
2611
+ if (manifestRead.stop !== null || !manifestRead.manifest) die(`--spec ${specPath}: the run copy's record could not be read after the fresh fetch`);
2612
+ }
2613
+ }
2626
2614
  const seed = manifestRead.manifest.seed;
2627
2615
  if (typeof seed !== 'string' || !seed) {
2628
2616
  die(`--spec ${specPath}: ${into}/source.json carries no seed commit — not a prepared copy this runner can trust`);
@@ -2661,6 +2649,7 @@ export async function main(argv, deps = {}) {
2661
2649
  spineName: target.spine,
2662
2650
  ...(target.into === undefined ? {} : { into: target.into }),
2663
2651
  model: argFlag('model'),
2652
+ providerKey: argFlag('provider'),
2664
2653
  readShim: argFlag('read-shim'),
2665
2654
  scout: argFlag('scout'),
2666
2655
  draftSpentUsd: argFlag('draft-spent-usd'),