bareloop 0.40.0 → 0.41.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 (51) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/bareloop.context.md +97 -124
  3. package/package.json +2 -2
  4. package/src/authorflow.js +9 -9
  5. package/src/authorjob.js +4 -4
  6. package/src/authorrun.js +25 -25
  7. package/src/authorscout.js +4 -4
  8. package/src/bundle.js +7 -4
  9. package/src/bundlerun.js +4 -1
  10. package/src/cli.js +1 -1
  11. package/src/config.js +129 -29
  12. package/src/draftspend.js +4 -4
  13. package/src/index.js +2 -4
  14. package/src/interviewrun.js +17 -25
  15. package/src/job.js +13 -7
  16. package/src/judged.js +11 -43
  17. package/src/keysfile.js +15 -7
  18. package/src/kinds.js +2 -2
  19. package/src/modellist.js +84 -0
  20. package/src/monthly.js +23 -43
  21. package/src/panel/authorroutes.js +19 -14
  22. package/src/panel/authorsession.js +62 -58
  23. package/src/panel/index.html +520 -145
  24. package/src/panel/runroutes.js +3 -0
  25. package/src/panel/server.js +35 -15
  26. package/src/panel/settingsroutes.js +232 -173
  27. package/src/planrun.js +2 -2
  28. package/src/providerrows.js +226 -141
  29. package/src/providers.js +58 -332
  30. package/src/providershapes.js +18 -0
  31. package/src/replay.js +1 -1
  32. package/src/thinking.js +130 -0
  33. package/src/userrun.js +50 -73
  34. package/types/authorflow.d.ts +5 -5
  35. package/types/authorjob.d.ts +2 -2
  36. package/types/authorscout.d.ts +2 -2
  37. package/types/config.d.ts +23 -6
  38. package/types/draftspend.d.ts +8 -8
  39. package/types/index.d.ts +1 -1
  40. package/types/job.d.ts +5 -5
  41. package/types/judged.d.ts +5 -36
  42. package/types/keysfile.d.ts +15 -4
  43. package/types/modellist.d.ts +50 -0
  44. package/types/monthly.d.ts +4 -3
  45. package/types/panel/authorsession.d.ts +2 -9
  46. package/types/panel/server.d.ts +14 -4
  47. package/types/providerrows.d.ts +192 -99
  48. package/types/providers.d.ts +34 -159
  49. package/types/providershapes.d.ts +10 -0
  50. package/types/thinking.d.ts +90 -0
  51. package/types/userrun.d.ts +6 -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';
@@ -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
@@ -273,7 +269,8 @@ class ExitSignal extends Error {
273
269
  * @property {string} [into] the source door's own directory (`source.json`, and the spine directory beside it).
274
270
  * Default `dirname(workdir)` — the copied-tree layout, where the tree is `<into>/tree`. A repo job on a WORKTREE
275
271
  * (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
272
+ * @property {string|null} [model] the raw `--model <id>` (a model id of a ticked Settings line), or null
273
+ * @property {string|null} [providerKey] the raw `--provider <KEY>` (a key's env name), or null
277
274
  * @property {string|null} [readShim] the raw `--read-shim` arm name, or null for the default
278
275
  * @property {string|null} [scout] the raw `--scout` value (`on`/`off`), or null for the default
279
276
  * @property {string|null} [draftSpentUsd] the raw `--draft-spent-usd` value (hamr's
@@ -306,6 +303,7 @@ class ExitSignal extends Error {
306
303
  * @property {string} spineName
307
304
  * @property {string} [into] see `RunOpts.into`
308
305
  * @property {string|null} model
306
+ * @property {string|null} providerKey
309
307
  * @property {string|null} readShim
310
308
  * @property {string|null} scout
311
309
  * @property {string|null} draftSpentUsd
@@ -389,25 +387,13 @@ async function execute(ctx) {
389
387
  /** @type {{workdir: string, seed: string, spine: string}} */
390
388
  const target = { workdir: ctx.workdir, seed: ctx.seed, spine: ctx.spineName };
391
389
 
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).
390
+ // --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
391
+ // 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
392
+ // same id. Runner territory: the spec names neither, so the signed hash is unaffected. There are no tier aliases and no built-in model.
393
+ // `modelFlagArg` is null when --model was never passed — distinct from a flag that happens to equal the spec's model, because the SPEC
394
+ // (below, once loaded) needs to know whether a flag was actually given (build-list #3: spec wins, a mismatched flag is refused).
407
395
  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); }
396
+ const providerFlagArg = ctx.providerKey ?? null;
411
397
  // --read-shim picks the READ SHIM's ARM (src/readshim.js, Phase 2 A0/A1/A2/A3). Runner
412
398
  // territory exactly like --model: the spec names no shim, so the signed hash is
413
399
  // unaffected. Phase 2 itself never flipped this default — its own "what this
@@ -545,11 +531,12 @@ async function execute(ctx) {
545
531
  try {
546
532
  modelResolution = resolveWorkerModel({
547
533
  specModel: spec.model,
548
- flagModel: modelFlagArg === null ? undefined : FLAG_TIER_MODEL,
549
- defaultModel: /** @type {NonNullable<typeof providerEntry>} */ (providerEntry).tiers.sonnet,
534
+ flagModel: modelFlagArg === null ? undefined : modelFlagArg,
535
+ // a scripted provider handed in by a test IS the run: it names its own model; a real run has no default
536
+ suppliedModel: deps.provider ? String(/** @type {any} */ (deps.provider).model ?? 'scripted') : undefined,
550
537
  });
551
538
  } catch (e) {
552
- die(/** @type {Error} */ (e).message);
539
+ die(spec.model === undefined && modelFlagArg === null ? REFUSE_NAME_MODEL : /** @type {Error} */ (e).message);
553
540
  }
554
541
  const MODEL = modelResolution.model;
555
542
  const MODEL_LABEL = modelResolution.source === 'spec' ? `${MODEL} (spec)` : MODEL;
@@ -1022,16 +1009,20 @@ async function execute(ctx) {
1022
1009
  // spine, then handed down as data (`runJob`'s `rates`) — nothing deep in the run re-reads config. A bad
1023
1010
  // price refuses here; it never falls back to the built-in guess. No price set = `null` = no `rates` key
1024
1011
  // passed anywhere, byte-identical to before. The judge's own row is resolved below, beside its key.
1025
- const keyCfg = cfgOn ? rowsForHome(cfgHome) : [];
1012
+ const keyCfg = cfgOn ? rowsForHome(cfgHome, env) : [];
1026
1013
  /** @type {ReturnType<typeof ratesFor>} */
1027
1014
  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
- }
1015
+ // 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
1016
+ // and its price. A scripted provider handed in by a test IS the run (no row). A refusal here is $0, before anything else.
1017
+ const W = deps.provider ? null : resolveWorker(keyCfg, spec, { model: modelFlagArg, provider: providerFlagArg });
1018
+ const refuseWorker = () => { if (W && !W.ok) { err(W.say.includes('Nothing was spent') ? W.say : `${W.say} Nothing was spent.`); throw new ExitSignal(2); } };
1019
+ // the preview cannot name its key without a line: refuse it here. A SIGNED run meets its other gates first (pause door, wall) and
1020
+ // is refused at the key check below — still before anything is spent
1021
+ if ((ctx.approve ?? null) === null) refuseWorker();
1022
+ /** the endpoint the worker talks to: the chosen row's own Base URL (a spec signed before addresses were required carries none) */
1023
+ const baseUrl = W && W.ok ? W.baseUrl : (typeof spec.baseUrl === 'string' ? spec.baseUrl : undefined);
1024
+ const wLine = W && W.ok ? ratesOfLine(W.line) : null;
1025
+ workerPrice = W ? (wLine && W.ok ? { ...wLine, envName: W.row.envName } : null) : ratesFor(spec.provider, spec.baseUrl, keyCfg, spec.model);
1035
1026
 
1036
1027
  if ((ctx.approve ?? null) !== specHash) {
1037
1028
  out(dead ? 'U — RESUME, continuing a halted run, REAL dollars' : 'U — user-mode e2e, ONE run, REAL dollars');
@@ -1192,7 +1183,7 @@ async function execute(ctx) {
1192
1183
  // right key only by accident of the runner's own generic error message,
1193
1184
  // never from this hint. `keyNameFor` is the same resolved name
1194
1185
  // 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}`;
1186
+ 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
1187
  /** the door the operator has already picked, as flags — hoisted out of the else
1197
1188
  * below so the inhibitor line at the bottom can print the WHOLE command rather
1198
1189
  * than a shape the operator has to assemble. Empty on an ordinary run and on the
@@ -1446,13 +1437,15 @@ async function execute(ctx) {
1446
1437
  let judgePrice = null;
1447
1438
  if (JUDGES) {
1448
1439
  try {
1449
- judgePrice = judgeRatesFor(judge, spec.provider, spec.baseUrl, workerPrice, keyCfg);
1440
+ judgePrice = judgeRatesFor(judge, spec.provider, baseUrl, workerPrice, keyCfg);
1450
1441
  } catch (e) {
1451
1442
  if (!(e instanceof ConfigError)) throw e;
1452
1443
  err(`${e.message} — refusing to start rather than guess a price. Nothing spent.`);
1453
1444
  throw new ExitSignal(2);
1454
1445
  }
1455
1446
  }
1447
+ /** 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 */
1448
+ const judgeRow = judgeRowFor(keyCfg, judge, spec.provider, W && W.ok ? W.row : null);
1456
1449
  // undefined, not null: both come straight off `env[...]` lookups below, which
1457
1450
  // (like process.env) never produce null for a missing key.
1458
1451
  /** @type {string|undefined} */
@@ -1468,13 +1461,10 @@ async function execute(ctx) {
1468
1461
  // row = the built-in. Names only — values come from the
1469
1462
  // env / keys file. The judge follows the worker's choice when it is the same provider
1470
1463
  // (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];
1464
+ refuseWorker();
1465
+ const workerKeyName = /** @type {NonNullable<typeof W> & {ok: true}} */ (W).row.envName;
1466
+ // A job's worker key is its Settings row's own variable — there is no built-in name.
1467
+ workerApiKey = keyValueOf(env, workerKeyName);
1478
1468
  if (!workerApiKey) { err(`${workerKeyName} not set (secrets load from the environment — never the tree)`); throw new ExitSignal(2); }
1479
1469
  // F181 — a key that reads as "set" above can still carry a line break/
1480
1470
  // control char/stray whitespace (a two-line secret-store entry, e.g.) and
@@ -1487,10 +1477,10 @@ async function execute(ctx) {
1487
1477
  // override matters most in the case this item exists for: a job whose worker and
1488
1478
  // judge are the SAME provider but different accounts, and the anthropic case
1489
1479
  // where one variable used to do two jobs.
1490
- judgeApiKey = env.JUDGE_API_KEY ?? judgeEnv[judgeEntry.envKey];
1480
+ judgeApiKey = env.JUDGE_API_KEY ?? keyValueOf(env, judgeRow?.envName);
1491
1481
  if (JUDGES && !judgeApiKey) {
1492
1482
  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.`);
1483
+ 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
1484
  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
1485
  throw new ExitSignal(2);
1496
1486
  }
@@ -1500,7 +1490,7 @@ async function execute(ctx) {
1500
1490
  if (JUDGES) {
1501
1491
  const judgeKeyProblem = apiKeyProblem(/** @type {string} */ (judgeApiKey));
1502
1492
  if (judgeKeyProblem) {
1503
- const judgeEnvName = env.JUDGE_API_KEY ? 'JUDGE_API_KEY' : judgeEntry.envKey;
1493
+ const judgeEnvName = env.JUDGE_API_KEY ? 'JUDGE_API_KEY' : (judgeRow?.envName ?? judge.provider);
1504
1494
  err(`${judgeEnvName} ${judgeKeyProblem} — refusing rather than crashing mid-call (never trimmed or repaired; fix the value at its source)`);
1505
1495
  throw new ExitSignal(2);
1506
1496
  }
@@ -1640,7 +1630,7 @@ async function execute(ctx) {
1640
1630
  // OpenAI-compatible gateway/endpoint. Undefined for anthropic-api jobs and
1641
1631
  // for an openai-api job that names none (bare-agent's OpenAIProvider then
1642
1632
  // defaults to api.openai.com/v1 on its own).
1643
- const baseUrl = typeof spec.baseUrl === 'string' ? spec.baseUrl : undefined;
1633
+ // (`baseUrl` — the worker row's Base URL — was resolved above with the Settings line)
1644
1634
  // TEST SEAM — same shape as the bundle door (src/bundlerun.js): a caller that supplies
1645
1635
  // `deps.provider` skips the real-key check above entirely and this
1646
1636
  // construction never runs (a scripted provider IS the run).
@@ -1648,26 +1638,11 @@ async function execute(ctx) {
1648
1638
  let providerFor = deps.providerFor;
1649
1639
  let judgeProvider = deps.judgeProvider ?? null;
1650
1640
  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 }));
1641
+ const legacyMaxTokens = W && W.ok ? W.row.legacyMaxTokens : false;
1642
+ provider = makeProvider(spec.provider, { apiKey: /** @type {string} */ (workerApiKey), model: MODEL, baseUrl, legacyMaxTokens });
1643
+ // P: the per-step model factory. The ONLY tier a plan may name is the schema token `sonnet` (STEP_MODELS, src/plan.js — a
1644
+ // documented schema token, not a model id); it means THIS job's worker model, so every step's provider is the one above.
1645
+ providerFor = () => provider;
1671
1646
  // SOFTGREEN — the JUDGED stage's own provider. Since PRD item 32.1 its identity is
1672
1647
  // RESOLVED (`judge` above), not pinned: the spec's signed `judge` override, else this
1673
1648
  // job's own worker model. It is still never a step knob and never agent-selectable —
@@ -1684,7 +1659,7 @@ async function execute(ctx) {
1684
1659
  // every live softgreen run would have done: run-author wired this seam and this
1685
1660
  // runner never did.
1686
1661
  judgeProvider = judgeApiKey
1687
- ? makeProvider(judge.provider, { apiKey: /** @type {string} */ (judgeApiKey), model: judge.model, baseUrl: judge.provider === spec.provider ? baseUrl : undefined })
1662
+ ? 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
1663
  : null;
1689
1664
  }
1690
1665
 
@@ -2433,6 +2408,7 @@ function buildCtx(mode, opts) {
2433
2408
  spineName: /** @type {string} */ (opts.spineName),
2434
2409
  ...(opts.into === undefined ? {} : { into: opts.into }),
2435
2410
  model: opts.model ?? null,
2411
+ providerKey: opts.providerKey ?? null,
2436
2412
  readShim: opts.readShim ?? null,
2437
2413
  scout: opts.scout ?? null,
2438
2414
  draftSpentUsd: opts.draftSpentUsd ?? null,
@@ -2661,6 +2637,7 @@ export async function main(argv, deps = {}) {
2661
2637
  spineName: target.spine,
2662
2638
  ...(target.into === undefined ? {} : { into: target.into }),
2663
2639
  model: argFlag('model'),
2640
+ providerKey: argFlag('provider'),
2664
2641
  readShim: argFlag('read-shim'),
2665
2642
  scout: argFlag('scout'),
2666
2643
  draftSpentUsd: argFlag('draft-spent-usd'),
@@ -619,7 +619,7 @@ export function resolveSourcePrefixes({ workdir, sourcePaths, seedFiles }: {
619
619
  */
620
620
  /**
621
621
  * @param {{ceilingUsd?: number|null,
622
- * onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number, thinkingApplied?: boolean|null}) => void}} [o]
622
+ * onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number, thinkingStatus?: string}) => void}} [o]
623
623
  * `ceilingUsd` is the OPERATOR's ceiling; `null`/absent is UNBOUNDED.
624
624
  * `onCall` is a REPORTING seam fired as each metered call lands — the shell's
625
625
  * only chance to know what has been spent while the run is still alive. It is
@@ -635,7 +635,7 @@ export function makeCostBook({ ceilingUsd, onCall }?: {
635
635
  label: string;
636
636
  costUsd: number | null;
637
637
  unpricedRounds: number;
638
- thinkingApplied?: boolean | null;
638
+ thinkingStatus?: string;
639
639
  }) => void;
640
640
  }): {
641
641
  /** @param {string} label @param {any} r a bare-agent run result @param {number} [attempt] */
@@ -700,7 +700,7 @@ export function makeCostBook({ ceilingUsd, onCall }?: {
700
700
  * `cacheMessages` (F18) and `maxTokens` (F30) are not optional on this stack.
701
701
  * `rates` is the customer's own price for this provider's key row (USD per 1K, `ratesFor`);
702
702
  * absent = bare-agent's built-in guess, exactly as before.
703
- * `draftThinking` ('on' | 'off', operator knob; default 'off') adds the drafter's per-call thinking option via `draftCallOptions`.
703
+ * `draftThinking` ('on' | 'off', the job's "Thinking on" tick spelled as a word; default 'off') adds the drafter's per-call thinking option via `thinkingCallOptions` (src/thinking.js, the one owner).
704
704
  * @param {any} provider @param {{system?: string, maxTokens?: number, rates?: {in: number, out: number}|null, draftThinking?: string}} [o]
705
705
  * @returns {(messages: any[], tools: any[], opts?: any) => Promise<any>}
706
706
  */
@@ -936,7 +936,7 @@ export function runConfirmTurn({ verdictType, answers, questions, labels, facts,
936
936
  * generate: Function, seedReadFn?: Function, closeCtx?: any,
937
937
  * ceilingUsd?: number|null,
938
938
  * onPhase?: (phase: string, data?: any) => void,
939
- * onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number, thinkingApplied?: boolean|null}) => void,
939
+ * onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number, thinkingStatus?: string}) => void,
940
940
  * maxRevisions?: number, structureRetries?: number,
941
941
  * structuredMode?: 'tool'|'text', catalogue?: Record<string, any>, writeScope?: string[]|null,
942
942
  * priorCalls?: {label: string, costUsd: number|null, unpricedRounds: number}[]|null,
@@ -976,7 +976,7 @@ export function authorClose({ workdir, seedRef, lang, verdictType, answers, ques
976
976
  label: string;
977
977
  costUsd: number | null;
978
978
  unpricedRounds: number;
979
- thinkingApplied?: boolean | null;
979
+ thinkingStatus?: string;
980
980
  }) => void;
981
981
  maxRevisions?: number;
982
982
  structureRetries?: number;
@@ -85,7 +85,7 @@ export function refuseLockedKind(kind: string): Refusal;
85
85
  * seedRef?: string|null, scout?: any, listing?: any, ceilingUsd?: number|null, judgeModel?: string|null,
86
86
  * rates?: {in: number, out: number}|null, draftThinking?: 'on'|'off',
87
87
  * onPhase?: (phase: string, data?: any) => void,
88
- * onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number, thinkingApplied?: boolean|null}) => void,
88
+ * onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number, thinkingStatus?: string}) => void,
89
89
  * seedFn?: Function, scoutFn?: Function, listingFn?: Function,
90
90
  * authorFn?: Function, authorOpts?: object, writeScope?: string[]|null,
91
91
  * signerFix?: Function|any, proposeFn?: Function, compileOpts?: object,
@@ -130,7 +130,7 @@ export function authorCloseForJob({ answers, repoPath, lang, verdictType, questi
130
130
  label: string;
131
131
  costUsd: number | null;
132
132
  unpricedRounds: number;
133
- thinkingApplied?: boolean | null;
133
+ thinkingStatus?: string;
134
134
  }) => void;
135
135
  seedFn?: Function;
136
136
  scoutFn?: Function;
@@ -92,7 +92,7 @@ export function defaultSurveyor({ workdir, granted, auditPath, ctx }: {
92
92
  * @param {{workdir: string, provider?: any,
93
93
  * rounds?: number, minBytes?: number, blobMax?: number, maxTokens?: number, ctx?: boolean,
94
94
  * attempts?: number, ceilingUsd?: number|null, callTimeoutMs?: number,
95
- * onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number, thinkingApplied?: boolean|null}) => void,
95
+ * onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number, thinkingStatus?: string}) => void,
96
96
  * createLoop?: (o: {system: string, policy: any, onLlmResult: any, provider: any, rates?: {in: number, out: number}}) => any,
97
97
  * rates?: {in: number, out: number}|null, callOptions?: Record<string, any>,
98
98
  * createSurveyor?: (o: {workdir: string, granted: readonly string[], ctx: boolean}) => Promise<any>}} o
@@ -115,7 +115,7 @@ export function runAuthorScout({ workdir, provider, rounds, minBytes, blobMax, m
115
115
  label: string;
116
116
  costUsd: number | null;
117
117
  unpricedRounds: number;
118
- thinkingApplied?: boolean | null;
118
+ thinkingStatus?: string;
119
119
  }) => void;
120
120
  createLoop?: (o: {
121
121
  system: string;
package/types/config.d.ts CHANGED
@@ -16,15 +16,31 @@ export function configPath(home?: string): string;
16
16
  export function readConfig(opts?: {
17
17
  home?: string;
18
18
  }): ConfigRead;
19
+ /**
20
+ * One key-row field's value problem in plain words, or null when fine.
21
+ * @param {string} field `model` | `shape` | `baseUrl` | `legacyMaxTokens`
22
+ * @param {unknown} v
23
+ * @returns {string|null}
24
+ */
25
+ export function providerFieldProblem(field: string, v: unknown): string | null;
26
+ /**
27
+ * THE one rule for a row's model lines, used by config validation and the Settings routes. A price
28
+ * is a PAIR (`inPerM` and `outPerM`, each a finite number >= 0 — 0 / 0 typed is a chosen price) or
29
+ * neither (null / absent). A ticked line (`use`) needs both. Plain words, or null.
30
+ * @param {unknown} list
31
+ * @returns {string|null}
32
+ */
33
+ export function modelsProblem(list: unknown): string | null;
19
34
  /**
20
35
  * Apply a patch and write. `null` deletes a field. Top-level fields shallow-merge;
21
- * `keys` merges per key variable name, then per field. Validated before anything is
22
- * written; nothing is written on a refusal.
36
+ * `keys` merges per key variable name, then per field; `models.<ENV>` is REPLACED WHOLE (`null`
37
+ * clears it). Validated before anything is written; nothing is written on a refusal.
23
38
  *
24
- * Patch fields understood: `monthlyLimitUsd` (finite > 0), `anthropicBalanceNote`
25
- * (finite >= 0), `keys.<ENV NAME>.{name,shape,baseUrl}` (Name = a model id, shape = a provider table name, baseUrl = blank or http(s)). Any other
26
- * field is passed through untouched (kept), but the whole document is swept for
27
- * secret-shaped strings first.
39
+ * Patch fields understood: `monthlyLimitUsd` (finite > 0), `keys.<ENV>.{shape,baseUrl,legacyMaxTokens}`,
40
+ * `models.<ENV>` (a list judged by {@link modelsProblem}). The first write of a row's `models` list
41
+ * removes that row's older `name` / `priceInPerM` / `priceOutPerM` in the SAME write (never two
42
+ * sources); a retired top-level `anthropicBalanceNote` is dropped on any write. Any other field is
43
+ * kept, but the whole document is swept for secret-shaped strings first.
28
44
  * @param {Record<string, any>} patch
29
45
  * @param {{ home?: string }} [opts]
30
46
  * @returns {Record<string, any>} the document as written
@@ -35,6 +51,7 @@ export function updateConfig(patch: Record<string, any>, opts?: {
35
51
  /** A plain-words config failure (bad value, secret-shaped string, unreadable file). */
36
52
  export class ConfigError extends Error {
37
53
  }
54
+ export function cleanBaseUrl(v: string): string;
38
55
  export type ConfigRead = {
39
56
  /**
40
57
  * the parsed document ({} when missing/unreadable)
@@ -2,10 +2,10 @@
2
2
  * One line of the drafting log. `no` = the step's position in the session's progress list (a step id can repeat).
3
3
  * - `step-start` {no, id, label, at}
4
4
  * - `step-end` {no, status: 'done'|'failed', label, at} (a re-opened step writes `step-reopen` {no, at} first)
5
- * - `step-label` {no, label, at} (the step reads `label` from now on, status and times untouched: the thinking row's "the model thought anyway")
6
- * - `call` {no, step, label, model, thinking, thinkingApplied?, costUsd (null stays null), unpricedRounds, at}: one METERED model call
7
- * (`thinking` = what was asked and sent: off | on | not-switchable | unverified; `thinkingApplied` = bare-agent's own verdict for the
8
- * call's run (true | false | null), present only when thinkingMode was sent)
5
+ * - `step-label` {no, label, at} (the step reads `label` from now on, status and times untouched: the thinking row's "the model thought anyway" / "can't change it" / "not confirmed")
6
+ * - `call` {no, step, label, model, thinking, thinkingSupport, thinkingStatus?, costUsd (null stays null), unpricedRounds, at}: one METERED model call
7
+ * (`thinking` = the tick's mode: off | on; `thinkingSupport` = bare-agent's lookup for the model: true | false | 'unverified';
8
+ * `thinkingStatus` = bare-agent's own verdict for the call's run: applied | unsupported | ignored | unknown, present only when thinkingMode was sent)
9
9
  * (no token counts: the call path does not carry them without reworking the library, so none are recorded)
10
10
  * @typedef {{kind: string, no: number, at: string, [k: string]: any}} DraftEvent
11
11
  */
@@ -108,10 +108,10 @@ export const DRAFT_LOG_FILE: "draft-log.jsonl";
108
108
  * One line of the drafting log. `no` = the step's position in the session's progress list (a step id can repeat).
109
109
  * - `step-start` {no, id, label, at}
110
110
  * - `step-end` {no, status: 'done'|'failed', label, at} (a re-opened step writes `step-reopen` {no, at} first)
111
- * - `step-label` {no, label, at} (the step reads `label` from now on, status and times untouched: the thinking row's "the model thought anyway")
112
- * - `call` {no, step, label, model, thinking, thinkingApplied?, costUsd (null stays null), unpricedRounds, at}: one METERED model call
113
- * (`thinking` = what was asked and sent: off | on | not-switchable | unverified; `thinkingApplied` = bare-agent's own verdict for the
114
- * call's run (true | false | null), present only when thinkingMode was sent)
111
+ * - `step-label` {no, label, at} (the step reads `label` from now on, status and times untouched: the thinking row's "the model thought anyway" / "can't change it" / "not confirmed")
112
+ * - `call` {no, step, label, model, thinking, thinkingSupport, thinkingStatus?, costUsd (null stays null), unpricedRounds, at}: one METERED model call
113
+ * (`thinking` = the tick's mode: off | on; `thinkingSupport` = bare-agent's lookup for the model: true | false | 'unverified';
114
+ * `thinkingStatus` = bare-agent's own verdict for the call's run: applied | unsupported | ignored | unknown, present only when thinkingMode was sent)
115
115
  * (no token counts: the call path does not carry them without reworking the library, so none are recorded)
116
116
  */
117
117
  export type DraftEvent = {
package/types/index.d.ts CHANGED
@@ -15,7 +15,7 @@ export { validateEnvelope, resolveTrySpec, resolveReuse, reuseSpecHash, selectBr
15
15
  export { classifyIncidents, foldLedger, ledgerDeltas, updateLedger, LEDGER_CLASSES, VOUCHED_RATE_SOURCES, rateProvenance, spendProvenance } from "./ledger.js";
16
16
  export { readShimArm, READ_SHIM_CAP, READ_SHIM_STRATEGY, READ_SHIM_DIFF_STRATEGY } from "./readshim.js";
17
17
  export { runDeclaredClose, runStage, seedRead, seedAtHead, LIVE_KINDS, STOP_FAULTS, JUDGED_MARKER, HUMAN_DECISIONS, SEED_EXEMPT_KINDS, normalizeHumanRuling, resolveHumanRuling, JUDGE_ATTEMPTS, MAX_JUDGED_PATHS, DOOR_OPEN_OUTCOMES, REVIEW_DOOR_CLASSES, doorOpens, mechanicalStages } from "./kinds.js";
18
- export { JUDGE_MODEL, resolveJudge, JUDGE_RULES, JUDGE_RULE_IDS, runLocate, decide, CALIBRATION_SIZE, validateCalibrationSet, expectedOf } from "./judged.js";
18
+ export { resolveJudge, JUDGE_RULES, JUDGE_RULE_IDS, runLocate, decide, CALIBRATION_SIZE, validateCalibrationSet, expectedOf } from "./judged.js";
19
19
  export { proposeJudgedArtifacts, signJudgedArtifacts, foldJudgedArtifacts } from "./cardauthor.js";
20
20
  export { runCalibration, INJECTION_LOCATE_BATTERY, CASUALTY_AXES } from "./calibrate.js";
21
21
  export { validateDeclaration, KIND_CATALOGUE, LOCKED_KINDS, genreEnv, genreInstruments, LOCKED_CLASSES, CLASS_BATTERIES, classGuards, closeCeiling, NEVER_OFFERED_KINDS } from "./authoring.js";
package/types/job.d.ts CHANGED
@@ -122,18 +122,18 @@ export function checkApproval(job: object, approvals: unknown): boolean;
122
122
  * mismatched flag is a mistake worth stopping on, never a silent swap); the
123
123
  * same id from both is not a conflict. Extracted from scripts/run-u.mjs so
124
124
  * the decision is testable without running the script or touching a provider.
125
- * @param {{ specModel?: string, flagModel?: string, defaultModel: string }} args
125
+ * @param {{ specModel?: string, flagModel?: string, suppliedModel?: string }} args
126
126
  * `specModel` — `job.model` if the signed spec carries one, else undefined.
127
127
  * `flagModel` — the model id an explicit `--model <tier>` resolved to, else
128
128
  * undefined (a flag that was never passed, not merely a default tier name).
129
- * `defaultModel` — the runner's own default when neither spec nor flag says.
129
+ * `suppliedModel` — a model the CALLER supplies when neither spec nor flag says (bareloop keeps none of its own).
130
130
  * @returns {{ model: string, source: 'spec'|'flag'|'default' }}
131
- * @throws {Error} when the spec names a model and the flag names a different one
131
+ * @throws {Error} when the spec names a model and the flag names a different one, or when no model is named at all
132
132
  */
133
- export function resolveWorkerModel({ specModel, flagModel, defaultModel }: {
133
+ export function resolveWorkerModel({ specModel, flagModel, suppliedModel }: {
134
134
  specModel?: string;
135
135
  flagModel?: string;
136
- defaultModel: string;
136
+ suppliedModel?: string;
137
137
  }): {
138
138
  model: string;
139
139
  source: "spec" | "flag" | "default";
package/types/judged.d.ts CHANGED
@@ -17,7 +17,7 @@
17
17
  * item 31.4 a green job runs on its own key alone, and this is what lets a
18
18
  * JUDGED one do the same.
19
19
  *
20
- * There is deliberately NO third fallback to `JUDGE_MODEL`. A caller that cannot
20
+ * There is deliberately NO third fallback to a library-pinned model. A caller that cannot
21
21
  * say which model will grade has not wired a judge, and the grading path stops
22
22
  * as a wiring gap rather than grading under an identity nobody chose — the same
23
23
  * answer an absent `judgeLoop` already gets. A silent default here would be the
@@ -79,7 +79,7 @@ export function resolveJudge({ specJudge, workerProvider, workerModel }?: {
79
79
  *
80
80
  * @param {{provider?: string, model?: string, judge?: any}} source draft or spec
81
81
  * @param {string} fallbackProviderName used only when `source.provider` is absent
82
- * @param {(o: {specModel?: string, flagModel?: string, defaultModel: string}) => {model: string, source: string}} resolveWorkerModelFn `resolveWorkerModel` from `./job.js`
82
+ * @param {(o: {specModel?: string, flagModel?: string, suppliedModel?: string}) => {model: string, source: string}} resolveWorkerModelFn `resolveWorkerModel` from `./job.js`
83
83
  * @returns {{provider: string, model: string}}
84
84
  */
85
85
  export function resolveJobJudge(source: {
@@ -89,7 +89,7 @@ export function resolveJobJudge(source: {
89
89
  }, fallbackProviderName: string, resolveWorkerModelFn: (o: {
90
90
  specModel?: string;
91
91
  flagModel?: string;
92
- defaultModel: string;
92
+ suppliedModel?: string;
93
93
  }) => {
94
94
  model: string;
95
95
  source: string;
@@ -309,38 +309,6 @@ export function decide(facts: any, card: any, { artifactText }?: {
309
309
  firstRed: string | null;
310
310
  reason: string | null;
311
311
  };
312
- /**
313
- * THE JUDGE'S HISTORICAL PIN. Until PRD item 32 (2026-09-10) this constant WAS
314
- * the judge: every grading path imported it, so bareloop could run a worker on
315
- * any provider and still judge only on Claude.
316
- *
317
- * It is no longer read by any grading path. The judge identity is now RESOLVED
318
- * per job (`resolveJudge` below) and travels as data — `ctx.judgeModel` into
319
- * `runJudgedFloor`, an explicit argument into the calibration gate, and
320
- * `closeDecl.calibration.judgeModel` into the signed spec, exactly as before.
321
- * Keeping the export is deliberate: it is part of the public API
322
- * (`src/index.js`) and an adopter importing it should not break on a minor.
323
- * What it means now is "the judge bareloop pinned before item 32", which is
324
- * still the right DEFAULT for an anthropic-api job and still the only tier with
325
- * established injection resistance upstream (BA-20's `INJECTION_BATTERY` is
326
- * haiku-4.5 only).
327
- *
328
- * THE ENFORCEMENT IS UNCHANGED, and still lives in three wired places because a
329
- * rule with no detector is prose (F45) — only the value being compared moved
330
- * from this constant to the resolved identity:
331
- * 1. the identity is STORED with the set it graded — `foldJudgedArtifacts`
332
- * writes it as `closeDecl.calibration.judgeModel`, inside the spec, so
333
- * `jobSpecHash` covers it by construction. Change the judge and every
334
- * signed judged spec hashes differently: the signature dies, and re-signing
335
- * is what re-runs the calibration gate.
336
- * 2. `validateCloseDecl` REQUIRES it on any stored set — a set nobody can
337
- * attribute to a judge is a floor nobody can tell apart from a moved one.
338
- * 3. `runJudgedFloor` refuses to grade when the stored identity is not the one
339
- * about to grade, naming both. It does not degrade in either direction:
340
- * grading on the new judge judges against a floor nobody calibrated, and
341
- * the old judge is not on offer.
342
- */
343
- export const JUDGE_MODEL: "claude-haiku-4-5";
344
312
  /**
345
313
  * The locate call's token ceiling. The POC's two real artifacts came back well
346
314
  * inside 2000; this doubles that for headroom on a longer artifact, and the
@@ -402,13 +370,14 @@ export const JUDGE_RULE_IDS: readonly string[];
402
370
  export const CALIBRATION_SIZE: 10;
403
371
  /** the two verdicts a case may expect — `decide()`'s own closed set */
404
372
  export const CASE_VERDICTS: readonly string[];
405
- export function defaultJudgeLoop({ provider, system, rates }: {
373
+ export function defaultJudgeLoop({ provider, system, rates, thinking }: {
406
374
  provider: any;
407
375
  system: string;
408
376
  rates?: {
409
377
  in: number;
410
378
  out: number;
411
379
  } | null;
380
+ thinking?: boolean;
412
381
  }): {
413
382
  run: (msgs: any[], tools: any[], opts?: any) => Promise<any>;
414
383
  };
@@ -25,19 +25,20 @@ export function parseKeysText(text: string): Record<string, string>;
25
25
  */
26
26
  export function filledKeyNames(home?: string): string[];
27
27
  /**
28
- * Create the keys file with the five empty preset lines when — and only when — it does not
29
- * exist (mode 600). An existing file is never touched.
30
- * @param {readonly string[]} presetNames the names to seed, in order
28
+ * Create the keys file with {@link KEYS_FILE_BODY} when — and only when — it does not
29
+ * exist (mode 600, dir 700). An existing file is never touched.
31
30
  * @param {string} [home]
32
31
  * @returns {boolean} true if it was created
33
32
  */
34
- export function ensureKeysFile(presetNames: readonly string[], home?: string): boolean;
33
+ export function ensureKeysFile(home?: string): boolean;
35
34
  /**
36
35
  * @typedef {object} KeysLoad
37
36
  * @property {boolean} exists the file is there
38
37
  * @property {string[]} names the NAMES the file defines (sorted; never values)
39
38
  * @property {string|null} warning a plain-words warning (file readable by others, or
40
39
  * unreadable), else null — a warning, never a refusal
40
+ * @property {boolean} [unreadable] true only when the file is there but could not be read (the Settings row then
41
+ * reads "refused — see above"); absent otherwise
41
42
  * @property {Record<string,string|undefined>} env shell env with the file filling the
42
43
  * names the shell does not set. THE ONLY place values leave this module.
43
44
  */
@@ -68,6 +69,11 @@ export function keysForDoor(deps: {
68
69
  env: Record<string, string | undefined>;
69
70
  home: string | undefined;
70
71
  };
72
+ /**
73
+ * The two comment lines a fresh keys file is created with — no provider name, no preset key.
74
+ * Each key the person types becomes a row in Settings > Providers.
75
+ */
76
+ export const KEYS_FILE_BODY: string;
71
77
  export type KeysLoad = {
72
78
  /**
73
79
  * the file is there
@@ -82,6 +88,11 @@ export type KeysLoad = {
82
88
  * unreadable), else null — a warning, never a refusal
83
89
  */
84
90
  warning: string | null;
91
+ /**
92
+ * true only when the file is there but could not be read (the Settings row then
93
+ * reads "refused — see above"); absent otherwise
94
+ */
95
+ unreadable?: boolean | undefined;
85
96
  /**
86
97
  * shell env with the file filling the
87
98
  * names the shell does not set. THE ONLY place values leave this module.