bareloop 0.43.0 → 0.44.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 +42 -0
- package/bareloop.context.md +52 -13
- package/package.json +2 -2
- package/src/authorflow.js +26 -11
- package/src/authoring.js +1 -1
- package/src/authorjob.js +27 -15
- package/src/authorreadout.js +7 -6
- 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/inputlook.js +38 -3
- package/src/job.js +1 -1
- package/src/kinds.js +5 -0
- package/src/pagedropwords.js +64 -0
- package/src/pagemap.js +264 -0
- package/src/pagerules.js +319 -0
- package/src/panel/authorroutes.js +34 -30
- package/src/panel/authorsession.js +29 -5
- package/src/panel/index.html +465 -65
- package/src/panel/jobplan.js +4 -2
- package/src/panel/runroutes.js +46 -22
- package/src/panel/server.js +791 -28
- package/src/panel/status.js +33 -0
- package/src/panelmoney.js +29 -0
- package/src/planrun.js +53 -4
- package/src/promptregisters.js +4 -0
- package/src/replay.js +118 -7
- package/src/rubricstep.js +123 -11
- 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 +10 -2
- 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/inputlook.d.ts +15 -0
- package/types/kinds.d.ts +5 -0
- package/types/pagedropwords.d.ts +26 -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/runroutes.d.ts +2 -2
- package/types/panel/server.d.ts +155 -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 +57 -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
|
@@ -19,24 +19,27 @@
|
|
|
19
19
|
|
|
20
20
|
import { randomBytes } from 'node:crypto';
|
|
21
21
|
import { spawn as realSpawn } from 'node:child_process';
|
|
22
|
-
import { openSync } from 'node:fs';
|
|
22
|
+
import { openSync, existsSync } from 'node:fs';
|
|
23
23
|
import { join } from 'node:path';
|
|
24
24
|
import { readFileSync } from 'node:fs';
|
|
25
25
|
import { planMatchesClose } from '../rubricstep.js';
|
|
26
26
|
import { createSession, validateJobCard, validateReuseCard, jobBoxesChanged, buildReuseSpec, cardFields, nextFreeJobName, startDestinationProblem, STEP_LABELS } from './authorsession.js';
|
|
27
27
|
import { parseInputs } from '../jobblock.js';
|
|
28
|
-
import { proveInputFiles, looksLikeRubricInput } from '../source.js';
|
|
28
|
+
import { proveInputFiles, looksLikeRubricInput, webFetchState } from '../source.js';
|
|
29
29
|
import { validateJob } from '../job.js';
|
|
30
30
|
|
|
31
31
|
/** plain words for a run whose saved job no longer passes today's `validateJob` (RULED 2026-10-15): never the raw red codes */
|
|
32
32
|
export const OLD_JOB_REUSE_WORDS = "This run was made by an older bareloop. Its saved job doesn't pass today's checks, so it can't be reused. Start a new job instead.";
|
|
33
33
|
export const OLD_JOB_RESUME_WORDS = "This run was made by an older bareloop. Its saved job doesn't pass today's checks, so it can't be resumed. Use Reuse workflow on a newer run, or start a new job.";
|
|
34
34
|
export const OLD_JOB_ACCEPT_WORDS = "This run was made by an older bareloop. Its saved job doesn't pass today's checks, so it can't be accepted. Use Reuse workflow on a newer run, or start a new job.";
|
|
35
|
+
export const OLD_JOB_RERUN_WORDS = "This run was made by an older bareloop. Its saved job doesn't pass today's checks, so it can't be rerun. Use Reuse workflow on a newer run, or start a new job.";
|
|
36
|
+
export const OLD_JOB_PAUSE_WORDS = "This run was made by an older bareloop. Its saved job doesn't pass today's checks, so it can't be paused from here. Use Reuse workflow on a newer run, or start a new job.";
|
|
35
37
|
import { checkMonthlyRoom, monthlyRefusalText } from '../monthly.js';
|
|
36
38
|
import { ConfigError } from '../config.js';
|
|
37
39
|
import { keysForDoor, keysHome } from '../keysfile.js';
|
|
38
40
|
import { endpointOf, judgePick, keyValueOf, rowsForHome, tickedLines } from '../providerrows.js';
|
|
39
41
|
import { thinkingForLine } from '../thinking.js';
|
|
42
|
+
import { panelMoney2 } from '../panelmoney.js';
|
|
40
43
|
import { apiKeyProblem, checkProviderReachable } from '../providers.js';
|
|
41
44
|
|
|
42
45
|
/** @returns {string} a fresh per-process token — never persisted, never logged */
|
|
@@ -90,6 +93,18 @@ export function checkHumanGuard(req, { token, port }) {
|
|
|
90
93
|
* one; a terminal session is kept (so its chat/hash stay readable) but no
|
|
91
94
|
* longer counts against the one-at-a-time rule.
|
|
92
95
|
*/
|
|
96
|
+
/**
|
|
97
|
+
* PRD v1.106 Part 5 Q2: the "fetching the page..." step is done once the run's own refetch has written `refrozenAt` (source.json, written last).
|
|
98
|
+
* Settled when the state is read, so nothing polls and the step never stays running after the fetch.
|
|
99
|
+
* @param {any} state
|
|
100
|
+
*/
|
|
101
|
+
export function settleFetchingStep(state) {
|
|
102
|
+
const st = Array.isArray(state?.steps) ? state.steps.find((/** @type {any} */ x) => x.id === 'fetching') : null;
|
|
103
|
+
if (!st || st.status !== 'running') return;
|
|
104
|
+
const file = join(String(state.outDir), 'source-seed', 'source.json');
|
|
105
|
+
if (existsSync(file) && !webFetchState(file, st.since).fetching) st.status = 'done';
|
|
106
|
+
}
|
|
107
|
+
|
|
93
108
|
const TERMINAL_PHASES = new Set(['refused', 'abandoned', 'error', 'signed', 'signing-failed']);
|
|
94
109
|
|
|
95
110
|
/**
|
|
@@ -131,11 +146,17 @@ export function createAuthorRoutes(opts) {
|
|
|
131
146
|
* @param {any} card the form (fields only)
|
|
132
147
|
* @param {{spec: any, workflowKey: string}|null} reuse
|
|
133
148
|
* @param {import('../jobblock.js').JobInput[]} inputs the parsed Inputs
|
|
134
|
-
* @param {{headless?: boolean
|
|
149
|
+
* @param {{headless?: boolean}} [o]
|
|
135
150
|
* @returns {Promise<{ok: true, session: ReturnType<typeof createSession>}|{ok: false, status: number, error: string}>}
|
|
136
151
|
*/
|
|
137
152
|
function startSession(card, reuse, inputs, o = {}) {
|
|
138
|
-
|
|
153
|
+
// ONE owner of "is this a rubric job on files": a reuse (and so a scheduled fire) reads it from the signed spec's verdict type,
|
|
154
|
+
// never from the card; a fresh job reads the card. A rubric job whose line 1 is a file or URL has no repo to prove inputs against;
|
|
155
|
+
// the session proves every line itself (`proveRubricInputs`), at $0, before any model call.
|
|
156
|
+
const rubric = reuse ? reuse.spec?.verdictType === 'soft-green' : card?.checkType === 'rubric';
|
|
157
|
+
if (reuse) card = { ...card, checkType: rubric ? 'rubric' : 'deterministic' };
|
|
158
|
+
const filesJob = rubric && inputs.length > 0 && looksLikeRubricInput(inputs[0].value);
|
|
159
|
+
return (filesJob ? Promise.resolve(/** @type {{ok: true}} */ ({ ok: true })) : proveInputFiles(inputs)).then((proof) => {
|
|
139
160
|
if (!proof.ok) return { ok: /** @type {false} */ (false), status: 400, error: proof.error };
|
|
140
161
|
if (!o.headless && hasLiveSession()) return { ok: /** @type {false} */ (false), status: 409, error: 'an authoring session is already live — one at a time' };
|
|
141
162
|
// the SAME Destination check the session runs, at $0, BEFORE the session (and its folder) exists
|
|
@@ -299,6 +320,7 @@ export function createAuthorRoutes(opts) {
|
|
|
299
320
|
if (pathname === '/api/author/live') {
|
|
300
321
|
if (req.method !== 'GET') { send(405, { ok: false, error: 'GET only' }); return true; }
|
|
301
322
|
const live = [...sessions.values()].find((s) => !TERMINAL_PHASES.has(s.state.phase));
|
|
323
|
+
if (live) settleFetchingStep(live.state);
|
|
302
324
|
send(200, { ok: true, sessionId: live ? live.state.id : null, state: live ? live.state : null });
|
|
303
325
|
return true;
|
|
304
326
|
}
|
|
@@ -357,10 +379,7 @@ export function createAuthorRoutes(opts) {
|
|
|
357
379
|
// A miss is a plain 400 naming the line, shown under the card exactly like every other refused start.
|
|
358
380
|
const ip = parseInputs(card.inputs);
|
|
359
381
|
if (!ip.ok) { send(400, { ok: false, error: ip.error }); return true; }
|
|
360
|
-
|
|
361
|
-
// line itself (`proveRubricInputs`), at $0, before any model call
|
|
362
|
-
const filesJob = reuse === null && card?.checkType === 'rubric' && looksLikeRubricInput(ip.inputs[0].value);
|
|
363
|
-
startSession(card, reuse, ip.inputs, { filesJob }).then((r) => {
|
|
382
|
+
startSession(card, reuse, ip.inputs).then((r) => {
|
|
364
383
|
if (!r.ok) { send(r.status, { ok: false, error: r.error }); return; }
|
|
365
384
|
send(200, { ok: true, sessionId: r.session.id, reuse: reuse !== null, state: r.session.state });
|
|
366
385
|
});
|
|
@@ -375,6 +394,7 @@ export function createAuthorRoutes(opts) {
|
|
|
375
394
|
|
|
376
395
|
if (sub === null) {
|
|
377
396
|
if (req.method !== 'GET') { send(405, { ok: false, error: 'GET only' }); return true; }
|
|
397
|
+
settleFetchingStep(session.state);
|
|
378
398
|
send(200, { ok: true, state: session.state });
|
|
379
399
|
return true;
|
|
380
400
|
}
|
|
@@ -417,28 +437,7 @@ export function createAuthorRoutes(opts) {
|
|
|
417
437
|
};
|
|
418
438
|
}
|
|
419
439
|
|
|
420
|
-
|
|
421
|
-
* The panel's OWN 2-decimal money render for a chat line — hamr's ruling
|
|
422
|
-
* 2026-09-28 (2nd addendum, panel money 2-decimals): a positive amount below
|
|
423
|
-
* a cent reads `<$0.01`, never `$0.00` (a real cost must never read as
|
|
424
|
-
* free). This is the server-side (Node, chat-text) sibling of
|
|
425
|
-
* `src/panel/index.html`'s own inline `panelMoney` — the two can't share
|
|
426
|
-
* code (one runs in the browser as inlined script, the other in Node), the
|
|
427
|
-
* same duplication `src/replay.js`'s `money()`/`index.html`'s `money()`
|
|
428
|
-
* already carry, so both apply the identical rule rather than drift.
|
|
429
|
-
* @param {number} n a non-negative dollar amount
|
|
430
|
-
* @returns {string}
|
|
431
|
-
*/
|
|
432
|
-
export function panelMoney2(n) {
|
|
433
|
-
if (n > 0 && n < 0.01) return '<$0.01';
|
|
434
|
-
// two-step rounding (matching index.html's own panelMoney): clean to
|
|
435
|
-
// 6-decimal precision first (the same precision src/text.js's tallyCalls
|
|
436
|
-
// sums to) before rounding to cents, so a summed float landing just under
|
|
437
|
-
// an exact cent boundary is never mis-rounded.
|
|
438
|
-
const clean = Math.round(n * 1e6) / 1e6;
|
|
439
|
-
const cents = Math.round(clean * 100 + 1e-6);
|
|
440
|
-
return `$${(cents / 100).toFixed(2)}`;
|
|
441
|
-
}
|
|
440
|
+
export { panelMoney2 };
|
|
442
441
|
|
|
443
442
|
/**
|
|
444
443
|
* THE ONLY function in this file (or anywhere in the panel) that spawns a
|
|
@@ -520,5 +519,10 @@ export function signRun(session, claimedHash, o) {
|
|
|
520
519
|
? `drafting spent ${draftIncomplete ? 'at least ' : ''}${panelMoney2(draftSpentUsd)} (folds out of the run's own cap)`
|
|
521
520
|
: '';
|
|
522
521
|
if (Array.isArray(session.state.steps)) session.state.steps.push({ id: 'signed', label: STEP_LABELS.signed, status: 'done', detail: draftLine });
|
|
522
|
+
// PRD v1.106 Part 5 Q2: a run with a web input fetches the page again before its log exists (3-5 s of silence): say so, next to the sign step
|
|
523
|
+
const sinceIso = new Date().toISOString();
|
|
524
|
+
if (Array.isArray(session.state.steps) && webFetchState(join(session.state.outDir, 'source-seed', 'source.json'), sinceIso).hasWeb) {
|
|
525
|
+
session.state.steps.push({ id: 'fetching', label: STEP_LABELS.fetching, status: 'running', since: sinceIso });
|
|
526
|
+
}
|
|
523
527
|
return { ok: true, job: session.state.resolvedSpecPath };
|
|
524
528
|
}
|
|
@@ -53,6 +53,7 @@ import { closeJudges } from '../kinds.js';
|
|
|
53
53
|
import { RUBRIC_LANG } from '../authoring.js';
|
|
54
54
|
import { rubricGoal, reconcileRubricPlan } from '../rubricstep.js';
|
|
55
55
|
import { redactSecrets } from '../validate.js';
|
|
56
|
+
import { isDropIt, isAgree } from '../pagemap.js';
|
|
56
57
|
import { tallyCalls } from '../text.js';
|
|
57
58
|
import { writeDraftSpend, appendDraftLog } from '../draftspend.js';
|
|
58
59
|
import { PLAIN_PROPOSAL_STOPS, proposalStopText, signingStopText, draftStopText, redsRecord, declarationRecord, calibrationSummary, tryRefusedText } from '../authorreadout.js';
|
|
@@ -85,6 +86,7 @@ export const STEP_LABELS = Object.freeze({
|
|
|
85
86
|
gates: 'checking signing gates',
|
|
86
87
|
hash: 'generating hash',
|
|
87
88
|
signed: 'signed hash',
|
|
89
|
+
fetching: 'fetching the page…',
|
|
88
90
|
});
|
|
89
91
|
|
|
90
92
|
/**
|
|
@@ -539,7 +541,20 @@ export function createSession(card, deps = {}) {
|
|
|
539
541
|
* @param {{kind: string, [k: string]: any}} step
|
|
540
542
|
* @returns {Promise<string|null>}
|
|
541
543
|
*/
|
|
542
|
-
const ask = (step) => new Promise((
|
|
544
|
+
const ask = (step) => new Promise((resolveFn0) => {
|
|
545
|
+
// PRD v1.106 Part 5 Q1: every question the person answers (the confirm card's, the early look ask, the last-try ask) is one `human` row in the
|
|
546
|
+
// drafting log, with the answer secret-masked BEFORE it is written. Written only when the answer exists (a blank one re-asks and leaves nothing).
|
|
547
|
+
const resolveFn = step.kind !== 'answer' ? resolveFn0 : (/** @type {string|null} */ value) => {
|
|
548
|
+
const said = value === null ? '' : redactSecrets(String(value).trim());
|
|
549
|
+
if (said !== '' && typeof step.question === 'string') {
|
|
550
|
+
const running = state.steps.findLastIndex((/** @type {any} */ x) => x.status === 'running');
|
|
551
|
+
appendDraftLog(outDir, {
|
|
552
|
+
kind: 'human', no: running === -1 ? Math.max(0, state.steps.length - 1) : running, source: typeof step.source === 'string' ? step.source : 'confirm',
|
|
553
|
+
question: redactSecrets(step.question), answer: said, ...(Number.isInteger(step.line) ? { line: step.line } : {}), at: new Date().toISOString(),
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
resolveFn0(value);
|
|
557
|
+
};
|
|
543
558
|
state.pendingAsk = step;
|
|
544
559
|
resolvePending = resolveFn;
|
|
545
560
|
if (step.kind === 'language') say('bot', `${step.field?.prompt ?? 'Which language is this job about?'} (${(step.candidates ?? []).join(', ')})`);
|
|
@@ -639,6 +654,8 @@ export function createSession(card, deps = {}) {
|
|
|
639
654
|
kind: 'call', no: callNo, step: state.steps[callNo]?.id ?? null, label: call.label, model: draftIdentity?.model ?? null,
|
|
640
655
|
thinking: draftThinkingPlan === null ? null : draftThinkingPlan.mode, thinkingSupport: draftThinkingPlan === null ? null : draftThinkingPlan.support,
|
|
641
656
|
...(call.thinkingStatus === undefined ? {} : { thinkingStatus: call.thinkingStatus }),
|
|
657
|
+
// idea A core: the page-word map's scrubbed answer (src/pagemap.js) - unsigned, kept so a run can be replayed at $0
|
|
658
|
+
...(typeof call.raw === 'string' ? { raw: call.raw } : {}),
|
|
642
659
|
costUsd: call.costUsd ?? null, unpricedRounds: call.unpricedRounds ?? 0, at: nowIso,
|
|
643
660
|
});
|
|
644
661
|
try {
|
|
@@ -1021,27 +1038,34 @@ export function createSession(card, deps = {}) {
|
|
|
1021
1038
|
// ask path as every other (the chat box + Send); the answer is REQUIRED (a blank one asks again, there is no skip) and is
|
|
1022
1039
|
// added WORD FOR WORD as a `~` rule on the job line it names — never rewritten. The lines are mutated in place, so the
|
|
1023
1040
|
// signed `jobLines`, the answers text and the plan card all read the same words.
|
|
1024
|
-
const answerQuestions = async (/** @type {{questions: {line: number, question: string}[], dropped: {reason: string, raw: any}[], confirmed: any}} */ o) => {
|
|
1041
|
+
const answerQuestions = async (/** @type {{questions: {line: number, question: string, rule?: number, source?: string, ask?: string, reading?: string}[], dropped: {reason: string, raw: any}[], confirmed: any}} */ o) => {
|
|
1025
1042
|
if (o.dropped.length) appendDraftLog(outDir, { kind: 'questions-dropped', no: Math.max(0, state.steps.length - 1), dropped: o.dropped, at: new Date().toISOString() });
|
|
1026
1043
|
const total = o.questions.length;
|
|
1044
|
+
/** @type {{line: number, rule: number}[]} */
|
|
1045
|
+
const left = [];
|
|
1027
1046
|
for (let k = 0; k < total; k += 1) {
|
|
1028
1047
|
const q = o.questions[k];
|
|
1029
1048
|
let text = '';
|
|
1030
1049
|
for (;;) {
|
|
1031
|
-
const a = await ask({ kind: 'answer', index: k + 1, total, question: q.question, line: q.line });
|
|
1050
|
+
const a = await ask({ kind: 'answer', source: typeof q.source === 'string' ? q.source : 'try-3', index: k + 1, total, question: q.question, line: q.line });
|
|
1032
1051
|
if (a === null) return null;
|
|
1033
1052
|
// one `~` line is one line: the person's own words, newlines folded to spaces, nothing else touched
|
|
1034
1053
|
text = String(a).replace(/\s*\n\s*/g, ' ').trim();
|
|
1035
1054
|
if (text) break;
|
|
1036
1055
|
say('bot', 'An answer is needed here — there is no skip. Type it in the box below and press Send.');
|
|
1037
1056
|
}
|
|
1057
|
+
// PRD v1.107 Part 5b: a code-forced look question about a `~` rule. A can't-check ask answered "drop it" adds NO rule; the caller puts that
|
|
1058
|
+
// rule on the look's Not checked list. A reading ask answered "agree" adds the code's reading, in plain words, as the `~` rule (so the
|
|
1059
|
+
// bound is now the person's own words); anything else typed is the person's own rule, word for word.
|
|
1060
|
+
if (q.source === 'look' && q.ask === 'reading' && typeof q.reading === 'string' && isAgree(text)) text = q.reading;
|
|
1061
|
+
if (q.source === 'look' && q.ask === 'cant-check' && Number.isInteger(q.rule) && isDropIt(text)) { left.push({ line: q.line, rule: /** @type {number} */ (q.rule) }); continue; }
|
|
1038
1062
|
const line = jobLines.find((l) => l.n === q.line);
|
|
1039
1063
|
if (line) { line.rule = line.rule ? `${line.rule}; ${text}` : text; line.rules.push(text); }
|
|
1040
1064
|
}
|
|
1041
1065
|
draft.jobLines = signedJobLines(jobLines);
|
|
1042
1066
|
answers[1] = renderJobLines(jobLines);
|
|
1043
|
-
state.messages.push({ ...planMessage(o.confirmed ?? {}, jobLines), text: 'Your answers are in as ~ rules on those job lines. Drafting again.' });
|
|
1044
|
-
return { jobLines, answers };
|
|
1067
|
+
state.messages.push({ ...planMessage(o.confirmed ?? {}, jobLines), text: left.length === total ? 'Noted: those stay Not checked. Drafting.' : 'Your answers are in as ~ rules on those job lines. Drafting again.' });
|
|
1068
|
+
return { jobLines, answers, left };
|
|
1045
1069
|
};
|
|
1046
1070
|
|
|
1047
1071
|
state.phase = 'drafting';
|