bareloop 0.46.0 → 0.47.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 +40 -0
- package/README.md +2 -0
- package/bareloop.context.md +38 -17
- package/package.json +1 -1
- package/src/authorreadout.js +47 -12
- package/src/cli.js +29 -45
- package/src/launcher.js +409 -0
- package/src/learn.js +24 -16
- package/src/learnstore.js +9 -17
- package/src/opener.js +30 -0
- package/src/panel/authorroutes.js +4 -3
- package/src/panel/authorsession.js +5 -7
- package/src/panel/index.html +13 -1
- package/src/panel/schedulestart.js +5 -0
- package/src/panel/server.js +57 -11
- package/src/panel/settingsroutes.js +3 -17
- package/src/panel/status.js +11 -0
- package/src/stagewords.js +18 -17
- package/src/tty-colour.js +27 -0
- package/src/userrun.js +2 -2
- package/types/authorreadout.d.ts +7 -0
- package/types/cli.d.ts +2 -1
- package/types/launcher.d.ts +234 -0
- package/types/learn.d.ts +8 -7
- package/types/learnstore.d.ts +4 -5
- package/types/opener.d.ts +12 -0
- package/types/panel/authorroutes.d.ts +1 -0
- package/types/panel/server.d.ts +24 -0
- package/types/panel/status.d.ts +8 -0
- package/types/stagewords.d.ts +16 -15
- package/types/tty-colour.d.ts +33 -0
package/src/panel/index.html
CHANGED
|
@@ -2910,7 +2910,17 @@
|
|
|
2910
2910
|
});
|
|
2911
2911
|
});
|
|
2912
2912
|
if(sum){
|
|
2913
|
-
|
|
2913
|
+
// count DISTINCT hints across the draft and replan blocks: the same ledger row (ids[i], parallel to hints[i]) handed twice is one hint
|
|
2914
|
+
var seen = {};
|
|
2915
|
+
var first = 0;
|
|
2916
|
+
items.forEach(function(it){
|
|
2917
|
+
var hs = Array.isArray(it.hints) ? it.hints : [];
|
|
2918
|
+
var byId = Array.isArray(it.ids) && it.ids.length === hs.length;
|
|
2919
|
+
hs.forEach(function(h, i){
|
|
2920
|
+
var key = byId ? "id:" + it.ids[i] : "n:" + it.phase + ":" + h.n;
|
|
2921
|
+
if(!seen[key]){ seen[key] = true; first += 1; }
|
|
2922
|
+
});
|
|
2923
|
+
});
|
|
2914
2924
|
var label = items.length ? head + " \u00B7 " + first + (first === 1 ? " hint" : " hints") : head;
|
|
2915
2925
|
// no arrow (hamr 2026-10-23): the state chip leads; aria-expanded carries open/closed. The same `[✓] word` state markup the schedule/step headers wear (stateDotHTML), then the plain-words title
|
|
2916
2926
|
var paint = function(){ sum.innerHTML = stateDotHTML(glyphClass("\u2713"), "learnings", "audit-learnings-state") + " " + escapeXml(label); };
|
|
@@ -4939,6 +4949,8 @@
|
|
|
4939
4949
|
var suffix = reach.modelListed === false ? " (model not listed)" : "";
|
|
4940
4950
|
return { keyOk: true, ok: true, text: "[✓] " + r.envKey + " found · " + modelId + " reachable" + suffix };
|
|
4941
4951
|
}
|
|
4952
|
+
// a blank Base URL is nothing to probe, not flakiness: say the server's own plain sentence (src/modellist.js owns it)
|
|
4953
|
+
if(reach.status === "no-base-url" && reach.note) return { keyOk: true, ok: false, text: "[✗] " + reach.note };
|
|
4942
4954
|
return { keyOk: true, ok: false, text: reachStatusText(reach.status) };
|
|
4943
4955
|
}
|
|
4944
4956
|
function refreshModelStatus(){
|
|
@@ -46,6 +46,11 @@ export function perFireSpec(schedule, cause) {
|
|
|
46
46
|
if (jobSpecHash(schedule.spec) !== schedule.specHash) return { ok: false, why: 'The saved schedule no longer matches what you signed. Nothing started.' };
|
|
47
47
|
const spec = JSON.parse(JSON.stringify(schedule.spec));
|
|
48
48
|
if (cause.kind === 'file') {
|
|
49
|
+
// the card text is `label: value` lines: a name holding a line break would read back as ANOTHER Inputs line, and edge whitespace
|
|
50
|
+
// is trimmed to a path that does not exist (a leading space in the file name sits mid-path and is harmless). Refused with its reason, never rewritten.
|
|
51
|
+
if (/[\r\n]/.test(cause.path) || cause.path !== cause.path.trim()) {
|
|
52
|
+
return { ok: false, why: `The file name ${JSON.stringify(cause.path)} has a line break or a space at its end, which the job form cannot carry. Rename the file. Nothing started.` };
|
|
53
|
+
}
|
|
49
54
|
const first = Array.isArray(spec.inputs) ? spec.inputs[0] : null;
|
|
50
55
|
if (!first) return { ok: false, why: 'The saved schedule has no input for the file to replace. Nothing started.' };
|
|
51
56
|
spec.inputs = [{ ...first, value: cause.path }, ...spec.inputs.slice(1)];
|
package/src/panel/server.js
CHANGED
|
@@ -68,7 +68,7 @@ import { SHAPES } from '../providershapes.js';
|
|
|
68
68
|
import { resolveProvider } from '../providers.js';
|
|
69
69
|
import { readResume, checkpointAgeGate, CHECKPOINT_OUTCOMES } from '../reuse.js';
|
|
70
70
|
import {
|
|
71
|
-
statusFor, partStatusFor, GOAL_MET_LINE, doorViewOf, ACTOR_MARKER, DOOR_WORDS,
|
|
71
|
+
statusFor, partStatusFor, GOAL_MET_LINE, doorViewOf, ACTOR_MARKER, DOOR_WORDS, isReusableGreen,
|
|
72
72
|
} from './status.js';
|
|
73
73
|
import { thinkingJobText } from '../thinking.js';
|
|
74
74
|
import { judgedFilesOf, lineRules, stepFileRel, proseNotCheckedItems } from '../rubricstep.js';
|
|
@@ -82,13 +82,24 @@ const HERE = dirname(fileURLToPath(import.meta.url));
|
|
|
82
82
|
/** bareloop's own version, from its package.json (read each time the page is served); 'unknown' if it cannot be read, never a guess
|
|
83
83
|
* @param {string} [file] test seam: another package.json
|
|
84
84
|
* @returns {string} */
|
|
85
|
-
function packageVersion(file = join(HERE, '..', '..', 'package.json')) {
|
|
85
|
+
export function packageVersion(file = join(HERE, '..', '..', 'package.json')) {
|
|
86
86
|
try {
|
|
87
87
|
const v = JSON.parse(readFileSync(file, 'utf8')).version;
|
|
88
88
|
return typeof v === 'string' && v.length > 0 ? v : 'unknown';
|
|
89
89
|
} catch { return 'unknown'; }
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* `GET /api/whoami` body. `drafts` counts the person's drafting sessions that are not finished (the headless scheduled ones are not drafts).
|
|
94
|
+
* @param {{ port: number, packageJsonPath?: string, authorRoutes?: ReturnType<typeof createAuthorRoutes>, schedules?: ReturnType<typeof createScheduleDoor> }} opts
|
|
95
|
+
*/
|
|
96
|
+
function whoamiPayload(opts) {
|
|
97
|
+
const drafts = opts.authorRoutes ? opts.authorRoutes.openDrafts() : 0;
|
|
98
|
+
let schedules = 0;
|
|
99
|
+
try { schedules = opts.schedules ? opts.schedules.scan().length : 0; } catch { schedules = 0; }
|
|
100
|
+
return { app: 'bareloop', version: packageVersion(opts.packageJsonPath), pid: process.pid, port: opts.port, drafts, schedules };
|
|
101
|
+
}
|
|
102
|
+
|
|
92
103
|
/** Largest POST body the panel buffers (job cards are short prose); a bigger one is refused with 413. */
|
|
93
104
|
const MAX_BODY_BYTES = 1024 * 1024;
|
|
94
105
|
|
|
@@ -542,7 +553,7 @@ function endedBase(summary, death, o) {
|
|
|
542
553
|
if (o.destinationRefused) {
|
|
543
554
|
// Reuse workflow only on a run that earned its green (hamr 2026-10-08, as below); every ending offers Reuse workflow
|
|
544
555
|
// (withReuseAction adds it) and is told to change the job
|
|
545
|
-
const earned = outcome
|
|
556
|
+
const earned = isReusableGreen(outcome);
|
|
546
557
|
return {
|
|
547
558
|
reason: 'Goal met, but the output could not be delivered.',
|
|
548
559
|
next: earned ? 'Fix the destination, then Reuse workflow.' : CHANGE,
|
|
@@ -550,10 +561,9 @@ function endedBase(summary, death, o) {
|
|
|
550
561
|
actions: earned ? REUSE : [],
|
|
551
562
|
};
|
|
552
563
|
}
|
|
553
|
-
//
|
|
554
|
-
//
|
|
555
|
-
|
|
556
|
-
if (outcome !== 'green') return { reason: 'Goal met.', next: CHANGE, line: GOAL_MET_LINE, actions: [] };
|
|
564
|
+
// a reusable green (isReusableGreen: green or already-green, hamr 2026-10-15) says nothing is left to do; every ending offers
|
|
565
|
+
// Reuse workflow (withReuseAction adds it). `satisfied` is a close VERDICT, never a run outcome, so it is not one.
|
|
566
|
+
if (!isReusableGreen(outcome)) return { reason: 'Goal met.', next: CHANGE, line: GOAL_MET_LINE, actions: [] };
|
|
557
567
|
return { reason: 'Goal met.', next: 'Nothing to do.', line: GOAL_MET_LINE, actions: REUSE };
|
|
558
568
|
}
|
|
559
569
|
if (outcome === 'cap-halt') {
|
|
@@ -1057,6 +1067,25 @@ export function runsListPayload(opts = {}) {
|
|
|
1057
1067
|
return { runs, jobs, scheduled, gaveUp, limits: RUNS_LIST_LIMITS };
|
|
1058
1068
|
}
|
|
1059
1069
|
|
|
1070
|
+
/**
|
|
1071
|
+
* The runs list as the panel builds it, for a caller with no panel running (the launcher header): the same `runsListPayload` over a
|
|
1072
|
+
* schedule door built read-only (no scheduler, no timer), so Needs you = the rows tagged `needs` + the schedules that gave up, ONE owner.
|
|
1073
|
+
* @param {{ home?: string, now?: number, env?: Record<string, string|undefined> }} [opts]
|
|
1074
|
+
* @returns {{ runs: any[], gaveUp: any[] }}
|
|
1075
|
+
*/
|
|
1076
|
+
export function runsListForCount(opts = {}) {
|
|
1077
|
+
const home = opts.home;
|
|
1078
|
+
const schedules = createScheduleDoor({
|
|
1079
|
+
...(home ? { home } : {}),
|
|
1080
|
+
jobRuns: (job) => jobRunsOf(job, { home }),
|
|
1081
|
+
startFrom: (runid) => getStartFrom(runid, { home }),
|
|
1082
|
+
jobOfRun: (runid) => readRunList({ home }).rows.find((r) => r && r.runid === runid)?.job ?? null,
|
|
1083
|
+
rowsFor: () => rowsForHome(keysHome(home), keysForDoor({ env: opts.env ?? {}, keysHome: home }).env),
|
|
1084
|
+
});
|
|
1085
|
+
const { runs, gaveUp } = runsListPayload({ home, ...(opts.now !== undefined ? { now: opts.now } : {}), schedules });
|
|
1086
|
+
return { runs, gaveUp };
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1060
1089
|
/**
|
|
1061
1090
|
* The run's DRAFTING PART (hamr 2026-10-06, option A), or null: it exists only when the run came out of a panel
|
|
1062
1091
|
* session whose drafting log carries a metered call (src/draftspend.js, `draftingPartFor`) and the spine carries the
|
|
@@ -1530,7 +1559,8 @@ function scheduleExtrasFor(runid, job, rows, nos, opts, view) {
|
|
|
1530
1559
|
}
|
|
1531
1560
|
const got = hit ?? fallback;
|
|
1532
1561
|
const no = got ? nos.get(got.r.runid) : undefined;
|
|
1533
|
-
|
|
1562
|
+
// events: a fire refused at the run-start fetch has no spine, only its settled event; without them it has no status word
|
|
1563
|
+
const word = got ? summarizeRow(got.r, Date.now(), undefined, readRunList(opts).events).status?.word : undefined;
|
|
1534
1564
|
lastWords = `last ${dateTimeWords(Number(lastRow.at))}${no ? ` · run-${no}` : ''}${word ? ` · ${word}` : ''}${got?.rec.catchUp === true ? ' · catch-up' : ''}`;
|
|
1535
1565
|
} else {
|
|
1536
1566
|
lastWords = `last ${dateTimeWords(Number(lastRow.at))} · ${skippedWords(lastRow, true)}`;
|
|
@@ -3514,7 +3544,7 @@ export function getStartFrom(runid, opts = {}) {
|
|
|
3514
3544
|
// T D.2: a SCHEDULED run's green reads `green-held` until the person accepts it, so that same refusal holds it unchanged
|
|
3515
3545
|
outcome: (() => {
|
|
3516
3546
|
const o = legsOf(records).at(-1)?.outcome ?? null;
|
|
3517
|
-
return o
|
|
3547
|
+
return isReusableGreen(o) && heldScheduled(records, triggerRecordOf(row)) ? 'green-held' : o;
|
|
3518
3548
|
})(),
|
|
3519
3549
|
card,
|
|
3520
3550
|
from: 'signed job',
|
|
@@ -3579,7 +3609,7 @@ export function listReuseJobs(opts = {}) {
|
|
|
3579
3609
|
try { records = parseJsonl(row.spine).records; } catch { continue; }
|
|
3580
3610
|
const outcome = legsOf(records).at(-1)?.outcome ?? null;
|
|
3581
3611
|
// a real green or an already-green (hamr 2026-10-15: if it is already fixed and nothing is to be done, it is green and can be reused)
|
|
3582
|
-
if (outcome
|
|
3612
|
+
if (!isReusableGreen(outcome)) continue;
|
|
3583
3613
|
if (heldScheduled(records, triggerRecordOf(row))) continue; // T D.2: a scheduled green waits for the person's Accept
|
|
3584
3614
|
const signed = signedSpecForRun(row, records);
|
|
3585
3615
|
if (!signed) continue;
|
|
@@ -3827,6 +3857,13 @@ export function handleRequest(req, res, opts) {
|
|
|
3827
3857
|
return;
|
|
3828
3858
|
}
|
|
3829
3859
|
|
|
3860
|
+
// L (PANEL-BUILD.md addendum 2026-10-24): who is answering here — the launcher's probe (`bareloop start|stop|status`). Read-only, no token;
|
|
3861
|
+
// the Host guard above already holds. `drafts` = open drafting sessions (in memory, lost on stop); `schedules` = jobs with a schedule on.
|
|
3862
|
+
if (pathname === '/api/whoami') {
|
|
3863
|
+
send(200, whoamiPayload(opts));
|
|
3864
|
+
return;
|
|
3865
|
+
}
|
|
3866
|
+
|
|
3830
3867
|
if (pathname === '/api/runs') { send(200, runsListPayload({ home: opts.home, schedules: opts.schedules })); return; }
|
|
3831
3868
|
|
|
3832
3869
|
const runMatch = /^\/api\/runs\/([^/]+)(\/(audit|job|rounds|result|download))?$/.exec(pathname);
|
|
@@ -3895,6 +3932,15 @@ export function handleRequest(req, res, opts) {
|
|
|
3895
3932
|
sendText(res, 404, 'not found');
|
|
3896
3933
|
}
|
|
3897
3934
|
|
|
3935
|
+
/**
|
|
3936
|
+
* The ONE spelling of the panel's own address (the launcher prints and opens it too).
|
|
3937
|
+
* @param {number} boundPort
|
|
3938
|
+
* @returns {string}
|
|
3939
|
+
*/
|
|
3940
|
+
export function panelUrl(boundPort) {
|
|
3941
|
+
return `http://127.0.0.1:${boundPort}`;
|
|
3942
|
+
}
|
|
3943
|
+
|
|
3898
3944
|
/**
|
|
3899
3945
|
* Start the panel server. Binds `127.0.0.1` ONLY. A taken port is a LOUD,
|
|
3900
3946
|
* non-zero-exit failure — this never falls back to another port (see file
|
|
@@ -4057,7 +4103,7 @@ export async function panelMain(argv, ctx) {
|
|
|
4057
4103
|
}
|
|
4058
4104
|
try {
|
|
4059
4105
|
const { port: boundPort } = await createPanelServer({ port, home: ctx.runlistHome, env: ctx.env });
|
|
4060
|
-
ctx.out(`bareloop panel — read-only,
|
|
4106
|
+
ctx.out(`bareloop panel — read-only, ${panelUrl(boundPort)} (Ctrl-C to stop)`);
|
|
4061
4107
|
// never resolves on its own — the process stays up until killed, same
|
|
4062
4108
|
// shape any other long-running dev server takes.
|
|
4063
4109
|
await new Promise(() => {});
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
readConfig, updateConfig, ConfigError, providerFieldProblem, cleanBaseUrl, modelsProblem,
|
|
22
22
|
} from '../config.js';
|
|
23
23
|
import { spendSummary, monthlyLimitOf } from '../monthly.js';
|
|
24
|
-
import {
|
|
24
|
+
import { openInOs } from '../opener.js';
|
|
25
25
|
import { loadKeysEnv, keysFilePath, keysHome, filledKeyNames, ensureKeysFile } from '../keysfile.js';
|
|
26
26
|
import { SHAPES } from '../providershapes.js';
|
|
27
27
|
import { keyRows, effectiveModels, usableKey, rowNamesOf } from '../providerrows.js';
|
|
@@ -208,24 +208,10 @@ export function createSettingsRoutes(opts) {
|
|
|
208
208
|
}
|
|
209
209
|
const dir = keysHome(opts.home);
|
|
210
210
|
const cannot = () => reply(200, { ok: true, opened: false, path: dir });
|
|
211
|
-
const platform = opts.platform ?? process.platform;
|
|
212
|
-
const cmd = platform === 'linux' ? 'xdg-open' : (platform === 'darwin' ? 'open' : null);
|
|
213
211
|
// under a test process (NODE_ENV=test or `node --test`) nothing is spawned unless a test injected an opener
|
|
214
212
|
const underTest = process.env.NODE_ENV === 'test' || process.env.NODE_TEST_CONTEXT !== undefined;
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
return new Promise((resolve) => {
|
|
218
|
-
let child;
|
|
219
|
-
try { child = opener(cmd, [dir], { detached: true, stdio: 'ignore' }); } catch { resolve(cannot()); return; }
|
|
220
|
-
let settled = false;
|
|
221
|
-
child.once('error', () => { if (!settled) { settled = true; resolve(cannot()); } });
|
|
222
|
-
child.once('spawn', () => {
|
|
223
|
-
if (settled) return;
|
|
224
|
-
settled = true;
|
|
225
|
-
if (typeof child.unref === 'function') child.unref();
|
|
226
|
-
resolve(reply(200, { ok: true, opened: true, path: dir }));
|
|
227
|
-
});
|
|
228
|
-
});
|
|
213
|
+
return openInOs(dir, { platform: opts.platform, spawnFn: opts.openFolderSpawn, underTest })
|
|
214
|
+
.then((opened) => (opened ? reply(200, { ok: true, opened: true, path: dir }) : cannot()));
|
|
229
215
|
}
|
|
230
216
|
|
|
231
217
|
/**
|
package/src/panel/status.js
CHANGED
|
@@ -20,6 +20,17 @@ import { capKindOf } from '../legs.js';
|
|
|
20
20
|
/** the sentence a passed run carries as its reason (the Ended block's `line`) */
|
|
21
21
|
export const GOAL_MET_LINE = 'goal met';
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* ONE spelling of "this run's outcome is a green that can be reused" (the Reuse picker, the Reuse start/prefill, the Ended block's
|
|
25
|
+
* next line). hamr 2026-10-15: an `already-green` (fixed before any work) counts. `satisfied` is a close verdict, never a run outcome.
|
|
26
|
+
* The SCHEDULE gate (schedules.js greenFor) deliberately stays on a real `green` only (T3).
|
|
27
|
+
* @param {string|null|undefined} outcome
|
|
28
|
+
* @returns {boolean}
|
|
29
|
+
*/
|
|
30
|
+
export function isReusableGreen(outcome) {
|
|
31
|
+
return outcome === 'green' || outcome === 'already-green';
|
|
32
|
+
}
|
|
33
|
+
|
|
23
34
|
/** @typedef {'running'|'waiting'|'passed'|'failed'|'capped'|'stopped'|'died'} StatusKey */
|
|
24
35
|
|
|
25
36
|
/** the table: key -> sign + word */
|
package/src/stagewords.js
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
// src/panel/server.js (verbatim) so the Audit/step map AND the learnings cards read the same sentence from the same
|
|
3
3
|
// code — a second spelling is how two screens come to disagree about one check. Pure; no file IO, no model.
|
|
4
4
|
|
|
5
|
+
/** the genre-owned noun for the count a TYPES-genre typecheck stage measures (see {@link stageQuestionText}) */
|
|
6
|
+
export const GENRE_WHAT = Object.freeze({ TYPES: 'type errors' });
|
|
7
|
+
|
|
8
|
+
/** `cmd`+`args`, as a comparable key — the one signal (besides `kind`
|
|
9
|
+
* itself) that two stages measure the SAME command's output, used to tell a
|
|
10
|
+
* genuine typecheck stage apart from the TYPES genre's other `lower-is-
|
|
11
|
+
* better`/`baseline: 0` stage (the failing-test-count half of `suite-green`'s
|
|
12
|
+
* "two assertions"), which shares that stage's own shape but not its
|
|
13
|
+
* population. `null` for a stage whose `cmd` isn't a non-empty string —
|
|
14
|
+
* never matches anything, so it never falsely suppresses a real typecheck
|
|
15
|
+
* question.
|
|
16
|
+
* @param {any} params @returns {string|null} */
|
|
17
|
+
export function cmdKey(params) {
|
|
18
|
+
if (!params || typeof params.cmd !== 'string' || params.cmd.length === 0) return null;
|
|
19
|
+
const args = Array.isArray(params.args) ? params.args : [];
|
|
20
|
+
return JSON.stringify([params.cmd, args]);
|
|
21
|
+
}
|
|
22
|
+
|
|
5
23
|
/**
|
|
6
24
|
* THE PLAIN-QUESTION OWNER (plain-checks build, 2026-09-28, hamr's ruling:
|
|
7
25
|
* the "checks N/M" headline is WITHDRAWN — "confusing, reads like 6 failed
|
|
@@ -64,23 +82,6 @@
|
|
|
64
82
|
* @param {{kind: string|null, params: any, genre: string|null, suiteCmdKeys: Set<string>}} o
|
|
65
83
|
* @returns {string|null}
|
|
66
84
|
*/
|
|
67
|
-
export const GENRE_WHAT = Object.freeze({ TYPES: 'type errors' });
|
|
68
|
-
|
|
69
|
-
/** `cmd`+`args`, as a comparable key — the one signal (besides `kind`
|
|
70
|
-
* itself) that two stages measure the SAME command's output, used to tell a
|
|
71
|
-
* genuine typecheck stage apart from the TYPES genre's other `lower-is-
|
|
72
|
-
* better`/`baseline: 0` stage (the failing-test-count half of `suite-green`'s
|
|
73
|
-
* "two assertions"), which shares that stage's own shape but not its
|
|
74
|
-
* population. `null` for a stage whose `cmd` isn't a non-empty string —
|
|
75
|
-
* never matches anything, so it never falsely suppresses a real typecheck
|
|
76
|
-
* question.
|
|
77
|
-
* @param {any} params @returns {string|null} */
|
|
78
|
-
export function cmdKey(params) {
|
|
79
|
-
if (!params || typeof params.cmd !== 'string' || params.cmd.length === 0) return null;
|
|
80
|
-
const args = Array.isArray(params.args) ? params.args : [];
|
|
81
|
-
return JSON.stringify([params.cmd, args]);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
85
|
export function stageQuestionText({
|
|
85
86
|
kind, params, genre, suiteCmdKeys,
|
|
86
87
|
}) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// ONE place for terminal colour (the launcher's header, menu and messages). Colours wrap words and never change them: with colour off
|
|
2
|
+
// every helper returns its text untouched, so plain-text readers (tests, pipes, logs) see the same words.
|
|
3
|
+
|
|
4
|
+
const wrap = (/** @type {string} */ code) => (/** @type {string} */ s) => `\x1b[${code}m${s}\x1b[0m`;
|
|
5
|
+
const id = (/** @type {string} */ s) => s;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {object} Paint
|
|
9
|
+
* @property {boolean} on
|
|
10
|
+
* @property {(s: string) => string} grey @property {(s: string) => string} green @property {(s: string) => string} yellow
|
|
11
|
+
* @property {(s: string) => string} red @property {(s: string) => string} cyan @property {(s: string) => string} boldCyan
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Colour is on only for a terminal: the stream says it is a TTY, `NO_COLOR` is empty/unset, and `TERM` is not `dumb`.
|
|
16
|
+
* @param {{isTTY?: boolean}|undefined} stream the output stream (process.stdout)
|
|
17
|
+
* @param {Record<string, string|undefined>} [env]
|
|
18
|
+
* @returns {Paint}
|
|
19
|
+
*/
|
|
20
|
+
export function makePaint(stream, env = process.env) {
|
|
21
|
+
const on = Boolean(stream && stream.isTTY) && !(env.NO_COLOR ?? '') && env.TERM !== 'dumb';
|
|
22
|
+
if (!on) return { on, grey: id, green: id, yellow: id, red: id, cyan: id, boldCyan: id };
|
|
23
|
+
return { on, grey: wrap('90'), green: wrap('32'), yellow: wrap('33'), red: wrap('31'), cyan: wrap('36'), boldCyan: wrap('1;36') };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** the no-colour paint, the default when a caller passes none */
|
|
27
|
+
export const PLAIN = makePaint(undefined, {});
|
package/src/userrun.js
CHANGED
|
@@ -1307,7 +1307,7 @@ async function execute(ctx) {
|
|
|
1307
1307
|
if (RULING.decision !== 'rerun') {
|
|
1308
1308
|
out(`\n${RULING.decision === 'accept' ? 'ACCEPTED' : 'PAUSED'} — the verdict this run minted (${doorRecord.outcome}) is UNTOUCHED; what you answered is what happens to the work.`);
|
|
1309
1309
|
if (RULING.decision === 'accept') {
|
|
1310
|
-
// Cross-run learnings: the human Accept
|
|
1310
|
+
// Cross-run learnings: the human Accept releases a hold an older version wrote for a rubric run (new runs count at run end) (forward only; a rerun or a
|
|
1311
1311
|
// pause never does). Its own try/catch: a record, never a verdict.
|
|
1312
1312
|
if (cfgOn && DOOR !== null) {
|
|
1313
1313
|
try { releaseRun({ home: cfgHome, runid: DOOR.includes('/') || DOOR.endsWith('.jsonl') ? basename(DOOR).replace(/^u-/, '').replace(/\.jsonl$/, '') : DOOR }); } catch (/** @type {any} */ e) { err(`WARNING: could not release this run's lessons in the learnings ledger (${redactSecrets(String(e?.message ?? e)).split('\n')[0]}).`); }
|
|
@@ -2025,7 +2025,7 @@ async function execute(ctx) {
|
|
|
2025
2025
|
} catch (/** @type {any} */ e) { err(`WARNING: could not settle this run's claim in ~/.config/bareloop/runs.jsonl (${e.message}) — the next run settles it once this process is gone.`); }
|
|
2026
2026
|
}
|
|
2027
2027
|
// The run-end tally of cross-run learnings ($0, code only, no model): the whole spine (all legs) and the gate audit become cards
|
|
2028
|
-
// folded into the ONE ledger (a rubric job's
|
|
2028
|
+
// folded into the ONE ledger (a rubric job's count at run end too). Its own try/catch, like the settle above: a
|
|
2029
2029
|
// record, never a verdict — a failure prints one WARNING line and can never change the outcome, the exit code or the spend.
|
|
2030
2030
|
// `execute` is shared by the panel, the CLI and the bundle runner, so this is the ONE call site. It runs with the hints setting
|
|
2031
2031
|
// on or off (OFF only means the planner is handed nothing). With no config home (a test with an injected env) it writes nothing.
|
package/types/authorreadout.d.ts
CHANGED
|
@@ -303,6 +303,13 @@ export function redsRecord(reds: PlainRed[], { cases, signing }?: {
|
|
|
303
303
|
}): Record<string, any>[];
|
|
304
304
|
/** the stops this module explains in plain words */
|
|
305
305
|
export const PLAIN_PROPOSAL_STOPS: readonly string[];
|
|
306
|
+
/**
|
|
307
|
+
* ONE owner of the plain sentence for every drafting stop code (hamr 2026-10-09: "a user should get a reason they can
|
|
308
|
+
* understand and do something about"). What happened; `STOP_TODO` below overrides the default "draft again" where that is wrong.
|
|
309
|
+
* Used by `draftStopText` for every stop the panel and the CLI show; a code not listed reads `STOP_UNKNOWN`, never the raw code.
|
|
310
|
+
* @type {Record<string, string>}
|
|
311
|
+
*/
|
|
312
|
+
export const STOP_HEADS: Record<string, string>;
|
|
306
313
|
/** codes the declaration gates (validateJob, validateCloseDecl) raise: one plain sentence covers the family */
|
|
307
314
|
export const DECLARATION_CODES: readonly string[];
|
|
308
315
|
export type PlainRed = {
|
package/types/cli.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* code, so `bin/bareloop.mjs` can set `process.exitCode` and let node flush
|
|
4
4
|
* queued stdout on its own (F-something: `process.exit()` can discard it).
|
|
5
5
|
* @param {string[]} argv
|
|
6
|
-
* @param {{ env?: Record<string,string|undefined>, stdout?: any, stderr?: any, cwd?: string, provider?: any, providerFor?: any, judgeProvider?: any, judgeModel?: string|null, now?: () => number, stdin?: any, runlistHome?: string, keysHome?: string }} deps
|
|
6
|
+
* @param {{ env?: Record<string,string|undefined>, stdout?: any, stderr?: any, cwd?: string, provider?: any, providerFor?: any, judgeProvider?: any, judgeModel?: string|null, now?: () => number, stdin?: any, runlistHome?: string, keysHome?: string, processEnv?: Record<string,string|undefined> }} deps
|
|
7
7
|
* @returns {Promise<number>}
|
|
8
8
|
*/
|
|
9
9
|
export function main(argv: string[], deps?: {
|
|
@@ -19,4 +19,5 @@ export function main(argv: string[], deps?: {
|
|
|
19
19
|
stdin?: any;
|
|
20
20
|
runlistHome?: string;
|
|
21
21
|
keysHome?: string;
|
|
22
|
+
processEnv?: Record<string, string | undefined>;
|
|
22
23
|
}): Promise<number>;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {object} Panel one answered probe
|
|
3
|
+
* @property {number} port @property {number} pid @property {string} version @property {number} drafts @property {number} schedules
|
|
4
|
+
* @property {string} url
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {object} LauncherDeps every OS touch, injectable (a test passes all of them)
|
|
8
|
+
* @property {string} [home] the bareloop home (`~/.config/bareloop`); undefined = the real one
|
|
9
|
+
* @property {number} [portStart] @property {number} [portCount]
|
|
10
|
+
* @property {number[]} [ports] an explicit port list (tests); wins over portStart/portCount
|
|
11
|
+
* @property {number} [timeoutMs] probe timeout per port
|
|
12
|
+
* @property {number} [deadlineMs] start/stop wait deadline
|
|
13
|
+
* @property {string} [nodeVersion] default process.versions.node
|
|
14
|
+
* @property {typeof fetch} [fetchImpl]
|
|
15
|
+
* @property {(cmd: string, args: string[], o: object) => any} [spawnFn] default node:child_process spawn
|
|
16
|
+
* @property {(pid: number, sig: string) => void} [kill] default process.kill
|
|
17
|
+
* @property {((target: string) => Promise<boolean>)} [open] default the shared OS opener
|
|
18
|
+
* @property {(port: number) => Promise<boolean>} [portFree] default: try to bind 127.0.0.1:port
|
|
19
|
+
* @property {(ms: number) => Promise<void>} [sleep]
|
|
20
|
+
* @property {(question: string) => Promise<string>} [ask] the y/N and menu reader
|
|
21
|
+
* @property {string} [bareloopBin] default bin/bareloop.mjs of this package
|
|
22
|
+
* @property {import('./tty-colour.js').Paint} [paint] colour for stdout lines (default none); {@link makePaint} decides TTY / NO_COLOR / TERM
|
|
23
|
+
* @property {import('./tty-colour.js').Paint} [paintErr] colour for stderr lines (default: `paint`)
|
|
24
|
+
* @property {number} [now] ms clock for the run list's age rules (default: real time); tests pass their own
|
|
25
|
+
* @property {string} [platform]
|
|
26
|
+
* @property {Record<string, string|undefined>} [env] raw env handed to the panel child (default process.env)
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* The run counts, from the panel's own `listRuns` rows. `status.key === 'running'` is the panel's word; `needs` is the one tag
|
|
30
|
+
* {@link import('./panel/server.js').needsStateFor} decides. A part that is 0 still shows.
|
|
31
|
+
* @param {any[]} rows
|
|
32
|
+
* @param {number} [gaveUp] schedules that gave up: the panel's Needs-you holds them too
|
|
33
|
+
* @returns {{running: number, needYou: number, total: number}}
|
|
34
|
+
*/
|
|
35
|
+
export function countRuns(rows: any[], gaveUp?: number): {
|
|
36
|
+
running: number;
|
|
37
|
+
needYou: number;
|
|
38
|
+
total: number;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* "installed ✓" = the folder exists, config.json is there and reads with no problem, `.env` exists with mode 600, node >= 22.
|
|
42
|
+
* Otherwise the FIRST missing thing in plain words.
|
|
43
|
+
* @param {{home?: string, nodeVersion?: string}} o
|
|
44
|
+
* @returns {{ok: boolean, problem: string|null}}
|
|
45
|
+
*/
|
|
46
|
+
export function installState(o: {
|
|
47
|
+
home?: string;
|
|
48
|
+
nodeVersion?: string;
|
|
49
|
+
}): {
|
|
50
|
+
ok: boolean;
|
|
51
|
+
problem: string | null;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Ask 127.0.0.1 across the ports in parallel; a reply with `app === "bareloop"` is a panel. Never throws.
|
|
55
|
+
* @param {{ports: number[], timeoutMs?: number, fetchImpl?: typeof fetch}} o
|
|
56
|
+
* @returns {Promise<Panel[]>} in port order
|
|
57
|
+
*/
|
|
58
|
+
export function probePanels(o: {
|
|
59
|
+
ports: number[];
|
|
60
|
+
timeoutMs?: number;
|
|
61
|
+
fetchImpl?: typeof fetch;
|
|
62
|
+
}): Promise<Panel[]>;
|
|
63
|
+
/**
|
|
64
|
+
* The header, ONCE per call: the two lines every `bareloop` command prints first, plus the panels the probe found (the commands
|
|
65
|
+
* reuse them, so nothing is probed twice).
|
|
66
|
+
* @param {LauncherDeps & {version?: string}} d
|
|
67
|
+
* @returns {Promise<{lines: string[], panels: Panel[]}>}
|
|
68
|
+
*/
|
|
69
|
+
export function buildHeader(d: LauncherDeps & {
|
|
70
|
+
version?: string;
|
|
71
|
+
}): Promise<{
|
|
72
|
+
lines: string[];
|
|
73
|
+
panels: Panel[];
|
|
74
|
+
}>;
|
|
75
|
+
/**
|
|
76
|
+
* @param {LauncherDeps & {version?: string}} d
|
|
77
|
+
* @returns {Promise<string[]>} the two header lines
|
|
78
|
+
*/
|
|
79
|
+
export function headerLines(d: LauncherDeps & {
|
|
80
|
+
version?: string;
|
|
81
|
+
}): Promise<string[]>;
|
|
82
|
+
/**
|
|
83
|
+
* `bareloop install` — creates only what is missing, never reads a key value.
|
|
84
|
+
* @param {{out: (s: string) => void, err: (s: string) => void}} io @param {LauncherDeps & {version?: string}} d @param {{lines: string[], panels: Panel[]}} [shown]
|
|
85
|
+
* @returns {Promise<number>}
|
|
86
|
+
*/
|
|
87
|
+
export function installCmd(io: {
|
|
88
|
+
out: (s: string) => void;
|
|
89
|
+
err: (s: string) => void;
|
|
90
|
+
}, d: LauncherDeps & {
|
|
91
|
+
version?: string;
|
|
92
|
+
}, shown?: {
|
|
93
|
+
lines: string[];
|
|
94
|
+
panels: Panel[];
|
|
95
|
+
}): Promise<number>;
|
|
96
|
+
/**
|
|
97
|
+
* `bareloop status` — the header, then one line per panel (or `Panel not running.`). Exit 0 running, 3 not running; 0 when the menu ran it.
|
|
98
|
+
* @param {{out: (s: string) => void}} io @param {LauncherDeps & {version?: string}} d @param {{lines: string[], panels: Panel[]}} [shown] header the menu already printed
|
|
99
|
+
* @returns {Promise<number>}
|
|
100
|
+
*/
|
|
101
|
+
export function statusCmd(io: {
|
|
102
|
+
out: (s: string) => void;
|
|
103
|
+
}, d: LauncherDeps & {
|
|
104
|
+
version?: string;
|
|
105
|
+
}, shown?: {
|
|
106
|
+
lines: string[];
|
|
107
|
+
panels: Panel[];
|
|
108
|
+
}): Promise<number>;
|
|
109
|
+
/**
|
|
110
|
+
* `bareloop start` — find a running panel first; else start one detached on the first free port 4700..4709, wait until it answers,
|
|
111
|
+
* open the page.
|
|
112
|
+
* @param {{out: (s: string) => void, err: (s: string) => void}} io @param {LauncherDeps & {version?: string}} d @param {{lines: string[], panels: Panel[]}} [shown]
|
|
113
|
+
* @returns {Promise<number>}
|
|
114
|
+
*/
|
|
115
|
+
export function startCmd(io: {
|
|
116
|
+
out: (s: string) => void;
|
|
117
|
+
err: (s: string) => void;
|
|
118
|
+
}, d: LauncherDeps & {
|
|
119
|
+
version?: string;
|
|
120
|
+
}, shown?: {
|
|
121
|
+
lines: string[];
|
|
122
|
+
panels: Panel[];
|
|
123
|
+
}): Promise<number>;
|
|
124
|
+
/**
|
|
125
|
+
* `bareloop stop [--yes]` — SIGTERM the pid of EVERY panel the probe finds (one panel at a time is the rule; no "which one"), wait until each
|
|
126
|
+
* port stops answering. Drafts are in memory: warn (summed over all found panels) and ask y/N unless `--yes`.
|
|
127
|
+
* @param {string[]} args
|
|
128
|
+
* @param {{out: (s: string) => void, err: (s: string) => void}} io @param {LauncherDeps & {version?: string}} d @param {{lines: string[], panels: Panel[]}} [shown]
|
|
129
|
+
* @returns {Promise<number>}
|
|
130
|
+
*/
|
|
131
|
+
export function stopCmd(args: string[], io: {
|
|
132
|
+
out: (s: string) => void;
|
|
133
|
+
err: (s: string) => void;
|
|
134
|
+
}, d: LauncherDeps & {
|
|
135
|
+
version?: string;
|
|
136
|
+
}, shown?: {
|
|
137
|
+
lines: string[];
|
|
138
|
+
panels: Panel[];
|
|
139
|
+
}): Promise<number>;
|
|
140
|
+
/**
|
|
141
|
+
* The bare `bareloop` menu: header, the four choices, one question. Runs the chosen command and returns its exit code.
|
|
142
|
+
* @param {{out: (s: string) => void, err: (s: string) => void}} io @param {LauncherDeps & {version?: string}} d
|
|
143
|
+
* @returns {Promise<number>}
|
|
144
|
+
*/
|
|
145
|
+
export function menuCmd(io: {
|
|
146
|
+
out: (s: string) => void;
|
|
147
|
+
err: (s: string) => void;
|
|
148
|
+
}, d: LauncherDeps & {
|
|
149
|
+
version?: string;
|
|
150
|
+
}): Promise<number>;
|
|
151
|
+
/** the first port tried, and how many in a row (4700..4709) */
|
|
152
|
+
export const PORT_START: 4700;
|
|
153
|
+
export const PORT_COUNT: 10;
|
|
154
|
+
/**
|
|
155
|
+
* one answered probe
|
|
156
|
+
*/
|
|
157
|
+
export type Panel = {
|
|
158
|
+
port: number;
|
|
159
|
+
pid: number;
|
|
160
|
+
version: string;
|
|
161
|
+
drafts: number;
|
|
162
|
+
schedules: number;
|
|
163
|
+
url: string;
|
|
164
|
+
};
|
|
165
|
+
/**
|
|
166
|
+
* every OS touch, injectable (a test passes all of them)
|
|
167
|
+
*/
|
|
168
|
+
export type LauncherDeps = {
|
|
169
|
+
/**
|
|
170
|
+
* the bareloop home (`~/.config/bareloop`); undefined = the real one
|
|
171
|
+
*/
|
|
172
|
+
home?: string | undefined;
|
|
173
|
+
portStart?: number | undefined;
|
|
174
|
+
portCount?: number | undefined;
|
|
175
|
+
/**
|
|
176
|
+
* an explicit port list (tests); wins over portStart/portCount
|
|
177
|
+
*/
|
|
178
|
+
ports?: number[] | undefined;
|
|
179
|
+
/**
|
|
180
|
+
* probe timeout per port
|
|
181
|
+
*/
|
|
182
|
+
timeoutMs?: number | undefined;
|
|
183
|
+
/**
|
|
184
|
+
* start/stop wait deadline
|
|
185
|
+
*/
|
|
186
|
+
deadlineMs?: number | undefined;
|
|
187
|
+
/**
|
|
188
|
+
* default process.versions.node
|
|
189
|
+
*/
|
|
190
|
+
nodeVersion?: string | undefined;
|
|
191
|
+
fetchImpl?: typeof fetch | undefined;
|
|
192
|
+
/**
|
|
193
|
+
* default node:child_process spawn
|
|
194
|
+
*/
|
|
195
|
+
spawnFn?: ((cmd: string, args: string[], o: object) => any) | undefined;
|
|
196
|
+
/**
|
|
197
|
+
* default process.kill
|
|
198
|
+
*/
|
|
199
|
+
kill?: ((pid: number, sig: string) => void) | undefined;
|
|
200
|
+
/**
|
|
201
|
+
* default the shared OS opener
|
|
202
|
+
*/
|
|
203
|
+
open?: ((target: string) => Promise<boolean>) | undefined;
|
|
204
|
+
/**
|
|
205
|
+
* default: try to bind 127.0.0.1:port
|
|
206
|
+
*/
|
|
207
|
+
portFree?: ((port: number) => Promise<boolean>) | undefined;
|
|
208
|
+
sleep?: ((ms: number) => Promise<void>) | undefined;
|
|
209
|
+
/**
|
|
210
|
+
* the y/N and menu reader
|
|
211
|
+
*/
|
|
212
|
+
ask?: ((question: string) => Promise<string>) | undefined;
|
|
213
|
+
/**
|
|
214
|
+
* default bin/bareloop.mjs of this package
|
|
215
|
+
*/
|
|
216
|
+
bareloopBin?: string | undefined;
|
|
217
|
+
/**
|
|
218
|
+
* colour for stdout lines (default none); {@link makePaint} decides TTY / NO_COLOR / TERM
|
|
219
|
+
*/
|
|
220
|
+
paint?: import("./tty-colour.js").Paint | undefined;
|
|
221
|
+
/**
|
|
222
|
+
* colour for stderr lines (default: `paint`)
|
|
223
|
+
*/
|
|
224
|
+
paintErr?: import("./tty-colour.js").Paint | undefined;
|
|
225
|
+
/**
|
|
226
|
+
* ms clock for the run list's age rules (default: real time); tests pass their own
|
|
227
|
+
*/
|
|
228
|
+
now?: number | undefined;
|
|
229
|
+
platform?: string | undefined;
|
|
230
|
+
/**
|
|
231
|
+
* raw env handed to the panel child (default process.env)
|
|
232
|
+
*/
|
|
233
|
+
env?: Record<string, string | undefined> | undefined;
|
|
234
|
+
};
|