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
@@ -20,7 +20,7 @@
20
20
  // `src/panel/server.js`'s POST routes). Nothing here changes what a step
21
21
  // means or what an answer does — only where the answer comes from.
22
22
 
23
- import { applyConfiguredKey, judgeRatesFor, keyNameFor, modelChoiceFor, ratesFor, rowsForHome } from '../providerrows.js';
23
+ import { judgePick, judgeRatesFor, judgeRowFor, keyValueOf, modelChoiceFor, ratesOfLine, rowsForHome } from '../providerrows.js';
24
24
  import { ConfigError } from '../config.js';
25
25
  import {
26
26
  mkdirSync, existsSync, writeFileSync, readdirSync,
@@ -45,9 +45,10 @@ import {
45
45
  questionsFor, requiredAnswersFor, makeLoopGenerate, CONFIRM_SYSTEM, SOFTGREEN_JUDGE_EXAMPLES_KEY,
46
46
  } from '../authorflow.js';
47
47
  import {
48
- resolveProvider, buildRunnerProviders, apiKeyProblem, assertDraftThinking, draftThinkingFor,
48
+ resolveProvider, buildRunnerProviders, apiKeyProblem,
49
49
  } from '../providers.js';
50
50
  import { resolveJobJudge, defaultJudgeLoop } from '../judged.js';
51
+ import { assertThinkingMode, thinkingPlanFor, thinkingRowText } from '../thinking.js';
51
52
  import { closeJudges } from '../kinds.js';
52
53
  import { redactSecrets } from '../validate.js';
53
54
  import { tallyCalls } from '../text.js';
@@ -110,19 +111,7 @@ export const PHASE_STEP = Object.freeze({
110
111
  'rubric-scrubbed': 'calibrate',
111
112
  });
112
113
 
113
- /**
114
- * The first drafting row's words (hamr 2026-10-11) — code-owned, one owner, ALWAYS starting "thinking on" or "thinking off".
115
- * `sent` is what the drafting calls carry (`draftThinkingFor`); `thoughtAnyway` = a call reported `thinkingApplied === false`
116
- * after 'off' was sent (bare-agent saw reasoning in the reply).
117
- * @param {'on'|'off'|'not-switchable'|'unverified'} sent @param {boolean} [thoughtAnyway]
118
- * @returns {string}
119
- */
120
- export function thinkingRowText(sent, thoughtAnyway = false) {
121
- if (sent === 'off') return thoughtAnyway ? 'thinking on (the model thought anyway)' : 'thinking off';
122
- if (sent === 'on') return 'thinking on';
123
- if (sent === 'not-switchable') return "thinking on (this model can't turn it off)";
124
- return 'thinking on (not verified for this model)';
125
- }
114
+ export { thinkingRowText };
126
115
 
127
116
  /** @typedef {{id: string, label: string, status: 'running'|'done'|'failed', detail: string}} StepLine */
128
117
 
@@ -194,7 +183,7 @@ export function jobNameTaken(name, opts = {}) {
194
183
  }
195
184
 
196
185
  /** the form's fields, in card order — what `card.json` stores, verbatim (P5 item 3) */
197
- export const CARD_FIELDS = Object.freeze(['jobName', 'checkType', 'model', 'jobText', 'inputs', 'destination', 'capUsd', 'maxWallMs']);
186
+ export const CARD_FIELDS = Object.freeze(['jobName', 'checkType', 'provider', 'model', 'thinking', 'jobText', 'inputs', 'destination', 'capUsd', 'maxWallMs']);
198
187
 
199
188
  /**
200
189
  * The repo a card names: Inputs line 1 (P7 — the Source box is gone; the first input IS the repo). `''` when the Inputs
@@ -268,13 +257,13 @@ export function buildReuseSpec(originSpec, card, rows) {
268
257
  spec.budgetUsd = card.capUsd;
269
258
  if (typeof card.maxWallMs === 'number' && Number.isFinite(card.maxWallMs) && card.maxWallMs > 0) spec.maxWallMs = card.maxWallMs;
270
259
  else delete spec.maxWallMs;
271
- const choice = rows && typeof card.model === 'string' ? modelChoiceFor(rows, card.model) : null;
260
+ const choice = rows && typeof card.model === 'string' ? modelChoiceFor(rows, { provider: card.provider, model: card.model }) : null;
272
261
  if (choice) {
273
262
  if (spec.verdictType === 'soft-green' && !spec.judge) spec.judge = resolveJobJudge(originSpec, originSpec.provider ?? choice.provider, resolveWorkerModel);
274
- const entry = resolveProvider(choice.provider);
263
+ resolveProvider(choice.provider);
275
264
  spec.provider = choice.provider;
276
265
  if (choice.baseUrl) spec.baseUrl = choice.baseUrl; else delete spec.baseUrl;
277
- if (choice.name !== entry.tiers.sonnet) spec.model = choice.name; else delete spec.model;
266
+ spec.model = choice.name; // a job always carries its own model
278
267
  }
279
268
  return spec;
280
269
  }
@@ -289,7 +278,8 @@ export function buildReuseSpec(originSpec, card, rows) {
289
278
  */
290
279
  export function validateReuseCard(card, opts = {}) {
291
280
  if (!card || typeof card !== 'object') return { ok: false, error: 'missing job card' };
292
- if (!modelChoiceFor(opts.rows ?? [], card.model)) return { ok: false, error: 'Model must be one of the Names in Settings > Providers' };
281
+ const pick = judgePick(opts.rows ?? [], { provider: card.provider, model: card.model });
282
+ if (!pick.ok) return { ok: false, error: pick.say };
293
283
  const ip = parseInputs(card.inputs);
294
284
  if (!ip.ok) return { ok: false, error: ip.error };
295
285
  if (typeof card.destination !== 'string' || card.destination.trim() === '') return { ok: false, error: 'Destination is required' };
@@ -332,7 +322,9 @@ export function validateJobCard(card, opts = {}) {
332
322
  return { ok: false, error: `a job named "${jobName}" already exists — P3 is new jobs only (Q4=A)` };
333
323
  }
334
324
  if (checkType !== 'deterministic' && checkType !== 'rubric') return { ok: false, error: 'Check type must be deterministic or rubric' };
335
- if (!modelChoiceFor(opts.rows ?? [], model)) return { ok: false, error: 'Model must be one of the Names in Settings > Providers' };
325
+ const pick = judgePick(opts.rows ?? [], { provider: card.provider, model });
326
+ if (!pick.ok) return { ok: false, error: pick.say };
327
+ if (card.thinking !== undefined && typeof card.thinking !== 'boolean') return { ok: false, error: 'Thinking on must be on or off' };
336
328
  const jb = parseJobBlock(card.jobText);
337
329
  if (!jb.ok) return { ok: false, error: jb.error };
338
330
  const misfit = jobBlockFitsCheck(jb.lines, checkType === 'rubric');
@@ -361,9 +353,9 @@ export function validateJobCard(card, opts = {}) {
361
353
  */
362
354
  export function createSession(card, deps = {}) {
363
355
  const env = deps.env ?? process.env;
364
- // The drafter's THINKING knob ('on' | 'off'): an explicit operator option, never read from env and not in the panel UI. A bad
365
- // value throws here, before a session exists. Default 'off' (hamr ruling 2026-10-11, after the paid ON/OFF contrast).
366
- const draftThinking = assertDraftThinking(deps.draftThinking ?? 'off');
356
+ // The drafter's THINKING mode ('on' | 'off'): the card's "Thinking on" tick (unticked = 'off'), signed into the job only when ticked.
357
+ // `deps.draftThinking` is an operator override for tests/probes; a bad value throws here, before a session exists.
358
+ const draftThinking = assertThinkingMode(deps.draftThinking ?? (card.thinking === true ? 'on' : 'off'));
367
359
  // P7: the person's own words, parsed ONCE by the one parser (src/jobblock.js). The card validator refused a malformed
368
360
  // block before a session exists; a reuse card's job text is the origin's and is not drafted from.
369
361
  const jobParsed = parseJobBlock(card.jobText);
@@ -557,16 +549,22 @@ export function createSession(card, deps = {}) {
557
549
  // the call's label, the drafting model, its cost (null stays null: unpriced is never $0). One owner: this onCall.
558
550
  const runningNo = state.steps.findLastIndex((/** @type {any} */ x) => x.status === 'running');
559
551
  const callNo = runningNo === -1 ? state.steps.length - 1 : runningNo;
560
- // bare-agent's own verdict on the call's run (true | false | null), present only when thinkingMode was sent. 'off' sent but
561
- // reasoning seen (false) turns the first drafting row into "thinking on (the model thought anyway)" and stays so.
562
- if (draftThinkingLogged === 'off' && call.thinkingApplied === false && !thoughtAnyway) {
563
- thoughtAnyway = true;
552
+ // bare-agent's own verdict on the call's run — `thinkingStatus`: 'applied' | 'unsupported' | 'ignored' | 'unknown' — present only
553
+ // when thinkingMode was sent. The first drafting row then says what REALLY happened ("the model thought anyway", "can't change it",
554
+ // "not confirmed"); a row that moved to a worse word never moves back.
555
+ if (draftThinkingPlan !== null && call.thinkingStatus !== undefined && call.thinkingStatus !== thinkingStatusSeen
556
+ && !(thinkingStatusSeen !== null && thinkingStatusSeen !== 'applied' && call.thinkingStatus === 'applied')) {
557
+ thinkingStatusSeen = call.thinkingStatus;
564
558
  const row = latestStep(state.steps, 'thinking');
565
- if (row) { row.label = thinkingRowText('off', true); appendDraftLog(outDir, { kind: 'step-label', no: state.steps.indexOf(row), label: row.label, at: new Date().toISOString() }); }
559
+ if (row) {
560
+ row.label = thinkingRowText({ mode: draftThinkingPlan.mode, support: draftThinkingPlan.support, status: call.thinkingStatus });
561
+ appendDraftLog(outDir, { kind: 'step-label', no: state.steps.indexOf(row), label: row.label, at: new Date().toISOString() });
562
+ }
566
563
  }
567
564
  appendDraftLog(outDir, {
568
565
  kind: 'call', no: callNo, step: state.steps[callNo]?.id ?? null, label: call.label, model: draftIdentity?.model ?? null,
569
- thinking: draftThinkingLogged, ...(call.thinkingApplied === undefined ? {} : { thinkingApplied: call.thinkingApplied }),
566
+ thinking: draftThinkingPlan === null ? null : draftThinkingPlan.mode, thinkingSupport: draftThinkingPlan === null ? null : draftThinkingPlan.support,
567
+ ...(call.thinkingStatus === undefined ? {} : { thinkingStatus: call.thinkingStatus }),
570
568
  costUsd: call.costUsd ?? null, unpricedRounds: call.unpricedRounds ?? 0, at: nowIso,
571
569
  });
572
570
  try {
@@ -578,10 +576,11 @@ export function createSession(card, deps = {}) {
578
576
  };
579
577
  /** @type {string|null} */
580
578
  let draftStartedAt = null;
581
- /** what the drafting calls really carry for thinking ('on' | 'off' | 'not-switchable' | 'unverified'), set once the Model Name resolves; null before @type {string|null} */
582
- let draftThinkingLogged = null;
583
- /** a drafting call reported thinkingApplied === false after 'off' was sent */
584
- let thoughtAnyway = false;
579
+ /** what the drafting calls will carry for thinking — the tick's mode and bare-agent's support answer — set once the Model resolves; null before
580
+ * @type {ReturnType<typeof thinkingPlanFor>|null} */
581
+ let draftThinkingPlan = null;
582
+ /** the worst-so-far `thinkingStatus` a drafting call reported (null until one did) @type {string|null} */
583
+ let thinkingStatusSeen = null;
585
584
  /** P6 item 1: the worktree this session made in the person's repo (null until made, and again once removed) @type {{repo: string, dir: string}|null} */
586
585
  let madeWorktree = null;
587
586
  /** a session that ends WITHOUT ever running (refused, errored, abandoned) takes its worktree with it; a signed one keeps it for the run */
@@ -607,36 +606,37 @@ export function createSession(card, deps = {}) {
607
606
 
608
607
  async function run() {
609
608
  // ── keys, at $0, before anything is prepared ────────────────────────
610
- const keyRows = rowsForHome(configHome);
611
- const modelChoice = modelChoiceFor(keyRows, card.model);
609
+ const keyRows = rowsForHome(configHome, env);
610
+ const pick = judgePick(keyRows, { provider: card.provider, model: card.model });
611
+ const modelChoice = pick.ok ? pick.choice : null;
612
612
  // hamr 2026-10-11: the FIRST drafting row says whether the drafter thinks, before any other line ("checking setup" follows).
613
613
  // A Reuse session drafts nothing, so it has no row. A provider shape that cannot be built is refused below, in words.
614
614
  if (modelChoice && deps.reuse === undefined) {
615
615
  try {
616
- draftThinkingLogged = draftThinkingFor({ providerName: modelChoice.provider, model: modelChoice.name, baseUrl: modelChoice.baseUrl ?? null }, draftThinking);
617
- state.steps.push({ id: 'thinking', label: thinkingRowText(/** @type {any} */ (draftThinkingLogged)), status: 'done', detail: '' });
616
+ draftThinkingPlan = thinkingPlanFor({ providerName: modelChoice.provider, model: modelChoice.name, baseUrl: modelChoice.baseUrl ?? null }, draftThinking);
617
+ state.steps.push({ id: 'thinking', label: thinkingRowText({ mode: draftThinkingPlan.mode, support: draftThinkingPlan.support }), status: 'done', detail: '' });
618
618
  logSteps();
619
- } catch { draftThinkingLogged = null; }
619
+ } catch { draftThinkingPlan = null; }
620
620
  }
621
621
  stepStart('setup');
622
- if (!modelChoice) { refuse(`Model "${card.model}" is not a Name in Settings > Providers. Stopped — nothing spent.`); return; }
622
+ if (!modelChoice) { refuse(pick.ok ? 'Pick a model. Nothing was spent.' : pick.say); return; }
623
623
  draftIdentity = { provider: modelChoice.provider, baseUrl: modelChoice.baseUrl ?? null, model: modelChoice.name };
624
624
  let providerEntry;
625
625
  try { providerEntry = resolveProvider(modelChoice.provider); } catch (e) { refuse(/** @type {Error} */ (e).message); return; }
626
- // P4a item 4 — the key variable the person picked in Settings stands in for the built-in one
626
+ // the key is the chosen row's own variable (P8: no built-in name)
627
627
  const keyCfg = keyRows;
628
- const draftEnv = applyConfiguredKey(env, modelChoice.provider, modelChoice.baseUrl, keyCfg, modelChoice.name);
629
- const draftKeyName = keyNameFor(modelChoice.provider, modelChoice.baseUrl, keyCfg, modelChoice.name).name;
630
- const apiKey = draftEnv[providerEntry.envKey];
628
+ const draftKeyName = modelChoice.envName;
629
+ const apiKey = keyValueOf(env, draftKeyName);
631
630
  if (!apiKey) { refuse(`${draftKeyName} not set. Stopped — nothing spent.`); return; }
632
631
  // the customer's own price on this row (USD per 1M in config.json), resolved once here at $0 and
633
632
  // handed to every model call this session makes; a bad price refuses, never falls back to the guess
634
- /** @type {ReturnType<typeof ratesFor>} */
633
+ /** @type {import('../providerrows.js').PriceResult} */
635
634
  let draftPrice = null;
636
- try { draftPrice = ratesFor(modelChoice.provider, modelChoice.baseUrl, keyCfg, modelChoice.name); } catch (e) {
637
- if (!(e instanceof ConfigError)) throw e;
638
- refuse(`${e.message}. Stopped — nothing spent.`);
639
- return;
635
+ {
636
+ const chosenRow = keyCfg.find((r) => r.envName === modelChoice.envName);
637
+ const dl = chosenRow?.models.find((l) => l.model === modelChoice.name);
638
+ const dp = dl ? ratesOfLine(dl) : null;
639
+ draftPrice = dp ? { ...dp, envName: modelChoice.envName } : null;
640
640
  }
641
641
  const keyProblem = apiKeyProblem(apiKey);
642
642
  if (keyProblem) { refuse(`${draftKeyName} ${keyProblem}. Stopped — nothing spent.`); return; }
@@ -791,9 +791,10 @@ export function createSession(card, deps = {}) {
791
791
  const judges = closeJudges(spec.closeDecl);
792
792
  const judge = judges ? resolveJobJudge(spec, modelChoice.provider, resolveWorkerModel) : null;
793
793
  let judgeProvider = null;
794
- /** @type {ReturnType<typeof ratesFor>} */
794
+ /** @type {import('../providerrows.js').PriceResult} */
795
795
  let judgePrice = null;
796
796
  if (judge) {
797
+ const judgeRow = judgeRowFor(keyCfg, judge, modelChoice.provider, keyCfg.find((r) => r.envName === modelChoice.envName) ?? null);
797
798
  try {
798
799
  judgePrice = judgeRatesFor(judge, modelChoice.provider, modelChoice.baseUrl, draftPrice, keyCfg);
799
800
  } catch (e) {
@@ -801,22 +802,23 @@ export function createSession(card, deps = {}) {
801
802
  refuse(`${e.message} — refusing before gate 4 spends anything`);
802
803
  return;
803
804
  }
804
- const judgeKeyValue = env.JUDGE_API_KEY ?? (judge.provider === modelChoice.provider ? draftEnv : applyConfiguredKey(env, judge.provider, undefined, keyCfg, judge.model))[resolveProvider(judge.provider).envKey];
805
+ const judgeKeyValue = env.JUDGE_API_KEY ?? (judge.provider === modelChoice.provider ? apiKey : keyValueOf(env, judgeRow?.envName));
805
806
  const judgeKeyProblem = judgeKeyValue ? apiKeyProblem(judgeKeyValue) : null;
806
807
  if (!judgeKeyValue || judgeKeyProblem) {
807
808
  refuse(`the judge key is not usable (${judgeKeyProblem ?? 'not set'}) — refusing before gate 4 spends anything`);
808
809
  return;
809
810
  }
810
811
  judgeProvider = buildRunnerProviders({
811
- providerName: modelChoice.provider, apiKey, model: MODEL, tierModels: providerEntry.tiers, baseUrl: modelChoice.baseUrl,
812
+ providerName: modelChoice.provider, apiKey, model: MODEL, baseUrl: modelChoice.baseUrl, legacyMaxTokens: modelChoice.legacyMaxTokens,
812
813
  judgeApiKey: judgeKeyValue, judgeModel: judge.model, judgeProviderName: judge.provider,
813
- judgeBaseUrl: judge.provider === modelChoice.provider ? modelChoice.baseUrl : undefined,
814
+ judgeBaseUrl: judge.provider === modelChoice.provider ? modelChoice.baseUrl : (judgeRow?.baseUrl.trim() || undefined),
815
+ judgeLegacyMaxTokens: judge.provider === modelChoice.provider ? modelChoice.legacyMaxTokens : (judgeRow?.legacyMaxTokens ?? false),
814
816
  }).judgeProvider;
815
817
  }
816
818
  const signing = await prepareSigningFn({
817
819
  spec, workdir: prep.tree, seedRef: seedRef, timeoutMs,
818
820
  shellCapUsd: spec.budgetUsd, ceilingUsd: card.capUsd, priorCalls: [...metered],
819
- judgeLoop: judgeProvider ? (o) => defaultJudgeLoop({ provider: judgeProvider, system: o.system, rates: judgePrice?.rates ?? null }) : null,
821
+ judgeLoop: judgeProvider ? (o) => defaultJudgeLoop({ provider: judgeProvider, system: o.system, rates: judgePrice?.rates ?? null, thinking: spec.thinking === true }) : null,
820
822
  judgeModel: judge?.model ?? null,
821
823
  onJudgeCost: (c) => onCall({ label: `${c.label}:${c.id}`, costUsd: c.costUsd, unpricedRounds: c.unpricedRounds }),
822
824
  });
@@ -886,8 +888,10 @@ export function createSession(card, deps = {}) {
886
888
  inputs: INPUTS,
887
889
  provider: modelChoice.provider,
888
890
  ...(modelChoice.baseUrl ? { baseUrl: modelChoice.baseUrl } : {}),
889
- // the Name is the model id: signed into the spec only when it is not the provider's own default
890
- ...(modelChoice.name !== providerEntry.tiers.sonnet ? { model: modelChoice.name } : {}),
891
+ // a job always carries its own model (P8: there is no provider default to omit it against)
892
+ model: modelChoice.name,
893
+ // the "Thinking on" tick is signed ONLY when ticked (unticked writes no field: the hash stays what it would have been)
894
+ ...(card.thinking === true && draftThinkingPlan?.support !== false ? { thinking: true } : {}),
891
895
  cadence: { unit: 'day', every: 1 },
892
896
  budgetUsd: card.capUsd,
893
897
  ...(typeof card.maxWallMs === 'number' ? { maxWallMs: card.maxWallMs } : {}),
@@ -911,10 +915,10 @@ export function createSession(card, deps = {}) {
911
915
  }
912
916
 
913
917
  const draftJudge = (() => {
914
- try { return resolveJobJudge(draft, modelChoice.provider, resolveWorkerModel); } catch { return { provider: modelChoice.provider, model: providerEntry.tiers.sonnet }; }
918
+ try { return resolveJobJudge(draft, modelChoice.provider, resolveWorkerModel); } catch { return { provider: modelChoice.provider, model: modelChoice.name }; }
915
919
  })();
916
920
  const { provider } = buildRunnerProviders({
917
- providerName: modelChoice.provider, apiKey, model: MODEL, tierModels: providerEntry.tiers, baseUrl: modelChoice.baseUrl,
921
+ providerName: modelChoice.provider, apiKey, model: MODEL, baseUrl: modelChoice.baseUrl, legacyMaxTokens: modelChoice.legacyMaxTokens,
918
922
  judgeApiKey: apiKey, judgeModel: MODEL, judgeProviderName: modelChoice.provider, judgeBaseUrl: modelChoice.baseUrl,
919
923
  });
920
924
  // TEST SEAMS (see the constructor's own note): a test overrides