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
@@ -2,7 +2,7 @@
2
2
  // `renderJobPlan()` draws in two places — the drafting confirm card (the plan bubble) and the read-only Job tab. The
3
3
  // server builds it; the page only draws it (so the two places can never read the same plan two ways).
4
4
  //
5
- // { lines: [{n, text, rule, checks: [{name, cls}], refused: [reason...]}],
5
+ // { lines: [{n, text, rule, rules: [string...], checks: [{name, cls}], refused: [reason...]}],
6
6
  // loose: [{name, cls}], checks that name no job line (an older caller, or a model slip)
7
7
  // notChecked: [text...], "the AI's own reading" — what the drafter says nothing checks
8
8
  // alwaysOn: [text...], the mandatory guards / protections, which serve no job line
@@ -11,9 +11,10 @@
11
11
  // `cls` is 'machine check' | 'judge' | null (null = not known yet: the confirm card's checks are the model's plan, the
12
12
  // stage kinds exist only after drafting). Pure; no file IO.
13
13
  import { KIND_CATALOGUE } from '../authoring.js';
14
+ import { checksOfClose, proseNotChecked, lineRules } from '../rubricstep.js';
14
15
 
15
16
  /** @typedef {{name: string, cls: 'machine check'|'judge'|null}} PlanCheck */
16
- /** @typedef {{n: number, text: string, rule: string, checks: PlanCheck[], refused: string[]}} PlanLine */
17
+ /** @typedef {{n: number, text: string, rule: string, rules: string[], checks: PlanCheck[], refused: string[]}} PlanLine */
17
18
  /** @typedef {{lines: PlanLine[], loose: PlanCheck[], notChecked: string[], alwaysOn: string[], older: boolean, goal?: string}} JobPlan */
18
19
 
19
20
  /**
@@ -29,9 +30,19 @@ export function checkClassOfKind(kind) {
29
30
  return spec.verdictClass === 'green' ? 'machine check' : 'judge';
30
31
  }
31
32
 
33
+ /**
34
+ * The `~` rules of one job line, each on its own (the page draws one `~` row per rule): the line's own `rules` array when it
35
+ * has one (a rule may itself contain "; "), else the joined `rule` split on "; " (an older line that saved only the join).
36
+ * @param {any} l @returns {string[]}
37
+ */
38
+ export function rulesOfLine(l) {
39
+ if (Array.isArray(l?.rules)) return l.rules.filter((/** @type {unknown} */ r) => typeof r === 'string' && r);
40
+ return typeof l?.rule === 'string' && l.rule ? l.rule.split('; ').filter(Boolean) : [];
41
+ }
42
+
32
43
  /**
33
44
  * The plan of a SIGNED spec. With `jobLines`, each stage lands under EACH line its `fromLine` array names (a stage with none is
34
- * a mandatory guard: "Always on"), `closeDecl.refused` reasons land under their line, and `closeDecl.notes` are the
45
+ * a mandatory guard: "Always checked"), `closeDecl.refused` reasons land under their line, and `closeDecl.notes` are the
35
46
  * drafter's own "not checked" words. Without `jobLines` (a spec that predates the job block) the signed goal sentence is
36
47
  * line 1 and every stage name is a check under it — `older: true`.
37
48
  * @param {any} spec
@@ -42,24 +53,34 @@ export function planFromSpec(spec) {
42
53
  let stages = [];
43
54
  if (spec && Array.isArray(spec.closeDecl?.stages)) {
44
55
  stages = spec.closeDecl.stages.filter((/** @type {any} */ s) => s && typeof s.name === 'string' && s.name)
45
- .map((/** @type {any} */ s) => ({ name: s.name, cls: checkClassOfKind(s.kind), fromLine: Array.isArray(s.fromLine) && s.fromLine.length ? s.fromLine.filter(Number.isInteger) : null }));
56
+ .flatMap((/** @type {any} */ s) => {
57
+ const fromLine = Array.isArray(s.fromLine) && s.fromLine.length ? s.fromLine.filter(Number.isInteger) : null;
58
+ // PART 1b: a rubric-step stage is shown as the checks it RUNS — one code-worded row per fact, per count, per limit — read by
59
+ // the same reader (`checksOfClose`) the confirm card and the sign-time invariant use, so the Job tab cannot differ from them
60
+ const rows = s.kind === 'rubric-step' ? checksOfClose({ stages: [s] }) : [];
61
+ return rows.length > 0 ? rows.map((r) => ({ name: r.text, cls: /** @type {'machine check'} */ ('machine check'), fromLine })) : [{ name: s.name, cls: checkClassOfKind(s.kind), fromLine }];
62
+ });
46
63
  } else if (spec && Array.isArray(spec.close)) {
47
64
  stages = spec.close.filter((/** @type {any} */ s) => s && typeof s.name === 'string' && s.name)
48
65
  .map((/** @type {any} */ s) => ({ name: s.name, cls: /** @type {'machine check'} */ ('machine check'), fromLine: null }));
49
66
  }
50
- const notes = Array.isArray(spec?.closeDecl?.notes) ? spec.closeDecl.notes.filter((/** @type {any} */ n) => typeof n === 'string' && n) : [];
67
+ // a prose step says what it does not check (PART 1b, RF5): the person judges the text at Accept
68
+ const notes = [
69
+ ...(Array.isArray(spec?.closeDecl?.notes) ? spec.closeDecl.notes.filter((/** @type {any} */ n) => typeof n === 'string' && n) : []),
70
+ ...proseNotChecked(spec?.closeDecl),
71
+ ];
51
72
  const jl = Array.isArray(spec?.jobLines) ? spec.jobLines.filter((/** @type {any} */ l) => l && Number.isInteger(l.n) && typeof l.text === 'string') : [];
52
73
  if (jl.length === 0) {
53
74
  return {
54
75
  lines: [{
55
- n: 1, text: typeof spec?.goal === 'string' && spec.goal ? spec.goal : '(no goal recorded)', rule: '',
76
+ n: 1, text: typeof spec?.goal === 'string' && spec.goal ? spec.goal : '(no goal recorded)', rule: '', rules: [],
56
77
  checks: stages.map((s) => ({ name: s.name, cls: s.cls })), refused: [],
57
78
  }],
58
79
  loose: [], notChecked: notes, alwaysOn: [], older: true,
59
80
  };
60
81
  }
61
82
  /** @type {PlanLine[]} */
62
- const lines = jl.map((/** @type {any} */ l) => ({ n: l.n, text: l.text, rule: typeof l.rule === 'string' ? l.rule : '', checks: [], refused: [] }));
83
+ const lines = jl.map((/** @type {any} */ l) => ({ n: l.n, text: l.text, rule: typeof l.rule === 'string' ? l.rule : '', rules: rulesOfLine(l), checks: [], refused: [] }));
63
84
  /** @type {PlanCheck[]} */
64
85
  const loose = [];
65
86
  /** @type {string[]} */
@@ -74,7 +95,9 @@ export function planFromSpec(spec) {
74
95
  const refused = Array.isArray(spec?.closeDecl?.refused) ? spec.closeDecl.refused : [];
75
96
  for (const r of refused) {
76
97
  const line = lines.find((l) => l.n === r?.line);
77
- if (line && typeof r.reason === 'string') line.refused.push(r.reason);
98
+ // a refusal of ONE ~ rule (PART 1b, RF16) names the rule in the person's words, so the row reads as that rule's Not checked
99
+ const ruleText = line && Number.isInteger(r.rule) ? lineRules(spec.jobLines.find((/** @type {any} */ l) => l?.n === r.line))[r.rule - 1] : null;
100
+ if (line && typeof r.reason === 'string') line.refused.push(ruleText ? `"${ruleText}" — ${r.reason}` : r.reason);
78
101
  }
79
102
  // the model's signed goal sentence rides along: the page draws it as the "AI's summary" row (an older job shows it as line 1 instead)
80
103
  return { lines, loose, notChecked: notes, alwaysOn, older: false, ...(typeof spec?.goal === 'string' && spec.goal ? { goal: spec.goal } : {}) };
@@ -82,8 +105,8 @@ export function planFromSpec(spec) {
82
105
 
83
106
  /**
84
107
  * The plan of the confirm turn, before anything is drafted: the person's own job lines, the model's checks under the
85
- * line each names, its own "not checked" words, and the real protections (code-owned) as "Always on".
86
- * @param {{n: number, text: string, rule: string}[]|null|undefined} jobLines
108
+ * line each names, its own "not checked" words, and the real protections (code-owned) as "Always checked".
109
+ * @param {{n: number, text: string, rule: string, rules?: string[]}[]|null|undefined} jobLines
87
110
  * @param {{checkItems?: {text: string, fromLine: number[]|null}[], checks?: string[], notChecked?: string[], protections?: string[]}} plan
88
111
  * @returns {JobPlan}
89
112
  */
@@ -93,7 +116,7 @@ export function planFromConfirm(jobLines, plan) {
93
116
  : (Array.isArray(plan?.checks) ? plan.checks.map((c) => ({ text: String(c), fromLine: null })) : []);
94
117
  const jl = Array.isArray(jobLines) ? jobLines : [];
95
118
  /** @type {PlanLine[]} */
96
- const lines = jl.map((l) => ({ n: l.n, text: l.text, rule: l.rule, checks: [], refused: [] }));
119
+ const lines = jl.map((l) => ({ n: l.n, text: l.text, rule: l.rule, rules: rulesOfLine(l), checks: [], refused: [] }));
97
120
  /** @type {PlanCheck[]} */
98
121
  const loose = [];
99
122
  for (const c of items) {
@@ -175,6 +175,9 @@ export function createRunRoutes(opts) {
175
175
  '--wait', 'systemd-inhibit', '--why=bareloop panel resume',
176
176
  process.execPath, bareloopBin,
177
177
  'run-u', '--spec', specPath, '--resume', m[1], '--approve', specHash,
178
+ // an older spec that names no model runs on the Settings line the person picks at resume (hash untouched, run-u's own rule)
179
+ ...(typeof body?.model === 'string' && body.model !== '' ? ['--model', body.model] : []),
180
+ ...(typeof body?.provider === 'string' && body.provider !== '' ? ['--provider', body.provider] : []),
178
181
  // the drafting fold rides every leg of the chain (a leg that dropped it would widen the ceiling)
179
182
  ...(plan.draftSpentUsd !== null ? ['--draft-spent-usd', String(plan.draftSpentUsd), ...(plan.draftSpendComplete === false ? ['--draft-spend-incomplete'] : [])] : []),
180
183
  ];
@@ -49,7 +49,8 @@ import { parseImportedRunId, safeSpinePath, spineStartedAt } from './importrun.j
49
49
  import { checkBundleDeps, resolveBundleSpec } from '../bundle.js';
50
50
  import { checkCloseByteSignature } from '../close-integrity.js';
51
51
  import { keysHome } from '../keysfile.js';
52
- import { rowsForHome, findRow, modelChoiceFor } from '../providerrows.js';
52
+ import { rowsForHome, findRow, modelChoiceFor, tickedLines, endpointOf } from '../providerrows.js';
53
+ import { SHAPES } from '../providershapes.js';
53
54
  import { resolveProvider } from '../providers.js';
54
55
  import { REUSE_LOCKED_FIELDS, REUSE_OPEN_FIELDS } from './authorsession.js';
55
56
  import { readResume, checkpointAgeGate, CHECKPOINT_OUTCOMES } from '../reuse.js';
@@ -59,6 +60,16 @@ import { planFromSpec } from './jobplan.js';
59
60
 
60
61
  const HERE = dirname(fileURLToPath(import.meta.url));
61
62
 
63
+ /** bareloop's own version, from its package.json (read each time the page is served); 'unknown' if it cannot be read, never a guess
64
+ * @param {string} [file] test seam: another package.json
65
+ * @returns {string} */
66
+ function packageVersion(file = join(HERE, '..', '..', 'package.json')) {
67
+ try {
68
+ const v = JSON.parse(readFileSync(file, 'utf8')).version;
69
+ return typeof v === 'string' && v.length > 0 ? v : 'unknown';
70
+ } catch { return 'unknown'; }
71
+ }
72
+
62
73
  /** Largest POST body the panel buffers (job cards are short prose); a bigger one is refused with 413. */
63
74
  const MAX_BODY_BYTES = 1024 * 1024;
64
75
 
@@ -1809,6 +1820,7 @@ function resolveSpecForRow(row) {
1809
1820
  * change any file?"
1810
1821
  * - `pattern-absent-in-diff` is always the `no-suppressions` guard: "no
1811
1822
  * casts or silencers added?"
1823
+ * - `rubric-step` (Part 1a) is one job line's output file: "line N — does its output pass the line's checks?"
1812
1824
  * - `command-exit` is the genre's `suite-green` stage (the only shipped use
1813
1825
  * of this kind): "does the test suite pass?"
1814
1826
  * - `count-not-worse` splits three ways on its own declared
@@ -1876,6 +1888,8 @@ function stageQuestionText({
1876
1888
  if (kind === 'files-changed') return 'did it change any file?';
1877
1889
  if (kind === 'pattern-absent-in-diff') return 'no casts or silencers added?';
1878
1890
  if (kind === 'command-exit') return 'does the test suite pass?';
1891
+ // Part 1a: one rubric-step stage = one job line's output file
1892
+ if (kind === 'rubric-step') return params && Number.isInteger(params.step) ? `line ${params.step} — does its output pass the line's checks?` : null;
1879
1893
  if (kind !== 'count-not-worse' || !params || typeof params !== 'object') return null;
1880
1894
  const what = Object.hasOwn(GENRE_WHAT, String(genre)) ? GENRE_WHAT[String(genre)] : null;
1881
1895
  if (params.direction === 'higher-is-better' && params.baseline === 'seed') {
@@ -2374,28 +2388,38 @@ function signedSpecForRun(row, records) {
2374
2388
  }
2375
2389
 
2376
2390
  /**
2377
- * The worker a signed spec ran: provider, baseUrl and model, the model falling back to the provider's default tier
2378
- * when the spec names none (the same fallback `resolveWorkerModel` makes at run time).
2391
+ * The worker a signed spec ran: provider, baseUrl and model ('' when an older spec names none: bareloop keeps no default).
2379
2392
  * @param {any} spec
2380
2393
  * @returns {{provider: string, baseUrl?: string, model: string}}
2381
2394
  */
2382
2395
  function workerOfSpec(spec) {
2383
2396
  const provider = typeof spec?.provider === 'string' ? spec.provider : '';
2384
- let model = typeof spec?.model === 'string' ? spec.model : '';
2385
- if (!model) { try { model = resolveProvider(provider).tiers.sonnet; } catch { model = ''; } }
2397
+ const model = typeof spec?.model === 'string' ? spec.model : '';
2386
2398
  return { provider, ...(typeof spec?.baseUrl === 'string' && spec.baseUrl ? { baseUrl: spec.baseUrl } : {}), model };
2387
2399
  }
2388
2400
  /** @param {{provider: string, baseUrl?: string, model: string}} w @returns {string} one spelling of "the same worker" */
2389
2401
  const workerId = (w) => `${w.provider}\u0000${w.baseUrl ?? ''}\u0000${w.model}`;
2402
+ /**
2403
+ * The model id a reuse card's Model box shows for a row: the spec's own model when the row has a line for it, else the
2404
+ * row's first ticked line; `undefined` when there is no row or no such line.
2405
+ * @param {import('../providerrows.js').KeyRow|null} row
2406
+ * @param {string|null|undefined} specModel
2407
+ * @returns {string|undefined}
2408
+ */
2409
+ function rowModelOf(row, specModel) {
2410
+ if (!row) return undefined;
2411
+ if (specModel && row.models.some((l) => l.model === specModel)) return specModel;
2412
+ return tickedLines([row])[0]?.line.model;
2413
+ }
2390
2414
  /**
2391
2415
  * The worker the reuse card's CURRENT Model box names: the Settings row's provider, baseUrl and Name. `null` when the
2392
2416
  * box is blank or names no row.
2393
2417
  * @param {string} model a Settings Name
2394
- * @param {{ home?: string }} opts
2418
+ * @param {{ home?: string, provider?: string }} opts `provider` is the key's env name the card picked
2395
2419
  * @returns {{provider: string, baseUrl?: string, model: string}|null}
2396
2420
  */
2397
2421
  function workerOfChoice(model, opts) {
2398
- const c = modelChoiceFor(rowsForHome(keysHome(opts.home)), model);
2422
+ const c = modelChoiceFor(rowsForHome(keysHome(opts.home)), { provider: opts.provider, model });
2399
2423
  return c ? { provider: c.provider, ...(c.baseUrl ? { baseUrl: c.baseUrl } : {}), model: c.name } : null;
2400
2424
  }
2401
2425
 
@@ -2478,10 +2502,11 @@ export function reuseLine(record) {
2478
2502
  * $ cap and the Time cap — start at what the run had. `ok:false` when the run's signed job is not on disk (nothing
2479
2503
  * to copy: change the job: Clear the card). `null` when the runid is not listed.
2480
2504
  * @param {string} runid
2481
- * @param {{ home?: string, model?: string }} [opts] `model` = the Model box's current Name (the estimate counts only runs on that worker); absent = the card's own Model
2505
+ * @param {{ home?: string, model?: string, provider?: string }} [opts] `model` = the Model box's current Name (the estimate counts only runs on that worker); absent = the card's own Model
2482
2506
  * @returns {{ok: true, origin: {runid: string, job: string}, outcome: string|null, card: Record<string, any>, from: 'signed job',
2483
2507
  * locked: readonly string[], open: readonly string[], note: string|null,
2484
- * specHash: string, workflowKey: string, spec: any, specPath: string,
2508
+ * specHash: string, workflowKey: string, spec: any, specPath: string, specNamesModel: boolean,
2509
+ * specWorker: {provider: string, endpoint: string, label: string},
2485
2510
  * trackRecord: ReturnType<typeof trackRecordFor>, line: string}|{ok: false, error: string}|null}
2486
2511
  */
2487
2512
  export function getStartFrom(runid, opts = {}) {
@@ -2507,7 +2532,7 @@ export function getStartFrom(runid, opts = {}) {
2507
2532
  const jobStart = records.find((r) => r && r.type === 'job-start') ?? null;
2508
2533
  const card = reuseCardFromSpec(spec, {
2509
2534
  model: typeof saved.model === 'string' && saved.model ? saved.model
2510
- : (findRow(rowsForHome(keysHome(opts.home)), { provider: spec.provider, baseUrl: spec.baseUrl, model: spec.model })?.name
2535
+ : (rowModelOf(findRow(rowsForHome(keysHome(opts.home)), { provider: spec.provider, baseUrl: spec.baseUrl, model: spec.model }), spec.model)
2511
2536
  ?? (typeof jobStart?.model === 'string' ? jobStart.model : '')),
2512
2537
  source: typeof saved.source === 'string' && saved.source ? saved.source : (typeof manifest?.source === 'string' ? manifest.source : (typeof row.patient === 'string' ? row.patient : '')),
2513
2538
  // a card saved by P7 or later keeps the Inputs text the person typed
@@ -2515,7 +2540,7 @@ export function getStartFrom(runid, opts = {}) {
2515
2540
  jobName: row.job,
2516
2541
  });
2517
2542
  const key = workflowKey(spec);
2518
- const trackRecord = trackRecordFor(key, { ...opts, worker: workerOfChoice(opts.model ?? card.model, opts) });
2543
+ const trackRecord = trackRecordFor(key, { ...opts, worker: workerOfChoice(opts.model ?? card.model, { ...opts, provider: opts.provider ?? card.provider }) });
2519
2544
  return {
2520
2545
  ok: true,
2521
2546
  origin: { runid, job: row.job },
@@ -2530,6 +2555,14 @@ export function getStartFrom(runid, opts = {}) {
2530
2555
  workflowKey: key,
2531
2556
  spec,
2532
2557
  specPath: signed.specPath,
2558
+ // an older spec signed before models were required names none: the Resume card then opens its Model box (run-u takes `--model`)
2559
+ specNamesModel: typeof spec.model === 'string' && spec.model !== '',
2560
+ // what resolveWorker will hold a picked line to: the spec's shape, and its endpoint when it names one ('' = any endpoint)
2561
+ specWorker: {
2562
+ provider: String(spec.provider),
2563
+ endpoint: typeof spec.baseUrl === 'string' && spec.baseUrl.trim() !== '' ? endpointOf(String(spec.provider), spec.baseUrl) : '',
2564
+ label: SHAPES.find((x) => x.id === spec.provider)?.label ?? String(spec.provider),
2565
+ },
2533
2566
  trackRecord,
2534
2567
  line: reuseLine(trackRecord),
2535
2568
  };
@@ -2540,7 +2573,7 @@ export function getStartFrom(runid, opts = {}) {
2540
2573
  * that has at least one GREEN local run, newest green first. `runid` = that job's newest green run, so the page hands
2541
2574
  * it to the existing start-from flow unchanged; `line` is `getStartFrom`'s own estimate line (never a second
2542
2575
  * spelling). Local runs only — an imported job keeps its Reuse button in the Import view. $0, read-only.
2543
- * @param {{ home?: string, model?: string }} [opts] `model` passes through to the estimate line, as start-from's does
2576
+ * @param {{ home?: string, model?: string, provider?: string }} [opts] `model` passes through to the estimate line, as start-from's does
2544
2577
  * @returns {{job: string, runid: string, checkType: 'deterministic'|'rubric', line: string}[]}
2545
2578
  */
2546
2579
  export function listReuseJobs(opts = {}) {
@@ -2621,7 +2654,7 @@ function describeSpec(spec) {
2621
2654
  * signs it on this machine at [Sign & run]. Same card, same locked/open boxes as a run's reuse. `null` when no such
2622
2655
  * imported job.
2623
2656
  * @param {string} id
2624
- * @param {{ home?: string, model?: string }} [opts] `model` as for {@link getStartFrom}
2657
+ * @param {{ home?: string, model?: string, provider?: string }} [opts] `model` as for {@link getStartFrom}
2625
2658
  * @returns {{ok: true, origin: {runid: null, job: string, importId: string}, card: Record<string, any>, from: 'imported job',
2626
2659
  * locked: readonly string[], open: readonly string[], note: string|null, specHash: string, workflowKey: string, spec: any,
2627
2660
  * specPath: null, trackRecord: ReturnType<typeof trackRecordFor>, line: string}|{ok: false, error: string}|null}
@@ -2640,10 +2673,10 @@ export function getStartFromImport(id, opts = {}) {
2640
2673
  // the bundle's provider is no longer forced: Model is open on a reuse, so the person's chosen Settings row is the worker.
2641
2674
  // The card opens on the row matching the bundle's own provider when there is one, else blank (the page's menu picks).
2642
2675
  const rows = rowsForHome(keysHome(opts.home));
2643
- const named = findRow(rows, { provider: spec.provider, baseUrl: spec.baseUrl, model: spec.model })?.name ?? '';
2676
+ const named = rowModelOf(findRow(rows, { provider: spec.provider, baseUrl: spec.baseUrl, model: spec.model }), spec.model) ?? '';
2644
2677
  const card = reuseCardFromSpec(spec, { model: named, source: '', jobName: row.job });
2645
2678
  const key = workflowKey(spec);
2646
- const trackRecord = trackRecordFor(key, { ...opts, worker: workerOfChoice(opts.model ?? card.model, opts) });
2679
+ const trackRecord = trackRecordFor(key, { ...opts, worker: workerOfChoice(opts.model ?? card.model, { ...opts, provider: opts.provider ?? card.provider }) });
2647
2680
  return {
2648
2681
  ok: true,
2649
2682
  origin: { runid: null, job: row.job, importId: id },
@@ -2690,7 +2723,7 @@ function sendText(res, code, text) {
2690
2723
  * only `createPanelServer` below always supplies one.
2691
2724
  * @param {import('node:http').IncomingMessage} req
2692
2725
  * @param {import('node:http').ServerResponse} res
2693
- * @param {{ home?: string, port: number, token?: string, authorRoutes?: ReturnType<typeof createAuthorRoutes>, settingsRoutes?: ReturnType<typeof createSettingsRoutes>, runRoutes?: ReturnType<typeof createRunRoutes>, importRoutes?: ReturnType<typeof createImportRoutes> }} opts
2726
+ * @param {{ home?: string, packageJsonPath?: string, port: number, token?: string, authorRoutes?: ReturnType<typeof createAuthorRoutes>, settingsRoutes?: ReturnType<typeof createSettingsRoutes>, runRoutes?: ReturnType<typeof createRunRoutes>, importRoutes?: ReturnType<typeof createImportRoutes> }} opts
2694
2727
  */
2695
2728
  export function handleRequest(req, res, opts) {
2696
2729
  /** the ONE writable route family for a path, or null — `/api/author/*` (authoring) or
@@ -2786,6 +2819,9 @@ export function handleRequest(req, res, opts) {
2786
2819
  return;
2787
2820
  }
2788
2821
  html = html.replace(/__BARELOOP_PANEL_PORT__/g, String(opts.port));
2822
+ // the topbar reads `bareloop · v<version> · ...`: the version is bareloop's OWN package.json, read when the page is served
2823
+ // and templated in exactly like the port — never written into the HTML
2824
+ html = html.replace(/__BARELOOP_PANEL_VERSION__/g, packageVersion(opts.packageJsonPath));
2789
2825
  // PANEL-BUILD.md P3's human-click guard: a per-server-start token,
2790
2826
  // templated into the page exactly like the port — absent on every P1/P2
2791
2827
  // caller (no `opts.token`), which is fine: the page's own JS only reads
@@ -2867,7 +2903,7 @@ export function handleRequest(req, res, opts) {
2867
2903
  * @param {{ port?: number, home?: string, env?: Record<string,string|undefined>,
2868
2904
  * sessionsRoot?: string, spawnFn?: (...a: any[]) => any, bareloopBin?: string,
2869
2905
  * fetchImpl?: typeof fetch, settleMs?: number, userHome?: string,
2870
- * openFolderSpawn?: (cmd: string, args: string[], o: object) => any, platform?: string }} [opts]
2906
+ * openFolderSpawn?: (cmd: string, args: string[], o: object) => any, platform?: string, packageJsonPath?: string }} [opts]
2871
2907
  * @returns {Promise<{ server: import('node:http').Server, port: number, token: string, close: () => Promise<void> }>}
2872
2908
  */
2873
2909
  export function createPanelServer(opts = {}) {
@@ -2892,7 +2928,7 @@ export function createPanelServer(opts = {}) {
2892
2928
  const server = createServer((req, res) => {
2893
2929
  try {
2894
2930
  handleRequest(req, res, {
2895
- home, port: boundPort, token, authorRoutes, settingsRoutes, runRoutes, importRoutes,
2931
+ home, port: boundPort, token, authorRoutes, settingsRoutes, runRoutes, importRoutes, packageJsonPath: opts.packageJsonPath,
2896
2932
  });
2897
2933
  } catch (e) {
2898
2934
  sendText(res, 500, `internal error: ${/** @type {Error} */ (e).message}`);