bareloop 0.39.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 (76) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/bareloop.context.md +137 -112
  3. package/package.json +2 -2
  4. package/src/authorflow.js +333 -58
  5. package/src/authoring.js +65 -2
  6. package/src/authorjob.js +57 -9
  7. package/src/authorreadout.js +50 -2
  8. package/src/authorrun.js +30 -27
  9. package/src/authorscout.js +9 -6
  10. package/src/bridges.js +1 -1
  11. package/src/bundle.js +7 -4
  12. package/src/bundlerun.js +4 -1
  13. package/src/calibrate.js +1 -1
  14. package/src/cardauthor.js +4 -1
  15. package/src/cli.js +1 -1
  16. package/src/config.js +129 -29
  17. package/src/declaredclose.js +4 -3
  18. package/src/draftspend.js +6 -1
  19. package/src/index.js +2 -4
  20. package/src/interviewrun.js +73 -52
  21. package/src/job.js +50 -11
  22. package/src/jobblock.js +140 -0
  23. package/src/judged.js +12 -44
  24. package/src/keysfile.js +15 -7
  25. package/src/kinds.js +2 -2
  26. package/src/ledger.js +1 -1
  27. package/src/modellist.js +84 -0
  28. package/src/monthly.js +23 -43
  29. package/src/panel/authorroutes.js +34 -19
  30. package/src/panel/authorsession.js +187 -72
  31. package/src/panel/index.html +699 -215
  32. package/src/panel/jobplan.js +107 -0
  33. package/src/panel/runroutes.js +3 -0
  34. package/src/panel/server.js +102 -28
  35. package/src/panel/settingsroutes.js +232 -173
  36. package/src/planrun.js +6 -6
  37. package/src/promptregisters.js +1 -1
  38. package/src/providerrows.js +226 -141
  39. package/src/providers.js +58 -250
  40. package/src/providershapes.js +18 -0
  41. package/src/replay.js +1 -1
  42. package/src/reuse.js +1 -1
  43. package/src/root.js +1 -1
  44. package/src/run.js +3 -3
  45. package/src/selection.js +1 -1
  46. package/src/source.js +64 -1
  47. package/src/text.js +13 -0
  48. package/src/thinking.js +130 -0
  49. package/src/transport.js +1 -1
  50. package/src/userrun.js +53 -76
  51. package/types/authorflow.d.ts +258 -39
  52. package/types/authoring.d.ts +7 -1
  53. package/types/authorjob.d.ts +35 -3
  54. package/types/authorreadout.d.ts +17 -1
  55. package/types/authorscout.d.ts +5 -3
  56. package/types/config.d.ts +23 -6
  57. package/types/declaredclose.d.ts +4 -1
  58. package/types/draftspend.d.ts +8 -2
  59. package/types/index.d.ts +1 -1
  60. package/types/job.d.ts +6 -6
  61. package/types/jobblock.d.ts +102 -0
  62. package/types/judged.d.ts +5 -36
  63. package/types/keysfile.d.ts +15 -4
  64. package/types/modellist.d.ts +50 -0
  65. package/types/monthly.d.ts +4 -3
  66. package/types/panel/authorsession.d.ts +44 -15
  67. package/types/panel/jobplan.d.ts +58 -0
  68. package/types/panel/server.d.ts +34 -4
  69. package/types/planrun.d.ts +1 -1
  70. package/types/providerrows.d.ts +192 -99
  71. package/types/providers.d.ts +35 -116
  72. package/types/providershapes.d.ts +10 -0
  73. package/types/run.d.ts +1 -1
  74. package/types/source.d.ts +26 -0
  75. package/types/thinking.d.ts +90 -0
  76. package/types/userrun.d.ts +6 -1
@@ -0,0 +1,107 @@
1
+ // The job plan MODEL (P7, docs/product/PANEL-BUILD.md "The job" block): ONE owner of the structure the page's single
2
+ // `renderJobPlan()` draws in two places — the drafting confirm card (the plan bubble) and the read-only Job tab. The
3
+ // server builds it; the page only draws it (so the two places can never read the same plan two ways).
4
+ //
5
+ // { lines: [{n, text, rule, checks: [{name, cls}], refused: [reason...]}],
6
+ // loose: [{name, cls}], checks that name no job line (an older caller, or a model slip)
7
+ // notChecked: [text...], "the AI's own reading" — what the drafter says nothing checks
8
+ // alwaysOn: [text...], the mandatory guards / protections, which serve no job line
9
+ // older: boolean } an older job: the numbered lines were not saved
10
+ //
11
+ // `cls` is 'machine check' | 'judge' | null (null = not known yet: the confirm card's checks are the model's plan, the
12
+ // stage kinds exist only after drafting). Pure; no file IO.
13
+ import { KIND_CATALOGUE } from '../authoring.js';
14
+
15
+ /** @typedef {{name: string, cls: 'machine check'|'judge'|null}} PlanCheck */
16
+ /** @typedef {{n: number, text: string, rule: string, checks: PlanCheck[], refused: string[]}} PlanLine */
17
+ /** @typedef {{lines: PlanLine[], loose: PlanCheck[], notChecked: string[], alwaysOn: string[], older: boolean, goal?: string}} JobPlan */
18
+
19
+ /**
20
+ * `machine check` for a kind a command decides, `judge` for one that needs judgement; `null` for a stage whose kind is
21
+ * unknown (a hand-written command close has no kind: it is a command, so a machine check).
22
+ * @param {unknown} kind
23
+ * @returns {'machine check'|'judge'|null}
24
+ */
25
+ export function checkClassOfKind(kind) {
26
+ if (typeof kind !== 'string') return null;
27
+ const spec = Object.hasOwn(KIND_CATALOGUE, kind) ? KIND_CATALOGUE[kind] : null;
28
+ if (!spec) return null;
29
+ return spec.verdictClass === 'green' ? 'machine check' : 'judge';
30
+ }
31
+
32
+ /**
33
+ * 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
35
+ * drafter's own "not checked" words. Without `jobLines` (a spec that predates the job block) the signed goal sentence is
36
+ * line 1 and every stage name is a check under it — `older: true`.
37
+ * @param {any} spec
38
+ * @returns {JobPlan}
39
+ */
40
+ export function planFromSpec(spec) {
41
+ /** @type {{name: string, cls: 'machine check'|'judge'|null, fromLine: number[]|null}[]} */
42
+ let stages = [];
43
+ if (spec && Array.isArray(spec.closeDecl?.stages)) {
44
+ 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 }));
46
+ } else if (spec && Array.isArray(spec.close)) {
47
+ stages = spec.close.filter((/** @type {any} */ s) => s && typeof s.name === 'string' && s.name)
48
+ .map((/** @type {any} */ s) => ({ name: s.name, cls: /** @type {'machine check'} */ ('machine check'), fromLine: null }));
49
+ }
50
+ const notes = Array.isArray(spec?.closeDecl?.notes) ? spec.closeDecl.notes.filter((/** @type {any} */ n) => typeof n === 'string' && n) : [];
51
+ const jl = Array.isArray(spec?.jobLines) ? spec.jobLines.filter((/** @type {any} */ l) => l && Number.isInteger(l.n) && typeof l.text === 'string') : [];
52
+ if (jl.length === 0) {
53
+ return {
54
+ lines: [{
55
+ n: 1, text: typeof spec?.goal === 'string' && spec.goal ? spec.goal : '(no goal recorded)', rule: '',
56
+ checks: stages.map((s) => ({ name: s.name, cls: s.cls })), refused: [],
57
+ }],
58
+ loose: [], notChecked: notes, alwaysOn: [], older: true,
59
+ };
60
+ }
61
+ /** @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: [] }));
63
+ /** @type {PlanCheck[]} */
64
+ const loose = [];
65
+ /** @type {string[]} */
66
+ const alwaysOn = [];
67
+ for (const s of stages) {
68
+ if (s.fromLine === null) { alwaysOn.push(s.name); continue; }
69
+ // a check that serves several job lines is shown under EACH line it serves (no new element: the same check line, repeated)
70
+ const served = lines.filter((l) => /** @type {number[]} */ (s.fromLine).includes(l.n));
71
+ for (const line of served) line.checks.push({ name: s.name, cls: s.cls });
72
+ if (served.length === 0) loose.push({ name: s.name, cls: s.cls });
73
+ }
74
+ const refused = Array.isArray(spec?.closeDecl?.refused) ? spec.closeDecl.refused : [];
75
+ for (const r of refused) {
76
+ const line = lines.find((l) => l.n === r?.line);
77
+ if (line && typeof r.reason === 'string') line.refused.push(r.reason);
78
+ }
79
+ // 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
+ return { lines, loose, notChecked: notes, alwaysOn, older: false, ...(typeof spec?.goal === 'string' && spec.goal ? { goal: spec.goal } : {}) };
81
+ }
82
+
83
+ /**
84
+ * 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
87
+ * @param {{checkItems?: {text: string, fromLine: number[]|null}[], checks?: string[], notChecked?: string[], protections?: string[]}} plan
88
+ * @returns {JobPlan}
89
+ */
90
+ export function planFromConfirm(jobLines, plan) {
91
+ const items = Array.isArray(plan?.checkItems) && plan.checkItems.length
92
+ ? plan.checkItems
93
+ : (Array.isArray(plan?.checks) ? plan.checks.map((c) => ({ text: String(c), fromLine: null })) : []);
94
+ const jl = Array.isArray(jobLines) ? jobLines : [];
95
+ /** @type {PlanLine[]} */
96
+ const lines = jl.map((l) => ({ n: l.n, text: l.text, rule: l.rule, checks: [], refused: [] }));
97
+ /** @type {PlanCheck[]} */
98
+ const loose = [];
99
+ for (const c of items) {
100
+ const served = lines.filter((l) => Array.isArray(c.fromLine) && c.fromLine.includes(l.n));
101
+ for (const line of served) line.checks.push({ name: c.text, cls: null });
102
+ if (served.length === 0) loose.push({ name: c.text, cls: null });
103
+ }
104
+ return {
105
+ lines, loose, notChecked: [...(plan?.notChecked ?? [])], alwaysOn: [...(plan?.protections ?? [])], older: false,
106
+ };
107
+ }
@@ -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
  ];
@@ -36,7 +36,7 @@ import {
36
36
  replayOne, parseJsonl, resolveSiblings, isSidecarByName,
37
37
  } from '../replayio.js';
38
38
  import { summarizeForAllLine, auditWindowsOf } from '../replay.js';
39
- import { legsOf, legsWallMs, stopFilePath } from '../legs.js';
39
+ import { legsOf, legsWallMs, stopFilePath, capKindOf } from '../legs.js';
40
40
  import { chainSpend } from '../ledger.js';
41
41
  import { runBehaviour } from '../behaviour.js';
42
42
  import { SPEND_RECORD_TYPES, floorsFromRecords } from '../ledger.js';
@@ -49,12 +49,14 @@ 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';
56
57
  import { statusFor, partStatusFor, GOAL_MET_LINE } from './status.js';
57
58
  import { createSettingsRoutes } from './settingsroutes.js';
59
+ import { planFromSpec } from './jobplan.js';
58
60
 
59
61
  const HERE = dirname(fileURLToPath(import.meta.url));
60
62
 
@@ -479,7 +481,7 @@ function endedBase(summary, death, o) {
479
481
  if (outcome === 'escalated') {
480
482
  const strikes = esc?.spend?.strikes;
481
483
  const limit = esc?.spend?.strikeLimit;
482
- if (cat === 'cap-halt' && o.moneyHalt) {
484
+ if (capKindOf({ outcome, category: cat, moneyHalt: o.moneyHalt }) === 'money') {
483
485
  const money = typeof summary.spentUsd === 'number' && typeof summary.budgetUsd === 'number'
484
486
  ? ` (${panelMoney2(summary.spentUsd)} of ${panelMoney2(summary.budgetUsd)})` : '';
485
487
  return {
@@ -494,7 +496,7 @@ function endedBase(summary, death, o) {
494
496
  actions: [],
495
497
  };
496
498
  }
497
- if (cat === 'wall-halt') {
499
+ if (capKindOf({ outcome, category: cat, moneyHalt: o.moneyHalt }) === 'time') {
498
500
  return { reason: 'Time cap reached.', next: CHANGE, line: 'time cap', actions: [] };
499
501
  }
500
502
  if (cat === 'provider-red') {
@@ -2098,6 +2100,42 @@ function toolsFromSpec(spec) {
2098
2100
  return names ? names.join(' · ') : null;
2099
2101
  }
2100
2102
 
2103
+ /**
2104
+ * The Signed row of the Job tab: `YYYY-MM-DD HH:MM · <first 6 of specHash>`, the time being the run's FIRST `job-start`
2105
+ * in local time (hamr, P7). `null` when either part is not on record — never a made-up stamp.
2106
+ * @param {string|null|undefined} ts an ISO stamp @param {string|null|undefined} hash
2107
+ * @returns {string|null}
2108
+ */
2109
+ export function signedLine(ts, hash) {
2110
+ const t = typeof ts === 'string' ? new Date(ts) : null;
2111
+ if (!t || Number.isNaN(t.getTime()) || typeof hash !== 'string' || hash.length < 6) return null;
2112
+ const p2 = (/** @type {number} */ n) => String(n).padStart(2, '0');
2113
+ return `${t.getFullYear()}-${p2(t.getMonth() + 1)}-${p2(t.getDate())} ${p2(t.getHours())}:${p2(t.getMinutes())} · ${hash.slice(0, 6)}`;
2114
+ }
2115
+
2116
+ /**
2117
+ * The Job box's text for a signed spec: the person's own numbered lines (a `~` line per entry of `rules`, or — for a spec
2118
+ * signed before `rules` existed — per '; '-split piece of `rule`), or — for
2119
+ * an older job that saved no job lines — the signed goal sentence as the one line.
2120
+ * @param {any} spec @returns {string}
2121
+ */
2122
+ export function jobTextFromSpec(spec) {
2123
+ if (Array.isArray(spec?.jobLines) && spec.jobLines.length) {
2124
+ return spec.jobLines.map((/** @type {any} */ l) => [String(l.text), ...(Array.isArray(l.rules) ? l.rules.map((r) => `~ ${r}`) : l.rule ? String(l.rule).split('; ').map((r) => `~ ${r}`) : [])].join('\n')).join('\n');
2125
+ }
2126
+ return typeof spec?.goal === 'string' ? spec.goal : '';
2127
+ }
2128
+
2129
+ /**
2130
+ * The Inputs box's text for a signed spec: its numbered inputs, or — for an older job — `repo: <source>` from the
2131
+ * caller's best knowledge of the source (or empty).
2132
+ * @param {any} spec @param {string} source @returns {string}
2133
+ */
2134
+ export function inputsTextFromSpec(spec, source) {
2135
+ if (Array.isArray(spec?.inputs) && spec.inputs.length) return spec.inputs.map((/** @type {any} */ i) => `${i.label}: ${i.value}`).join('\n');
2136
+ return source ? `repo: ${source}` : '';
2137
+ }
2138
+
2101
2139
  /**
2102
2140
  * `<src/panel>/../../jobs` — the repo's `jobs/` directory of signed specs,
2103
2141
  * resolved from the bareloop PACKAGE ROOT (this module's own on-disk
@@ -2180,8 +2218,20 @@ export function getRunJob(runid, opts = {}) {
2180
2218
  : (typeof jobStart?.model === 'string' ? jobStart.model : null),
2181
2219
  );
2182
2220
 
2221
+ // the Signed row: the run's FIRST job-start, local time (P7)
2222
+ let firstStart = null;
2223
+ if (existsSync(row.spine)) {
2224
+ try { firstStart = parseJsonl(row.spine).records.find((r) => r && typeof r === 'object' && r.type === 'job-start') ?? null; } catch { firstStart = null; }
2225
+ }
2183
2226
  /** @param {any} spec @param {boolean} resolved @param {string} resolvedFrom @param {string|null} note */
2184
2227
  const fromSpec = (spec, resolved, resolvedFrom, note) => ({
2228
+ // P7: the same plan model the chat's plan bubble is drawn from (src/panel/jobplan.js), read from the SIGNED spec
2229
+ plan: planFromSpec(spec),
2230
+ jobLines: Array.isArray(spec.jobLines) ? spec.jobLines : null,
2231
+ inputs: Array.isArray(spec.inputs) && spec.inputs.length ? spec.inputs : [{ n: 1, label: 'repo', value: sourceDisplay() }],
2232
+ signedAt: typeof firstStart?.ts === 'string' ? firstStart.ts : null,
2233
+ specHash: typeof jobStart?.specHash === 'string' ? jobStart.specHash : null,
2234
+ signed: signedLine(firstStart?.ts, typeof jobStart?.specHash === 'string' ? jobStart.specHash : null),
2185
2235
  runid,
2186
2236
  job: typeof spec.job === 'string' ? spec.job : row.job,
2187
2237
  resolved,
@@ -2202,6 +2252,7 @@ export function getRunJob(runid, opts = {}) {
2202
2252
  note,
2203
2253
  });
2204
2254
  const none = () => ({
2255
+ plan: null, jobLines: null, inputs: [{ n: 1, label: 'repo', value: sourceDisplay() }], signedAt: null, specHash: null, signed: null,
2205
2256
  runid,
2206
2257
  job: row.job,
2207
2258
  resolved: false,
@@ -2263,6 +2314,10 @@ export function getRunJob(runid, opts = {}) {
2263
2314
  // (d) the run's own job-start record — narrower, unsigned.
2264
2315
  if (jobStart) {
2265
2316
  return {
2317
+ plan: null, jobLines: null, inputs: [{ n: 1, label: 'repo', value: sourceDisplay() }],
2318
+ signedAt: typeof jobStart.ts === 'string' ? jobStart.ts : null,
2319
+ specHash: typeof jobStart.specHash === 'string' ? jobStart.specHash : null,
2320
+ signed: signedLine(jobStart.ts, jobStart.specHash),
2266
2321
  runid,
2267
2322
  job: typeof jobStart.job === 'string' ? jobStart.job : row.job,
2268
2323
  resolved: false,
@@ -2320,28 +2375,38 @@ function signedSpecForRun(row, records) {
2320
2375
  }
2321
2376
 
2322
2377
  /**
2323
- * The worker a signed spec ran: provider, baseUrl and model, the model falling back to the provider's default tier
2324
- * when the spec names none (the same fallback `resolveWorkerModel` makes at run time).
2378
+ * The worker a signed spec ran: provider, baseUrl and model ('' when an older spec names none: bareloop keeps no default).
2325
2379
  * @param {any} spec
2326
2380
  * @returns {{provider: string, baseUrl?: string, model: string}}
2327
2381
  */
2328
2382
  function workerOfSpec(spec) {
2329
2383
  const provider = typeof spec?.provider === 'string' ? spec.provider : '';
2330
- let model = typeof spec?.model === 'string' ? spec.model : '';
2331
- if (!model) { try { model = resolveProvider(provider).tiers.sonnet; } catch { model = ''; } }
2384
+ const model = typeof spec?.model === 'string' ? spec.model : '';
2332
2385
  return { provider, ...(typeof spec?.baseUrl === 'string' && spec.baseUrl ? { baseUrl: spec.baseUrl } : {}), model };
2333
2386
  }
2334
2387
  /** @param {{provider: string, baseUrl?: string, model: string}} w @returns {string} one spelling of "the same worker" */
2335
2388
  const workerId = (w) => `${w.provider}\u0000${w.baseUrl ?? ''}\u0000${w.model}`;
2389
+ /**
2390
+ * 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
2391
+ * row's first ticked line; `undefined` when there is no row or no such line.
2392
+ * @param {import('../providerrows.js').KeyRow|null} row
2393
+ * @param {string|null|undefined} specModel
2394
+ * @returns {string|undefined}
2395
+ */
2396
+ function rowModelOf(row, specModel) {
2397
+ if (!row) return undefined;
2398
+ if (specModel && row.models.some((l) => l.model === specModel)) return specModel;
2399
+ return tickedLines([row])[0]?.line.model;
2400
+ }
2336
2401
  /**
2337
2402
  * The worker the reuse card's CURRENT Model box names: the Settings row's provider, baseUrl and Name. `null` when the
2338
2403
  * box is blank or names no row.
2339
2404
  * @param {string} model a Settings Name
2340
- * @param {{ home?: string }} opts
2405
+ * @param {{ home?: string, provider?: string }} opts `provider` is the key's env name the card picked
2341
2406
  * @returns {{provider: string, baseUrl?: string, model: string}|null}
2342
2407
  */
2343
2408
  function workerOfChoice(model, opts) {
2344
- const c = modelChoiceFor(rowsForHome(keysHome(opts.home)), model);
2409
+ const c = modelChoiceFor(rowsForHome(keysHome(opts.home)), { provider: opts.provider, model });
2345
2410
  return c ? { provider: c.provider, ...(c.baseUrl ? { baseUrl: c.baseUrl } : {}), model: c.name } : null;
2346
2411
  }
2347
2412
 
@@ -2424,10 +2489,11 @@ export function reuseLine(record) {
2424
2489
  * $ cap and the Time cap — start at what the run had. `ok:false` when the run's signed job is not on disk (nothing
2425
2490
  * to copy: change the job: Clear the card). `null` when the runid is not listed.
2426
2491
  * @param {string} runid
2427
- * @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
2492
+ * @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
2428
2493
  * @returns {{ok: true, origin: {runid: string, job: string}, outcome: string|null, card: Record<string, any>, from: 'signed job',
2429
2494
  * locked: readonly string[], open: readonly string[], note: string|null,
2430
- * specHash: string, workflowKey: string, spec: any, specPath: string,
2495
+ * specHash: string, workflowKey: string, spec: any, specPath: string, specNamesModel: boolean,
2496
+ * specWorker: {provider: string, endpoint: string, label: string},
2431
2497
  * trackRecord: ReturnType<typeof trackRecordFor>, line: string}|{ok: false, error: string}|null}
2432
2498
  */
2433
2499
  export function getStartFrom(runid, opts = {}) {
@@ -2453,14 +2519,15 @@ export function getStartFrom(runid, opts = {}) {
2453
2519
  const jobStart = records.find((r) => r && r.type === 'job-start') ?? null;
2454
2520
  const card = reuseCardFromSpec(spec, {
2455
2521
  model: typeof saved.model === 'string' && saved.model ? saved.model
2456
- : (findRow(rowsForHome(keysHome(opts.home)), { provider: spec.provider, baseUrl: spec.baseUrl, model: spec.model })?.name
2522
+ : (rowModelOf(findRow(rowsForHome(keysHome(opts.home)), { provider: spec.provider, baseUrl: spec.baseUrl, model: spec.model }), spec.model)
2457
2523
  ?? (typeof jobStart?.model === 'string' ? jobStart.model : '')),
2458
2524
  source: typeof saved.source === 'string' && saved.source ? saved.source : (typeof manifest?.source === 'string' ? manifest.source : (typeof row.patient === 'string' ? row.patient : '')),
2459
- judgeExamples: typeof saved.judgeExamples === 'string' ? saved.judgeExamples : '',
2525
+ // a card saved by P7 or later keeps the Inputs text the person typed
2526
+ inputs: typeof saved.inputs === 'string' && saved.inputs ? saved.inputs : '',
2460
2527
  jobName: row.job,
2461
2528
  });
2462
2529
  const key = workflowKey(spec);
2463
- const trackRecord = trackRecordFor(key, { ...opts, worker: workerOfChoice(opts.model ?? card.model, opts) });
2530
+ const trackRecord = trackRecordFor(key, { ...opts, worker: workerOfChoice(opts.model ?? card.model, { ...opts, provider: opts.provider ?? card.provider }) });
2464
2531
  return {
2465
2532
  ok: true,
2466
2533
  origin: { runid, job: row.job },
@@ -2470,11 +2537,19 @@ export function getStartFrom(runid, opts = {}) {
2470
2537
  from: 'signed job',
2471
2538
  locked: REUSE_LOCKED_FIELDS,
2472
2539
  open: REUSE_OPEN_FIELDS,
2473
- note: card.checkType === 'rubric' && card.judgeExamples === '' ? 'judge examples were not saved for this run' : null,
2540
+ note: Array.isArray(spec.jobLines) && spec.jobLines.length ? null : 'older job — numbered lines were not saved',
2474
2541
  specHash: signed.specHash,
2475
2542
  workflowKey: key,
2476
2543
  spec,
2477
2544
  specPath: signed.specPath,
2545
+ // an older spec signed before models were required names none: the Resume card then opens its Model box (run-u takes `--model`)
2546
+ specNamesModel: typeof spec.model === 'string' && spec.model !== '',
2547
+ // what resolveWorker will hold a picked line to: the spec's shape, and its endpoint when it names one ('' = any endpoint)
2548
+ specWorker: {
2549
+ provider: String(spec.provider),
2550
+ endpoint: typeof spec.baseUrl === 'string' && spec.baseUrl.trim() !== '' ? endpointOf(String(spec.provider), spec.baseUrl) : '',
2551
+ label: SHAPES.find((x) => x.id === spec.provider)?.label ?? String(spec.provider),
2552
+ },
2478
2553
  trackRecord,
2479
2554
  line: reuseLine(trackRecord),
2480
2555
  };
@@ -2485,7 +2560,7 @@ export function getStartFrom(runid, opts = {}) {
2485
2560
  * that has at least one GREEN local run, newest green first. `runid` = that job's newest green run, so the page hands
2486
2561
  * it to the existing start-from flow unchanged; `line` is `getStartFrom`'s own estimate line (never a second
2487
2562
  * spelling). Local runs only — an imported job keeps its Reuse button in the Import view. $0, read-only.
2488
- * @param {{ home?: string, model?: string }} [opts] `model` passes through to the estimate line, as start-from's does
2563
+ * @param {{ home?: string, model?: string, provider?: string }} [opts] `model` passes through to the estimate line, as start-from's does
2489
2564
  * @returns {{job: string, runid: string, checkType: 'deterministic'|'rubric', line: string}[]}
2490
2565
  */
2491
2566
  export function listReuseJobs(opts = {}) {
@@ -2518,7 +2593,7 @@ export function listReuseJobs(opts = {}) {
2518
2593
  * The reuse card for a signed spec: locked boxes from the spec through the Job tab's own readers, open boxes from
2519
2594
  * the spec's own fence and caps (Source and the saved text from the caller — a spec carries neither).
2520
2595
  * @param {any} spec
2521
- * @param {{model: string, source: string, judgeExamples: string, jobName: string}} extra
2596
+ * @param {{model: string, source: string, inputs?: string, jobName: string}} extra `inputs` = the Inputs text the person saved (else built from the spec's inputs, else `repo: <source>`)
2522
2597
  * @returns {Record<string, any>}
2523
2598
  */
2524
2599
  function reuseCardFromSpec(spec, extra) {
@@ -2527,12 +2602,9 @@ function reuseCardFromSpec(spec, extra) {
2527
2602
  jobName: typeof spec.job === 'string' && spec.job ? spec.job : extra.jobName,
2528
2603
  checkType: spec.verdictType === 'soft-green' ? 'rubric' : 'deterministic',
2529
2604
  model: typeof spec.model === 'string' && spec.model && !extra.model ? spec.model : extra.model,
2530
- goal: typeof spec.goal === 'string' ? spec.goal : '',
2531
- source: extra.source,
2605
+ jobText: jobTextFromSpec(spec),
2606
+ inputs: extra.inputs || inputsTextFromSpec(spec, extra.source),
2532
2607
  destination: writeScope.join(', '),
2533
- success: successFromSpec(spec) ?? '',
2534
- guardrails: guardrailsFromSpec(spec) ?? '',
2535
- judgeExamples: extra.judgeExamples,
2536
2608
  capUsd: typeof spec.budgetUsd === 'number' ? spec.budgetUsd : null,
2537
2609
  maxWallMs: typeof spec.maxWallMs === 'number' ? spec.maxWallMs : undefined,
2538
2610
  };
@@ -2542,7 +2614,7 @@ function reuseCardFromSpec(spec, extra) {
2542
2614
  * P5 item 4 — the facts of a spec for an IMPORTED job's view, read by the SAME readers the Job tab uses for a run
2543
2615
  * (so an imported job and a run read alike): check type, goal, success checks, guardrails, model, caps.
2544
2616
  * @param {any} spec
2545
- * @returns {{checkType: string, goal: string, success: string, guardrails: string, model: string, budgetUsd: number|null, maxWallMs: number|null}}
2617
+ * @returns {{checkType: string, goal: string, success: string, guardrails: string, plan: ReturnType<typeof planFromSpec>, inputs: any[]|null, model: string, budgetUsd: number|null, maxWallMs: number|null}}
2546
2618
  */
2547
2619
  function describeSpec(spec) {
2548
2620
  const nr = (/** @type {string|null} */ v) => (typeof v === 'string' && v.length > 0 ? v : 'not recorded');
@@ -2551,6 +2623,8 @@ function describeSpec(spec) {
2551
2623
  goal: nr(typeof spec.goal === 'string' ? spec.goal : null),
2552
2624
  success: nr(successFromSpec(spec)),
2553
2625
  guardrails: nr(guardrailsFromSpec(spec)),
2626
+ plan: planFromSpec(spec),
2627
+ inputs: Array.isArray(spec.inputs) && spec.inputs.length ? spec.inputs : null,
2554
2628
  model: nr(typeof spec.model === 'string' ? spec.model : null),
2555
2629
  budgetUsd: typeof spec.budgetUsd === 'number' ? spec.budgetUsd : null,
2556
2630
  maxWallMs: typeof spec.maxWallMs === 'number' ? spec.maxWallMs : null,
@@ -2567,7 +2641,7 @@ function describeSpec(spec) {
2567
2641
  * signs it on this machine at [Sign & run]. Same card, same locked/open boxes as a run's reuse. `null` when no such
2568
2642
  * imported job.
2569
2643
  * @param {string} id
2570
- * @param {{ home?: string, model?: string }} [opts] `model` as for {@link getStartFrom}
2644
+ * @param {{ home?: string, model?: string, provider?: string }} [opts] `model` as for {@link getStartFrom}
2571
2645
  * @returns {{ok: true, origin: {runid: null, job: string, importId: string}, card: Record<string, any>, from: 'imported job',
2572
2646
  * locked: readonly string[], open: readonly string[], note: string|null, specHash: string, workflowKey: string, spec: any,
2573
2647
  * specPath: null, trackRecord: ReturnType<typeof trackRecordFor>, line: string}|{ok: false, error: string}|null}
@@ -2586,10 +2660,10 @@ export function getStartFromImport(id, opts = {}) {
2586
2660
  // the bundle's provider is no longer forced: Model is open on a reuse, so the person's chosen Settings row is the worker.
2587
2661
  // The card opens on the row matching the bundle's own provider when there is one, else blank (the page's menu picks).
2588
2662
  const rows = rowsForHome(keysHome(opts.home));
2589
- const named = findRow(rows, { provider: spec.provider, baseUrl: spec.baseUrl, model: spec.model })?.name ?? '';
2590
- const card = reuseCardFromSpec(spec, { model: named, source: '', judgeExamples: '', jobName: row.job });
2663
+ const named = rowModelOf(findRow(rows, { provider: spec.provider, baseUrl: spec.baseUrl, model: spec.model }), spec.model) ?? '';
2664
+ const card = reuseCardFromSpec(spec, { model: named, source: '', jobName: row.job });
2591
2665
  const key = workflowKey(spec);
2592
- const trackRecord = trackRecordFor(key, { ...opts, worker: workerOfChoice(opts.model ?? card.model, opts) });
2666
+ const trackRecord = trackRecordFor(key, { ...opts, worker: workerOfChoice(opts.model ?? card.model, { ...opts, provider: opts.provider ?? card.provider }) });
2593
2667
  return {
2594
2668
  ok: true,
2595
2669
  origin: { runid: null, job: row.job, importId: id },