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.
- package/CHANGELOG.md +60 -0
- package/bareloop.context.md +53 -13
- package/package.json +2 -2
- package/src/authorflow.js +27 -12
- package/src/authoring.js +1 -1
- package/src/authorjob.js +52 -16
- package/src/authorreadout.js +12 -6
- package/src/authorrun.js +8 -2
- package/src/declaredclose.js +4 -1
- package/src/draftspend.js +20 -2
- package/src/facts.js +54 -9
- package/src/factwords.js +58 -6
- package/src/fitcheck.js +166 -0
- package/src/inputlook.js +38 -3
- package/src/job.js +1 -1
- package/src/kinds.js +5 -0
- package/src/pagedropwords.js +110 -0
- package/src/pagedry.js +125 -0
- package/src/pagemap.js +269 -0
- package/src/pagerules.js +318 -0
- package/src/panel/authorroutes.js +34 -30
- package/src/panel/authorsession.js +66 -11
- package/src/panel/index.html +469 -65
- package/src/panel/jobplan.js +14 -7
- package/src/panel/runroutes.js +46 -22
- package/src/panel/server.js +792 -28
- package/src/panel/status.js +33 -0
- package/src/panelmoney.js +29 -0
- package/src/planrun.js +53 -4
- package/src/promptregisters.js +8 -0
- package/src/replay.js +118 -7
- package/src/rubricstep.js +157 -15
- package/src/runlist.js +2 -1
- package/src/schedulewords.js +6 -3
- package/src/source.js +18 -0
- package/src/thinking.js +63 -2
- package/src/userrun.js +28 -14
- package/types/authorflow.d.ts +42 -6
- package/types/authorjob.d.ts +20 -4
- package/types/declaredclose.d.ts +2 -0
- package/types/draftspend.d.ts +27 -1
- package/types/facts.d.ts +15 -0
- package/types/factwords.d.ts +26 -3
- package/types/fitcheck.d.ts +113 -0
- package/types/inputlook.d.ts +15 -0
- package/types/kinds.d.ts +5 -0
- package/types/pagedropwords.d.ts +39 -0
- package/types/pagedry.d.ts +37 -0
- package/types/pagemap.d.ts +154 -0
- package/types/pagerules.d.ts +86 -0
- package/types/panel/authorroutes.d.ts +17 -14
- package/types/panel/authorsession.d.ts +4 -2
- package/types/panel/jobplan.d.ts +5 -2
- package/types/panel/runroutes.d.ts +2 -2
- package/types/panel/server.d.ts +156 -2
- package/types/panel/status.d.ts +51 -0
- package/types/panelmoney.d.ts +5 -0
- package/types/replay.d.ts +21 -1
- package/types/rubricstep.d.ts +62 -1
- package/types/runlist.d.ts +4 -1
- package/types/schedulewords.d.ts +4 -2
- package/types/source.d.ts +11 -0
- package/types/thinking.d.ts +39 -0
- package/types/userrun.d.ts +4 -0
package/src/panel/jobplan.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
135
|
+
lines, loose, notChecked, readNotes, alwaysOn: [...(plan?.protections ?? [])], older: false,
|
|
129
136
|
};
|
|
130
137
|
}
|
package/src/panel/runroutes.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
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
|
|
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
|
|
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) {
|
|
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,
|
|
206
|
+
const logFile = join(dir, `${decide}-${runid}-${Date.now()}.log`);
|
|
193
207
|
const args = [
|
|
194
|
-
'--wait', 'systemd-inhibit',
|
|
208
|
+
'--wait', 'systemd-inhibit', `--why=bareloop panel ${decide}`,
|
|
195
209
|
process.execPath, bareloopBin,
|
|
196
|
-
'run-u', '--spec', ctx.specPath, '--door', runid, '--decide',
|
|
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
|
-
|
|
207
|
-
|
|
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,
|
|
213
|
-
else finish(409, { ok: false, error: engineRefusalWords(readLogTail(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
|
|
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 '}
|
|
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) {
|