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
@@ -22,14 +22,16 @@ import { spawn as realSpawn } from 'node:child_process';
22
22
  import { openSync } from 'node:fs';
23
23
  import { join } from 'node:path';
24
24
  import { readFileSync } from 'node:fs';
25
+ import { planMatchesClose } from '../rubricstep.js';
25
26
  import { createSession, validateJobCard, validateReuseCard, lockedFieldChanged, buildReuseSpec, cardFields, STEP_LABELS } from './authorsession.js';
26
27
  import { parseInputs } from '../jobblock.js';
27
- import { proveInputFiles } from '../source.js';
28
+ import { proveInputFiles, looksLikeRubricInput } from '../source.js';
28
29
  import { validateJob } from '../job.js';
29
30
  import { checkMonthlyRoom, monthlyRefusalText } from '../monthly.js';
30
31
  import { ConfigError } from '../config.js';
31
32
  import { keysForDoor, keysHome } from '../keysfile.js';
32
- import { keyNameFor, modelChoiceFor, rowsForHome, chatModels, usableKey } from '../providerrows.js';
33
+ import { endpointOf, judgePick, keyValueOf, rowsForHome, tickedLines } from '../providerrows.js';
34
+ import { thinkingForLine } from '../thinking.js';
33
35
  import { apiKeyProblem, checkProviderReachable } from '../providers.js';
34
36
 
35
37
  /** @returns {string} a fresh per-process token — never persisted, never logged */
@@ -137,13 +139,13 @@ export function createAuthorRoutes(opts) {
137
139
  if (pathname === '/api/author/model-check') {
138
140
  if (req.method !== 'GET') { send(405, { ok: false, error: 'GET only' }); return true; }
139
141
  const q = new URL(/** @type {string} */ (req.url), 'http://127.0.0.1').searchParams;
140
- const modelId = q.get('model') ?? '';
141
- const rows = rowsForHome(keysHome(opts.home));
142
- const choice = modelChoiceFor(rows, modelId);
143
- if (!choice) { send(400, { ok: false, error: `unknown model "${modelId}"` }); return true; }
144
- // the key of the Settings row this Name belongs to
145
- const keyName = keyNameFor(choice.provider, choice.baseUrl, rows, choice.name).name;
146
- const raw = envNow()[keyName];
142
+ const rows = rowsForHome(keysHome(opts.home), envNow());
143
+ const pick = judgePick(rows, { provider: q.get('provider'), model: q.get('model') });
144
+ if (!pick.ok) { send(400, { ok: false, error: pick.say }); return true; }
145
+ const choice = pick.choice;
146
+ // the key of the Settings row this line belongs to
147
+ const keyName = choice.envName;
148
+ const raw = keyValueOf(envNow(), keyName);
147
149
  const problem = raw ? apiKeyProblem(raw) : null;
148
150
  const keyStatus = !raw ? 'missing' : (problem ? 'bad-shape' : 'found');
149
151
  if (keyStatus !== 'found') {
@@ -157,7 +159,7 @@ export function createAuthorRoutes(opts) {
157
159
  // `checkProviderReachable`'s own doc) — a short timeout so a flaky
158
160
  // endpoint never hangs the card.
159
161
  checkProviderReachable({
160
- providerName: choice.provider, apiKey: /** @type {string} */ (usableKey(raw)), model: choice.name,
162
+ providerName: choice.provider, apiKey: /** @type {string} */ (raw), model: choice.name,
161
163
  baseUrl: choice.baseUrl, fetchImpl, timeoutMs: 4000,
162
164
  }).then((r) => {
163
165
  send(200, {
@@ -180,7 +182,11 @@ export function createAuthorRoutes(opts) {
180
182
  // ── P4b: Chat's Model menu IS the Settings rows (a blank Name is not offered). ──
181
183
  if (pathname === '/api/author/models') {
182
184
  if (req.method !== 'GET') { send(405, { ok: false, error: 'GET only' }); return true; }
183
- send(200, { ok: true, models: chatModels(rowsForHome(keysHome(opts.home))) });
185
+ // every ticked-and-priced line of a row, labelled `<KEY> · <model>`; `thinking` is the server's answer for the "Thinking on" box
186
+ const lines = tickedLines(rowsForHome(keysHome(opts.home), envNow())).map(({ row, line }) => ({
187
+ slot: row.envName, model: line.model, label: `${row.envName} · ${line.model}`, shape: row.provider, endpoint: endpointOf(row.provider, row.baseUrl), thinking: thinkingForLine(row.provider, line.model, row.baseUrl),
188
+ }));
189
+ send(200, { ok: true, lines, say: lines.length === 0 ? 'Tick a model in Settings > Providers.' : '' });
184
190
  return true;
185
191
  }
186
192
 
@@ -207,14 +213,14 @@ export function createAuthorRoutes(opts) {
207
213
  const sp = new URL(/** @type {string} */ (req.url), 'http://127.0.0.1').searchParams;
208
214
  const runid = sp.get('runid') ?? '';
209
215
  // `model` = the card's Model box right now: the estimate line counts only runs on that worker
210
- const asModel = sp.has('model') ? { model: sp.get('model') ?? '' } : {};
216
+ const asModel = sp.has('model') ? { model: sp.get('model') ?? '', provider: sp.get('provider') ?? '' } : {};
211
217
  const pre = sp.has('import') ? opts.startFrom.getImport(sp.get('import') ?? '', asModel) : opts.startFrom.get(runid, asModel);
212
218
  if (pre === null) { send(404, { ok: false, error: 'no such run' }); return true; }
213
219
  if (!pre.ok) { send(409, pre); return true; }
214
220
  send(200, {
215
221
  ok: true, origin: pre.origin, card: pre.card, from: pre.from, note: pre.note,
216
222
  locked: pre.locked, open: pre.open, specHash: pre.specHash, workflowKey: pre.workflowKey, trackRecord: pre.trackRecord,
217
- line: pre.line,
223
+ line: pre.line, specNamesModel: pre.specNamesModel, specWorker: pre.specWorker,
218
224
  });
219
225
  return true;
220
226
  }
@@ -225,7 +231,7 @@ export function createAuthorRoutes(opts) {
225
231
  if (!opts.startFrom || !opts.startFrom.listJobs) { send(404, { ok: false, error: 'not found' }); return true; }
226
232
  if (req.method !== 'GET') { send(405, { ok: false, error: 'GET only' }); return true; }
227
233
  const sp = new URL(/** @type {string} */ (req.url), 'http://127.0.0.1').searchParams;
228
- const asModel = sp.has('model') ? { model: sp.get('model') ?? '' } : {};
234
+ const asModel = sp.has('model') ? { model: sp.get('model') ?? '', provider: sp.get('provider') ?? '' } : {};
229
235
  send(200, { ok: true, jobs: opts.startFrom.listJobs(asModel) });
230
236
  return true;
231
237
  }
@@ -243,7 +249,7 @@ export function createAuthorRoutes(opts) {
243
249
  if (req.method !== 'POST') { send(405, { ok: false, error: 'POST only' }); return true; }
244
250
  if (hasLiveSession()) { send(409, { ok: false, error: 'an authoring session is already live — one at a time' }); return true; }
245
251
  const card = body ?? {};
246
- const rows = rowsForHome(keysHome(opts.home));
252
+ const rows = rowsForHome(keysHome(opts.home), envNow());
247
253
  // Reuse workflow — started from a green run or an imported job: only Source, Destination and the two caps may
248
254
  // differ from the signed workflow. The SERVER refuses any other change (the page greys those boxes, but is
249
255
  // never trusted); the spec it signs is a copy of the origin's with just those four set. Nothing is drafted.
@@ -280,7 +286,10 @@ export function createAuthorRoutes(opts) {
280
286
  // A miss is a plain 400 naming the line, shown under the card exactly like every other refused start.
281
287
  const ip = parseInputs(card.inputs);
282
288
  if (!ip.ok) { send(400, { ok: false, error: ip.error }); return true; }
283
- proveInputFiles(ip.inputs).then((proof) => {
289
+ // Part 1a: a rubric job whose first input is a local FILE has no repo to prove inputs against; the session proves every
290
+ // line itself (`proveRubricInputs`), at $0, before any model call
291
+ const filesJob = reuse === null && card?.checkType === 'rubric' && looksLikeRubricInput(ip.inputs[0].value);
292
+ (filesJob ? Promise.resolve(/** @type {{ok: true}} */ ({ ok: true })) : proveInputFiles(ip.inputs)).then((proof) => {
284
293
  if (!proof.ok) { send(400, { ok: false, error: proof.error }); return; }
285
294
  if (hasLiveSession()) { send(409, { ok: false, error: 'an authoring session is already live — one at a time' }); return; }
286
295
  const session = createSession(cardFields(card), {
@@ -388,6 +397,15 @@ export function signRun(session, claimedHash, o) {
388
397
  if (e instanceof ConfigError) return { ok: false, error: `${e.message} — refusing rather than guess the monthly limit` };
389
398
  return { ok: false, error: 'could not read the signed spec to check the monthly limit — refusing to sign' };
390
399
  }
400
+ // PART 1a, the sign-vs-run invariant: the checks on the plan card the person saw are exactly the checks of the close about to be
401
+ // signed, per line. A mismatch is refused in plain words and nothing is spawned.
402
+ if (session.state.finalPlan) {
403
+ try {
404
+ const spec = JSON.parse(readFileSync(session.state.resolvedSpecPath, 'utf8'));
405
+ const m = planMatchesClose(session.state.finalPlan.checkItems, spec.closeDecl);
406
+ if (!m.ok) return { ok: false, error: m.error };
407
+ } catch { return { ok: false, error: 'could not read the signed spec to compare it with the plan you were shown — refusing to sign' }; }
408
+ }
391
409
  const logFile = join(session.state.outDir, 'run.log');
392
410
  // array argv, never a shell string — `--approve <hash>` is a literal
393
411
  // element, checkable byte-for-byte by a test stubbing `spawnFn`, and there