bareloop 0.43.0 → 0.45.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 (64) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/bareloop.context.md +53 -13
  3. package/package.json +2 -2
  4. package/src/authorflow.js +27 -12
  5. package/src/authoring.js +1 -1
  6. package/src/authorjob.js +52 -16
  7. package/src/authorreadout.js +12 -6
  8. package/src/authorrun.js +8 -2
  9. package/src/declaredclose.js +4 -1
  10. package/src/draftspend.js +20 -2
  11. package/src/facts.js +54 -9
  12. package/src/factwords.js +58 -6
  13. package/src/fitcheck.js +166 -0
  14. package/src/inputlook.js +38 -3
  15. package/src/job.js +1 -1
  16. package/src/kinds.js +5 -0
  17. package/src/pagedropwords.js +110 -0
  18. package/src/pagedry.js +125 -0
  19. package/src/pagemap.js +269 -0
  20. package/src/pagerules.js +318 -0
  21. package/src/panel/authorroutes.js +34 -30
  22. package/src/panel/authorsession.js +66 -11
  23. package/src/panel/index.html +469 -65
  24. package/src/panel/jobplan.js +14 -7
  25. package/src/panel/runroutes.js +46 -22
  26. package/src/panel/server.js +792 -28
  27. package/src/panel/status.js +33 -0
  28. package/src/panelmoney.js +29 -0
  29. package/src/planrun.js +53 -4
  30. package/src/promptregisters.js +8 -0
  31. package/src/replay.js +118 -7
  32. package/src/rubricstep.js +157 -15
  33. package/src/runlist.js +2 -1
  34. package/src/schedulewords.js +6 -3
  35. package/src/source.js +18 -0
  36. package/src/thinking.js +63 -2
  37. package/src/userrun.js +28 -14
  38. package/types/authorflow.d.ts +42 -6
  39. package/types/authorjob.d.ts +20 -4
  40. package/types/declaredclose.d.ts +2 -0
  41. package/types/draftspend.d.ts +27 -1
  42. package/types/facts.d.ts +15 -0
  43. package/types/factwords.d.ts +26 -3
  44. package/types/fitcheck.d.ts +113 -0
  45. package/types/inputlook.d.ts +15 -0
  46. package/types/kinds.d.ts +5 -0
  47. package/types/pagedropwords.d.ts +39 -0
  48. package/types/pagedry.d.ts +37 -0
  49. package/types/pagemap.d.ts +154 -0
  50. package/types/pagerules.d.ts +86 -0
  51. package/types/panel/authorroutes.d.ts +17 -14
  52. package/types/panel/authorsession.d.ts +4 -2
  53. package/types/panel/jobplan.d.ts +5 -2
  54. package/types/panel/runroutes.d.ts +2 -2
  55. package/types/panel/server.d.ts +156 -2
  56. package/types/panel/status.d.ts +51 -0
  57. package/types/panelmoney.d.ts +5 -0
  58. package/types/replay.d.ts +21 -1
  59. package/types/rubricstep.d.ts +62 -1
  60. package/types/runlist.d.ts +4 -1
  61. package/types/schedulewords.d.ts +4 -2
  62. package/types/source.d.ts +11 -0
  63. package/types/thinking.d.ts +39 -0
  64. package/types/userrun.d.ts +4 -0
@@ -11,11 +11,12 @@
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
+ import { splitReadNotes } from '../pagedropwords.js';
15
+ import { checksOfClose, signedFactNames, proseNotChecked, lineRules, confirmedGoalOf } from '../rubricstep.js';
15
16
 
16
17
  /** @typedef {{name: string, cls: 'machine check'|'judge'|null}} PlanCheck */
17
18
  /** @typedef {{n: number, text: string, rule: string, rules: string[], checks: PlanCheck[], refused: string[]}} PlanLine */
18
- /** @typedef {{lines: PlanLine[], loose: PlanCheck[], notChecked: string[], alwaysOn: string[], older: boolean, goal?: string}} JobPlan */
19
+ /** @typedef {{lines: PlanLine[], loose: PlanCheck[], notChecked: string[], readNotes: string[], alwaysOn: string[], older: boolean, goal?: string}} JobPlan */
19
20
 
20
21
  /**
21
22
  * `machine check` for a kind a command decides, `judge` for one that needs judgement; `null` for a stage whose kind is
@@ -65,10 +66,12 @@ export function planFromSpec(spec) {
65
66
  .map((/** @type {any} */ s) => ({ name: s.name, cls: /** @type {'machine check'} */ ('machine check'), fromLine: null }));
66
67
  }
67
68
  // a prose step says what it does not check (PART 1b, RF5): the person judges the text at Accept
68
- const notes = [
69
+ const allNotes = [
69
70
  ...(Array.isArray(spec?.closeDecl?.notes) ? spec.closeDecl.notes.filter((/** @type {any} */ n) => typeof n === 'string' && n) : []),
70
71
  ...proseNotChecked(spec?.closeDecl),
71
72
  ];
73
+ // code's notes on which written forms of a still-checked fact were not used are "How the page was read", not Not checked
74
+ const { notChecked: notes, readNotes } = splitReadNotes(allNotes, signedFactNames(spec?.closeDecl));
72
75
  const jl = Array.isArray(spec?.jobLines) ? spec.jobLines.filter((/** @type {any} */ l) => l && Number.isInteger(l.n) && typeof l.text === 'string') : [];
73
76
  if (jl.length === 0) {
74
77
  return {
@@ -76,7 +79,7 @@ export function planFromSpec(spec) {
76
79
  n: 1, text: typeof spec?.goal === 'string' && spec.goal ? spec.goal : '(no goal recorded)', rule: '', rules: [],
77
80
  checks: stages.map((s) => ({ name: s.name, cls: s.cls })), refused: [],
78
81
  }],
79
- loose: [], notChecked: notes, alwaysOn: [], older: true,
82
+ loose: [], notChecked: notes, readNotes, alwaysOn: [], older: true,
80
83
  };
81
84
  }
82
85
  /** @type {PlanLine[]} */
@@ -100,14 +103,17 @@ export function planFromSpec(spec) {
100
103
  if (line && typeof r.reason === 'string') line.refused.push(ruleText ? `"${ruleText}" — ${r.reason}` : r.reason);
101
104
  }
102
105
  // 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)
103
- return { lines, loose, notChecked: notes, alwaysOn, older: false, ...(typeof spec?.goal === 'string' && spec.goal ? { goal: spec.goal } : {}) };
106
+ // (a rubric job's signed goal is the whole worker brief: only the sentence the person confirmed is the summary)
107
+ const summary = typeof spec?.goal === 'string' && spec.goal ? confirmedGoalOf(spec.goal) : null;
108
+ return { lines, loose, notChecked: notes, readNotes, alwaysOn, older: false, ...(summary ? { goal: summary } : {}) };
104
109
  }
105
110
 
106
111
  /**
107
112
  * The plan of the confirm turn, before anything is drafted: the person's own job lines, the model's checks under the
108
113
  * line each names, its own "not checked" words, and the real protections (code-owned) as "Always checked".
109
114
  * @param {{n: number, text: string, rule: string, rules?: string[]}[]|null|undefined} jobLines
110
- * @param {{checkItems?: {text: string, fromLine: number[]|null}[], checks?: string[], notChecked?: string[], protections?: string[]}} plan
115
+ * @param {{checkItems?: {text: string, fromLine: number[]|null}[], checks?: string[], notChecked?: string[], protections?: string[], signedFacts?: string[]}} plan
116
+ * `signedFacts` = the names of the facts the look signs (so an S1 copy of a signed fact reads as a note, not Not checked)
111
117
  * @returns {JobPlan}
112
118
  */
113
119
  export function planFromConfirm(jobLines, plan) {
@@ -124,7 +130,8 @@ export function planFromConfirm(jobLines, plan) {
124
130
  for (const line of served) line.checks.push({ name: c.text, cls: null });
125
131
  if (served.length === 0) loose.push({ name: c.text, cls: null });
126
132
  }
133
+ const { notChecked, readNotes } = splitReadNotes(plan?.notChecked, plan?.signedFacts);
127
134
  return {
128
- lines, loose, notChecked: [...(plan?.notChecked ?? [])], alwaysOn: [...(plan?.protections ?? [])], older: false,
135
+ lines, loose, notChecked, readNotes, alwaysOn: [...(plan?.protections ?? [])], older: false,
129
136
  };
130
137
  }
@@ -16,12 +16,15 @@ import {
16
16
  openSync, closeSync, writeFileSync, readFileSync, readdirSync,
17
17
  } from 'node:fs';
18
18
  import { dirname, join } from 'node:path';
19
- import { checkHumanGuard, OLD_JOB_ACCEPT_WORDS, OLD_JOB_RESUME_WORDS } from './authorroutes.js';
19
+ import {
20
+ checkHumanGuard, OLD_JOB_ACCEPT_WORDS, OLD_JOB_RESUME_WORDS, OLD_JOB_RERUN_WORDS, OLD_JOB_PAUSE_WORDS,
21
+ } from './authorroutes.js';
20
22
  import { checkMonthlyRoom, monthlyRefusalText } from '../monthly.js';
21
23
  import { ConfigError } from '../config.js';
22
24
  import { keysForDoor } from '../keysfile.js';
23
25
  import { jobSpecHash, validateJob } from '../job.js';
24
26
  import { stopFilePath } from '../legs.js';
27
+ import { panelMoney2 } from '../panelmoney.js';
25
28
  import { appendNeedsRemoved } from '../runlist.js';
26
29
 
27
30
  /** the tail of an engine log shown to the person when the engine refuses at start (bytes) */
@@ -65,7 +68,7 @@ function readLogTail(file) {
65
68
  * recorded...`, `CHECKPOINT EXPIRED — ...`); anything else (validator reds, exit statuses, stack traces, argv) becomes ONE fixed
66
69
  * sentence naming the log. The one owner for the Resume and Accept routes.
67
70
  * @param {string} tail the log tail
68
- * @param {'resume'|'accept'} kind
71
+ * @param {'resume'|'accept'|'rerun'|'pause'} kind
69
72
  * @param {string} logFile
70
73
  * @param {(stage: string) => string|null} [questionOf] stage name to the panel's plain question (`null` = none)
71
74
  * @returns {string}
@@ -85,9 +88,10 @@ export function engineRefusalWords(tail, kind, logFile, questionOf = () => null)
85
88
  return line;
86
89
  }
87
90
  }
88
- return kind === 'resume'
89
- ? `Resume did not start. Nothing was spent on it. The details are in ${logFile}.`
90
- : `Accept did not go through. Nothing was recorded. The details are in ${logFile}.`;
91
+ if (kind === 'resume') return `Resume did not start. Nothing was spent on it. The details are in ${logFile}.`;
92
+ if (kind === 'rerun') return `Rerun did not start. Nothing was spent on it. The details are in ${logFile}.`;
93
+ if (kind === 'pause') return `Pause did not go through. Nothing was recorded. The details are in ${logFile}.`;
94
+ return `Accept did not go through. Nothing was recorded. The details are in ${logFile}.`;
91
95
  }
92
96
 
93
97
  /**
@@ -159,24 +163,30 @@ export function createRunRoutes(opts) {
159
163
  }
160
164
 
161
165
  /**
162
- * T Part 8 — `POST /api/runs/:runid/accept`. The person's Accept of a run waiting at its REVIEW DOOR, recorded through the
163
- * library's own door path and nothing else: `run-u --spec <signed spec> --door <runid> --decide accept --approve <hash>`
166
+ * T Part 8 + PRD v1.107 Part 6 — the person's answer to a run waiting at its REVIEW DOOR: `accept`, `rerun` (with their words) or `pause`, recorded through
167
+ * the library's own door path and nothing else: `run-u --spec <signed spec> --door <runid> --decide <accept|rerun|pause> [--text …] --approve <hash>`
164
168
  * (src/userrun.js, `answerReviewDoor`, src/reviewdoor.js). The panel invents no verdict path: the engine re-runs the MECHANICAL
165
- * stages against the tree as it stands, a red there is `door-accept-red` and refuses, and only then is the disposition written
169
+ * stages against the tree as it stands for an accept (a red there is `door-accept-red` and refuses), and only then is the disposition written
166
170
  * to the run's own spine. The verdict the run minted is untouched. The engine's own words are what the page shows.
171
+ * An accept and a pause end when the engine does; a RERUN launches a fresh engagement (a paid run), so it answers like Resume: started, or the engine's refusal.
167
172
  * @param {import('node:http').IncomingMessage} req
168
173
  * @param {(code: number, obj: any) => void} send
169
174
  * @param {string} runid
175
+ * @param {'accept'|'rerun'|'pause'} decide
176
+ * @param {any} body the request body (`text` for a rerun)
170
177
  * @returns {boolean}
171
178
  */
172
- function handleAccept(req, send, runid) {
179
+ function handleDoor(req, send, runid, decide, body) {
173
180
  const guard = checkHumanGuard(req, { token: opts.token, port: opts.port });
174
181
  if (!guard.ok) { send(403, { ok: false, error: `refused — ${guard.reason}` }); return true; }
175
182
  if (req.method !== 'POST') { send(405, { ok: false, error: 'POST only' }); return true; }
176
183
  const ctx = opts.getAcceptContext ? opts.getAcceptContext(runid) : null;
177
184
  if (!ctx) { send(404, { ok: false, error: 'no such run' }); return true; }
178
- if (ctx.live) { send(409, { ok: false, error: 'This run is still running, so there is nothing to accept yet.' }); return true; }
179
- if (!ctx.waiting) { send(409, { ok: false, error: 'This run is not waiting for your Accept (it opened no review door, the door was already answered, or it has expired).' }); return true; }
185
+ if (ctx.live) { send(409, { ok: false, error: decide === 'accept' ? 'This run is still running, so there is nothing to accept yet.' : `This run is still running, so there is nothing to ${decide} yet.` }); return true; }
186
+ if (!ctx.waiting) { send(409, { ok: false, error: decide === 'accept' ? 'This run is not waiting for your Accept (it opened no review door, the door was already answered, or it has expired).' : `This run is not waiting for your answer, so it cannot be ${decide === 'rerun' ? 'rerun' : 'paused'} (it opened no review door, the door was already answered, or it has expired).` }); return true; }
187
+ // a rerun's words ARE the gap the worker is given: empty words are refused here in plain words (the engine refuses them too, src/kinds.js normalizeHumanRuling)
188
+ const text = decide === 'rerun' && typeof body?.text === 'string' ? body.text.trim() : '';
189
+ if (decide === 'rerun' && text === '') { send(400, { ok: false, error: 'Write what you want done differently, then press Rerun. Your words are what the next attempt is given.' }); return true; }
180
190
  if (!ctx.specPath || !ctx.specHash) { send(409, { ok: false, error: "The signed job this run was judged by cannot be found on disk, so its door cannot be answered from here. Nothing was recorded." }); return true; }
181
191
  // RULED 2026-10-15: an old run whose saved job fails today's checks is refused at $0 in plain words, before any spawn
182
192
  /** @type {any} */
@@ -185,15 +195,21 @@ export function createRunRoutes(opts) {
185
195
  if (signed !== null) {
186
196
  let jv;
187
197
  try { jv = validateJob(signed, { shellCapUsd: signed.budgetUsd }); } catch { jv = { ok: false, reds: [] }; }
188
- if (!jv.ok) { send(409, { ok: false, error: OLD_JOB_ACCEPT_WORDS, reds: jv.reds.map((r) => `${r.code} at ${r.path}`) }); return true; }
198
+ if (!jv.ok) {
199
+ const words = decide === 'accept' ? OLD_JOB_ACCEPT_WORDS : (decide === 'rerun' ? OLD_JOB_RERUN_WORDS : OLD_JOB_PAUSE_WORDS);
200
+ send(409, { ok: false, error: words, reds: jv.reds.map((r) => `${r.code} at ${r.path}`) });
201
+ return true;
202
+ }
189
203
  }
190
204
  const questionOf = (/** @type {string} */ n) => (signed !== null && opts.stageQuestionsOfSpec ? opts.stageQuestionsOfSpec(signed).get(n) ?? null : null);
191
205
  const dir = dirname(ctx.specPath);
192
- const logFile = join(dir, `accept-${runid}-${Date.now()}.log`);
206
+ const logFile = join(dir, `${decide}-${runid}-${Date.now()}.log`);
193
207
  const args = [
194
- '--wait', 'systemd-inhibit', '--why=bareloop panel accept',
208
+ '--wait', 'systemd-inhibit', `--why=bareloop panel ${decide}`,
195
209
  process.execPath, bareloopBin,
196
- 'run-u', '--spec', ctx.specPath, '--door', runid, '--decide', 'accept', '--approve', ctx.specHash,
210
+ 'run-u', '--spec', ctx.specPath, '--door', runid, '--decide', decide, '--approve', ctx.specHash,
211
+ // last, so no flag lookup in the engine can land on the person's words
212
+ ...(decide === 'rerun' ? ['--text', text] : []),
197
213
  ];
198
214
  /** @type {number|null} */
199
215
  let logFd = null;
@@ -203,14 +219,18 @@ export function createRunRoutes(opts) {
203
219
  if (typeof child?.unref === 'function') child.unref();
204
220
  let done = false;
205
221
  const finish = (/** @type {number} */ code, /** @type {any} */ obj) => { if (!done) { done = true; send(code, obj); } };
206
- // an accept that outlasts the wait is still going: say so (202), never claim it passed
207
- const timer = setTimeout(() => finish(202, { ok: true, pending: true, runid, log: logFile }), opts.acceptWaitMs ?? DEFAULT_ACCEPT_WAIT_MS);
222
+ const answered = { accept: { accepted: true }, rerun: { rerunning: true }, pause: { paused: true } }[decide];
223
+ // an accept that outlasts the wait is still going: say so (202), never claim it passed. A rerun is a run: it is "started" once it outlives the settle window.
224
+ const timer = setTimeout(
225
+ () => (decide === 'rerun' ? finish(200, { ok: true, ...answered, runid, log: logFile }) : finish(202, { ok: true, pending: true, runid, log: logFile })),
226
+ decide === 'rerun' ? settleMs : (opts.acceptWaitMs ?? DEFAULT_ACCEPT_WAIT_MS),
227
+ );
208
228
  if (typeof timer.unref === 'function') timer.unref();
209
229
  if (typeof child?.once === 'function') {
210
230
  child.once('exit', (/** @type {number|null} */ code) => {
211
231
  clearTimeout(timer);
212
- if (code === 0) finish(200, { ok: true, accepted: true, runid, log: logFile });
213
- else finish(409, { ok: false, error: engineRefusalWords(readLogTail(logFile), 'accept', logFile, questionOf), log: logFile });
232
+ if (code === 0) finish(200, { ok: true, ...answered, runid, log: logFile });
233
+ else finish(409, { ok: false, error: engineRefusalWords(readLogTail(logFile), decide, logFile, questionOf), log: logFile });
214
234
  });
215
235
  child.once('error', (/** @type {Error} */ e) => {
216
236
  clearTimeout(timer);
@@ -256,15 +276,19 @@ export function createRunRoutes(opts) {
256
276
  const stopM = /^\/api\/runs\/([A-Za-z0-9._~-]+)\/stop$/.exec(pathname);
257
277
  const m = /^\/api\/runs\/([A-Za-z0-9._~-]+)\/resume$/.exec(pathname);
258
278
  const acceptM = /^\/api\/runs\/([A-Za-z0-9._~-]+)\/accept$/.exec(pathname);
279
+ const rerunM = /^\/api\/runs\/([A-Za-z0-9._~-]+)\/rerun$/.exec(pathname);
280
+ const pauseM = /^\/api\/runs\/([A-Za-z0-9._~-]+)\/pause$/.exec(pathname);
259
281
  const needsM = /^\/api\/runs\/([A-Za-z0-9._~-]+)\/needs-remove$/.exec(pathname);
260
- if (!m && !stopM && !acceptM && !needsM) return false;
282
+ if (!m && !stopM && !acceptM && !needsM && !rerunM && !pauseM) return false;
261
283
  const send = (/** @type {number} */ code, /** @type {any} */ obj) => {
262
284
  const text = JSON.stringify(obj);
263
285
  res.writeHead(code, { 'content-type': 'application/json; charset=utf-8', 'content-length': Buffer.byteLength(text) });
264
286
  res.end(text);
265
287
  };
266
288
  if (stopM) return handleStop(req, send, stopM[1]);
267
- if (acceptM) return handleAccept(req, send, acceptM[1]);
289
+ if (acceptM) return handleDoor(req, send, acceptM[1], 'accept', body);
290
+ if (rerunM) return handleDoor(req, send, rerunM[1], 'rerun', body);
291
+ if (pauseM) return handleDoor(req, send, pauseM[1], 'pause', body);
268
292
  if (needsM) return handleNeedsRemove(req, send, needsM[1]);
269
293
  if (!m) return false;
270
294
  const guard = checkHumanGuard(req, { token: opts.token, port: opts.port });
@@ -300,7 +324,7 @@ export function createRunRoutes(opts) {
300
324
  // tighten-only: a cap at or below what the run has ALREADY used buys an immediate re-halt
301
325
  // (the engine only warns), so it is refused here at $0, before anything is written or spawned
302
326
  if (typeof plan.spentUsd === 'number' && spec.budgetUsd <= plan.spentUsd) {
303
- send(400, { ok: false, error: `The money cap must be above what is already spent (${plan.spendComplete ? '' : 'at least '}$${plan.spentUsd.toFixed(2)}) — raise it, then Resume.` });
327
+ send(400, { ok: false, error: `The money cap must be above what is already spent (${plan.spendComplete ? '' : 'at least '}${panelMoney2(plan.spentUsd)}) — raise it, then Resume.` });
304
328
  return true;
305
329
  }
306
330
  if (typeof plan.wallUsedMs === 'number' && typeof spec.maxWallMs === 'number' && spec.maxWallMs <= plan.wallUsedMs) {