bareloop 0.36.0 → 0.38.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 +44 -0
- package/bareloop.context.md +52 -22
- package/package.json +1 -1
- package/src/authorrun.js +2 -3
- package/src/draftspend.js +1 -3
- package/src/exits.js +3 -0
- package/src/legs.js +15 -0
- package/src/panel/authorroutes.js +19 -1
- package/src/panel/authorsession.js +14 -16
- package/src/panel/index.html +379 -147
- package/src/panel/server.js +79 -15
- package/src/panel/status.js +27 -4
- package/src/planrun.js +11 -8
- package/src/ralph.js +8 -2
- package/src/replay.js +23 -3
- package/src/source.js +12 -0
- package/types/legs.d.ts +12 -0
- package/types/panel/authorroutes.d.ts +4 -1
- package/types/panel/authorsession.d.ts +2 -7
- package/types/panel/server.d.ts +22 -4
- package/types/panel/status.d.ts +21 -1
- package/types/planrun.d.ts +3 -3
- package/types/ralph.d.ts +6 -1
- package/types/source.d.ts +7 -0
package/src/panel/server.js
CHANGED
|
@@ -53,7 +53,7 @@ import { rowsForHome, findRow, modelChoiceFor } from '../providerrows.js';
|
|
|
53
53
|
import { resolveProvider } from '../providers.js';
|
|
54
54
|
import { REUSE_LOCKED_FIELDS, REUSE_OPEN_FIELDS } from './authorsession.js';
|
|
55
55
|
import { readResume, checkpointAgeGate, CHECKPOINT_OUTCOMES } from '../reuse.js';
|
|
56
|
-
import { statusFor, GOAL_MET_LINE } from './status.js';
|
|
56
|
+
import { statusFor, partStatusFor, GOAL_MET_LINE } from './status.js';
|
|
57
57
|
import { createSettingsRoutes } from './settingsroutes.js';
|
|
58
58
|
|
|
59
59
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -74,7 +74,7 @@ export const RUNID_RE = /^[A-Za-z0-9._~-]+$/;
|
|
|
74
74
|
export const DEFAULT_PORT = 4700;
|
|
75
75
|
|
|
76
76
|
/**
|
|
77
|
-
* `[✓]`/`[✗]`/`[▶]` — the ONLY vocabulary a result is ever rendered in
|
|
77
|
+
* `[✓]`/`[✗]`/`[■]`/`[▶]` — the ONLY vocabulary a result is ever rendered in
|
|
78
78
|
* (auto-memory `ui-verdict-words.md`: never the words green/red/soft-green
|
|
79
79
|
* anywhere in the page). `null` (no `job-end` reached — a killed-mid-run or
|
|
80
80
|
* still-running spine) reads `▶` — the same "in progress / unresolved" glyph
|
|
@@ -82,10 +82,10 @@ export const DEFAULT_PORT = 4700;
|
|
|
82
82
|
* running attempt and an archived spine that never reached its own end are
|
|
83
83
|
* the same fact from this read-only side: no verdict has been recorded yet.
|
|
84
84
|
* @param {string|null} outcome
|
|
85
|
-
* @returns {'✓'|'✗'|'▶'}
|
|
85
|
+
* @returns {'✓'|'✗'|'■'|'▶'}
|
|
86
86
|
*/
|
|
87
87
|
export function glyphForOutcome(outcome) {
|
|
88
|
-
return /** @type {'✓'|'✗'|'▶'} */ (statusFor({ outcome }).sign);
|
|
88
|
+
return /** @type {'✓'|'✗'|'■'|'▶'} */ (statusFor({ outcome }).sign);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/**
|
|
@@ -417,7 +417,7 @@ const GOAL_NOT_MET = new Set(['plan-red', 'check-red', 'step-red', 'escalated'])
|
|
|
417
417
|
* @returns {{reason: string, next: string, line: string, actions: {id: string, label: string}[]}|null}
|
|
418
418
|
*/
|
|
419
419
|
export function endedFor(summary, death, o = {}) {
|
|
420
|
-
const base = endedBase(summary, death, o);
|
|
420
|
+
const base = withEditAction(endedBase(summary, death, o));
|
|
421
421
|
const wt = o.worktree;
|
|
422
422
|
if (!base || !wt) return base;
|
|
423
423
|
// P6 item 1 — the run worked on a worktree in the person's own repo. GREEN: the branch carries the work (the folder is
|
|
@@ -432,21 +432,31 @@ export function endedFor(summary, death, o = {}) {
|
|
|
432
432
|
return wt.exists ? { ...base, next: `${base.next} Your work is in ${wt.folder}.` } : base;
|
|
433
433
|
}
|
|
434
434
|
|
|
435
|
+
/**
|
|
436
|
+
* "Edit in chat" (hamr 2026-10-07): offered on EVERY non-green ending (failed, capped, stopped, died, every other red),
|
|
437
|
+
* after Resume when both show; a green ending keeps Reuse workflow only. ONE place, so no ending branch can forget it.
|
|
438
|
+
* @param {{reason: string, next: string, line: string, actions: {id: string, label: string}[]}|null} ended
|
|
439
|
+
*/
|
|
440
|
+
function withEditAction(ended) {
|
|
441
|
+
if (!ended || ended.actions.some((a) => a.id === 'reuse')) return ended;
|
|
442
|
+
return { ...ended, actions: [...ended.actions, { id: 'edit', label: 'Edit in chat' }] };
|
|
443
|
+
}
|
|
444
|
+
|
|
435
445
|
/** @param {any} summary @param {any} death @param {any} o */
|
|
436
446
|
function endedBase(summary, death, o) {
|
|
437
447
|
const resumeOk = !!(o.resume && o.resume.ok);
|
|
438
448
|
/** @type {{id: string, label: string}[]} */
|
|
439
449
|
const RESUME = [{ id: 'resume', label: 'Resume' }];
|
|
440
450
|
/** Reuse workflow (replaces P5 item 3's Start from this, hamr 2026-10-03): the same signed job on a new source — a NEW
|
|
441
|
-
* run, never a rerun. Offered on GREEN rows only; every red row says "Change the job:
|
|
451
|
+
* run, never a rerun. Offered on GREEN rows only; every red row says "Change the job: press Edit in chat" instead. */
|
|
442
452
|
const REUSE = [{ id: 'reuse', label: 'Reuse workflow' }];
|
|
443
|
-
const CHANGE = 'Change the job:
|
|
453
|
+
const CHANGE = 'Change the job: press Edit in chat.';
|
|
444
454
|
const detailOf = (/** @type {string|null} */ s) => {
|
|
445
455
|
if (typeof s !== 'string' || s.length === 0) return '';
|
|
446
456
|
return s.length > 120 ? `${s.slice(0, 120)}…` : s;
|
|
447
457
|
};
|
|
448
458
|
/** a resumable ending whose Resume the engine would refuse: say so, never offer the button */
|
|
449
|
-
const resumeOr = (/** @type {string} */ okNext) => (resumeOk ? okNext : `Resume is not available for this run${o.resume && o.resume.why ? ` (${o.resume.why})` : ''}
|
|
459
|
+
const resumeOr = (/** @type {string} */ okNext) => (resumeOk ? okNext : `Resume is not available for this run${o.resume && o.resume.why ? ` (${o.resume.why})` : ''}. ${CHANGE}`);
|
|
450
460
|
|
|
451
461
|
if (death.died) {
|
|
452
462
|
return {
|
|
@@ -497,13 +507,20 @@ function endedBase(summary, death, o) {
|
|
|
497
507
|
|
|
498
508
|
if (outcome === 'green' || outcome === 'already-green' || outcome === 'satisfied') {
|
|
499
509
|
if (o.destinationRefused) {
|
|
510
|
+
// Reuse workflow only on a run that earned its green (hamr 2026-10-08, as below); the rest take Edit in chat
|
|
511
|
+
// (withEditAction adds it) and are told to change the job, not to reuse it
|
|
512
|
+
const earned = outcome === 'green';
|
|
500
513
|
return {
|
|
501
514
|
reason: 'Goal met, but the output could not be delivered.',
|
|
502
|
-
next: 'Fix the destination, then Reuse workflow.',
|
|
515
|
+
next: earned ? 'Fix the destination, then Reuse workflow.' : CHANGE,
|
|
503
516
|
line: `${GOAL_MET_LINE} — not delivered`,
|
|
504
|
-
actions: REUSE,
|
|
517
|
+
actions: earned ? REUSE : [],
|
|
505
518
|
};
|
|
506
519
|
}
|
|
520
|
+
// hamr 2026-10-08: already-green (the check passed before any work) is not a reusable job — only a run that
|
|
521
|
+
// earned its green offers Reuse workflow; the rest take Edit in chat (withEditAction adds it). `satisfied` is a
|
|
522
|
+
// close VERDICT, never a run outcome (ralph and the job-end emitters write `green`), so it rides with already-green.
|
|
523
|
+
if (outcome !== 'green') return { reason: 'Goal met.', next: CHANGE, line: GOAL_MET_LINE, actions: [] };
|
|
507
524
|
return { reason: 'Goal met.', next: 'Nothing to do.', line: GOAL_MET_LINE, actions: REUSE };
|
|
508
525
|
}
|
|
509
526
|
if (outcome === 'cap-halt') {
|
|
@@ -545,7 +562,7 @@ function endedBase(summary, death, o) {
|
|
|
545
562
|
if (outcome === 'step-stalled') {
|
|
546
563
|
return {
|
|
547
564
|
reason: 'A step stopped making progress.',
|
|
548
|
-
next: resumeOr('Resume, or change the job:
|
|
565
|
+
next: resumeOr('Resume, or change the job: press Edit in chat.'),
|
|
549
566
|
line: resumeOk ? 'step stalled — resume' : 'step stalled',
|
|
550
567
|
actions: resumeOk ? RESUME : [],
|
|
551
568
|
};
|
|
@@ -897,7 +914,7 @@ function getRunDetailBody(runid, opts) {
|
|
|
897
914
|
const timelineKind = summary.timelineKind;
|
|
898
915
|
// the drafting part (see draftingFor) heads the parts list; the enrichment below touches the spine's own parts only
|
|
899
916
|
const draftingPartHere = draftingFor(row, summary, opts);
|
|
900
|
-
const draftingShiftHere =
|
|
917
|
+
const draftingShiftHere = draftingShift(row, summary, opts);
|
|
901
918
|
const enrichedParts = enrichPartsWithStageKind(summary.parts, stageKindMetaFromSpec(resolveSpecForRow(row)));
|
|
902
919
|
const death = deriveDeath(row, rawSpineRecords, summary.outcome);
|
|
903
920
|
const { ended, resume, status } = endedForRow(row, rawSpineRecords, summary, death, true);
|
|
@@ -1117,7 +1134,9 @@ function getRunDetailBody(runid, opts) {
|
|
|
1117
1134
|
// {@link enrichPartsWithStageKind}. `kindMeta` is `null` (every stage
|
|
1118
1135
|
// passes through unchanged) whenever no spec resolves at all, so the two
|
|
1119
1136
|
// tabs still never disagree about order/counts/blocked-call figures.
|
|
1120
|
-
|
|
1137
|
+
// a run that DIED (no job-end, runner gone — `death`, decided once above) leaves its last part in flight: that part reads
|
|
1138
|
+
// `died`, never the "happened, so passed" default. Replay alone cannot tell died from still running, so the verdict lives here.
|
|
1139
|
+
parts: (draftingPartHere && Array.isArray(enrichedParts) ? [draftingPartHere, ...enrichedParts] : enrichedParts)?.map((p, i, all) => ({ ...p, status: partStatusFor(death.died && i === all.length - 1 && p.kind !== 'drafting' && p.outcome == null && !p.stopReason ? { ...p, stopReason: 'died' } : p) })),
|
|
1121
1140
|
replans: summary.replans,
|
|
1122
1141
|
close: summary.close,
|
|
1123
1142
|
branch: summary.branch,
|
|
@@ -2406,7 +2425,7 @@ export function reuseLine(record) {
|
|
|
2406
2425
|
* to copy: change the job: Clear the card). `null` when the runid is not listed.
|
|
2407
2426
|
* @param {string} runid
|
|
2408
2427
|
* @param {{ home?: string, model?: string }} [opts] `model` = the Model box's current Name (the estimate counts only runs on that worker); absent = the card's own Model
|
|
2409
|
-
* @returns {{ok: true, origin: {runid: string, job: string}, card: Record<string, any>, from: 'signed job',
|
|
2428
|
+
* @returns {{ok: true, origin: {runid: string, job: string}, outcome: string|null, card: Record<string, any>, from: 'signed job',
|
|
2410
2429
|
* locked: readonly string[], open: readonly string[], note: string|null,
|
|
2411
2430
|
* specHash: string, workflowKey: string, spec: any, specPath: string,
|
|
2412
2431
|
* trackRecord: ReturnType<typeof trackRecordFor>, line: string}|{ok: false, error: string}|null}
|
|
@@ -2445,6 +2464,8 @@ export function getStartFrom(runid, opts = {}) {
|
|
|
2445
2464
|
return {
|
|
2446
2465
|
ok: true,
|
|
2447
2466
|
origin: { runid, job: row.job },
|
|
2467
|
+
// the run's terminal outcome — the Reuse start (POST /api/author/start) refuses anything but a real green
|
|
2468
|
+
outcome: legsOf(records).at(-1)?.outcome ?? null,
|
|
2448
2469
|
card,
|
|
2449
2470
|
from: 'signed job',
|
|
2450
2471
|
locked: REUSE_LOCKED_FIELDS,
|
|
@@ -2459,6 +2480,40 @@ export function getStartFrom(runid, opts = {}) {
|
|
|
2459
2480
|
};
|
|
2460
2481
|
}
|
|
2461
2482
|
|
|
2483
|
+
/**
|
|
2484
|
+
* The Reuse workflow picker's list: one entry per JOB (same-job identity = `workflowKey`, the one `getStartFrom` uses)
|
|
2485
|
+
* that has at least one GREEN local run, newest green first. `runid` = that job's newest green run, so the page hands
|
|
2486
|
+
* it to the existing start-from flow unchanged; `line` is `getStartFrom`'s own estimate line (never a second
|
|
2487
|
+
* spelling). Local runs only — an imported job keeps its Reuse button in the Import view. $0, read-only.
|
|
2488
|
+
* @param {{ home?: string, model?: string }} [opts] `model` passes through to the estimate line, as start-from's does
|
|
2489
|
+
* @returns {{job: string, runid: string, checkType: 'deterministic'|'rubric', line: string}[]}
|
|
2490
|
+
*/
|
|
2491
|
+
export function listReuseJobs(opts = {}) {
|
|
2492
|
+
const { rows } = readRunList(opts);
|
|
2493
|
+
const seen = new Set();
|
|
2494
|
+
/** @type {{job: string, runid: string, checkType: 'deterministic'|'rubric', line: string}[]} */
|
|
2495
|
+
const jobs = [];
|
|
2496
|
+
for (let i = rows.length - 1; i >= 0; i -= 1) {
|
|
2497
|
+
const row = rows[i];
|
|
2498
|
+
if (!row || !existsSync(row.spine)) continue;
|
|
2499
|
+
let records;
|
|
2500
|
+
try { records = parseJsonl(row.spine).records; } catch { continue; }
|
|
2501
|
+
const outcome = legsOf(records).at(-1)?.outcome ?? null;
|
|
2502
|
+
// only a REAL green (hamr 2026-10-08): `already-green` means the check passed before any work, so there is nothing to reuse
|
|
2503
|
+
// (src/reuse.js 'green-predates-run'). Narrower than the Run tab's Reuse button on purpose.
|
|
2504
|
+
if (outcome !== 'green') continue;
|
|
2505
|
+
const signed = signedSpecForRun(row, records);
|
|
2506
|
+
if (!signed) continue;
|
|
2507
|
+
const key = workflowKey(signed.spec);
|
|
2508
|
+
if (seen.has(key)) continue;
|
|
2509
|
+
const pre = getStartFrom(row.runid, opts);
|
|
2510
|
+
if (!pre || !pre.ok) continue;
|
|
2511
|
+
seen.add(key);
|
|
2512
|
+
jobs.push({ job: pre.card.jobName, runid: row.runid, checkType: pre.card.checkType === 'rubric' ? 'rubric' : 'deterministic', line: pre.line });
|
|
2513
|
+
}
|
|
2514
|
+
return jobs;
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2462
2517
|
/**
|
|
2463
2518
|
* The reuse card for a signed spec: locked boxes from the spec through the Job tab's own readers, open boxes from
|
|
2464
2519
|
* the spec's own fence and caps (Source and the saved text from the caller — a spec carries neither).
|
|
@@ -2636,7 +2691,15 @@ export function handleRequest(req, res, opts) {
|
|
|
2636
2691
|
/** @type {any} */
|
|
2637
2692
|
let body = null;
|
|
2638
2693
|
if (raw.length > 0) { try { body = JSON.parse(raw); } catch { body = null; } }
|
|
2639
|
-
|
|
2694
|
+
// this callback runs OUTSIDE createServer's try/catch (which covers only the synchronous GET path), so a
|
|
2695
|
+
// synchronous throw in a route handler (a full disk, a permission fault) would be an uncaught exception that
|
|
2696
|
+
// takes the whole panel down: answer 500 JSON instead and keep serving
|
|
2697
|
+
try {
|
|
2698
|
+
routes.handle(req, res, pathname, body);
|
|
2699
|
+
} catch (e) {
|
|
2700
|
+
if (res.headersSent) { res.end(); return; }
|
|
2701
|
+
sendJson(res, 500, { ok: false, error: `internal error: ${/** @type {Error} */ (e)?.message ?? String(e)}` });
|
|
2702
|
+
}
|
|
2640
2703
|
});
|
|
2641
2704
|
return;
|
|
2642
2705
|
}
|
|
@@ -2801,6 +2864,7 @@ export function createPanelServer(opts = {}) {
|
|
|
2801
2864
|
startFrom: {
|
|
2802
2865
|
get: (runid, o = {}) => getStartFrom(runid, { home, ...o }),
|
|
2803
2866
|
getImport: (id, o = {}) => getStartFromImport(id, { home, ...o }),
|
|
2867
|
+
listJobs: (o = {}) => listReuseJobs({ home, ...o }),
|
|
2804
2868
|
},
|
|
2805
2869
|
});
|
|
2806
2870
|
settingsRoutes = createSettingsRoutes({
|
package/src/panel/status.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// hamr's ruling 2026-10-04 (option A): every surface (run cards, expanded run rows, the right-side header, imported
|
|
3
3
|
// rows) reads `status` from the server; the page never maps an outcome to a word itself.
|
|
4
4
|
//
|
|
5
|
-
// [▶] running · [·] waiting · [✓] passed · [✗] failed · [✗] capped · [
|
|
5
|
+
// [▶] running · [·] waiting · [✓] passed · [✗] failed · [✗] capped · [■] stopped · [?] died
|
|
6
6
|
//
|
|
7
7
|
// Outcome -> word (every outcome the panel knows; anything not listed is `failed`):
|
|
8
8
|
// green, already-green, satisfied -> passed
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
// and every other outcome (refusals, instrument stops) -> failed
|
|
16
16
|
// `waiting` is the step-level sign for a step that has not started; no run outcome maps to it.
|
|
17
17
|
|
|
18
|
+
import { capKindOf } from '../legs.js';
|
|
19
|
+
|
|
18
20
|
/** the sentence a passed run carries as its reason (the Ended block's `line`) */
|
|
19
21
|
export const GOAL_MET_LINE = 'goal met';
|
|
20
22
|
|
|
@@ -27,7 +29,7 @@ export const STATUS = Object.freeze({
|
|
|
27
29
|
passed: Object.freeze({ sign: '✓', word: 'passed' }),
|
|
28
30
|
failed: Object.freeze({ sign: '✗', word: 'failed' }),
|
|
29
31
|
capped: Object.freeze({ sign: '✗', word: 'capped' }),
|
|
30
|
-
stopped: Object.freeze({ sign: '
|
|
32
|
+
stopped: Object.freeze({ sign: '■', word: 'stopped' }),
|
|
31
33
|
died: Object.freeze({ sign: '?', word: 'died' }),
|
|
32
34
|
});
|
|
33
35
|
|
|
@@ -44,8 +46,29 @@ export function statusFor(o) {
|
|
|
44
46
|
if (o.died) key = 'died';
|
|
45
47
|
else if (out === null || out === undefined) key = 'running';
|
|
46
48
|
else if (out === 'green' || out === 'already-green' || out === 'satisfied') key = 'passed';
|
|
47
|
-
else if (
|
|
49
|
+
else if (capKindOf(o) !== null) key = 'capped';
|
|
48
50
|
else if (out === 'stopped') key = 'stopped';
|
|
49
|
-
else if (out === 'escalated' && ((o.category === 'cap-halt' && o.moneyHalt) || o.category === 'wall-halt')) key = 'capped';
|
|
50
51
|
return { key, ...STATUS[key] };
|
|
51
52
|
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The word one PART of a run wears on the step map, the step cards and the Audit rows (hamr 2026-10-07: `[✓] passed` /
|
|
56
|
+
* `[✗] failed`, never "done"). Reads the same table; `running` is the page's call (it knows which part is live), so a
|
|
57
|
+
* part with no verdict and no cut-off is `passed` here.
|
|
58
|
+
* drafting, green/satisfied/already-green outcome, or a part that simply happened (scout/plan/replan/judge) -> passed
|
|
59
|
+
* a red outcome -> its run-outcome word (failed; a cap/stop outcome keeps capped/stopped)
|
|
60
|
+
* a part its leg's halt cut off (`stopReason`): 'money cap' / 'time cap' -> capped, 'you stopped it' -> stopped,
|
|
61
|
+
* 'died' -> died, anything else (provider failed, step stalled, a raw outcome) -> failed
|
|
62
|
+
* @param {{kind?: string, outcome?: string|null, stopReason?: string|null}} part
|
|
63
|
+
* @returns {{key: StatusKey, sign: string, word: string}}
|
|
64
|
+
*/
|
|
65
|
+
export function partStatusFor(part) {
|
|
66
|
+
if (part.kind === 'drafting') return statusFor({ outcome: 'green' });
|
|
67
|
+
if (typeof part.outcome === 'string') return statusFor({ outcome: part.outcome });
|
|
68
|
+
const why = part.stopReason;
|
|
69
|
+
if (why) {
|
|
70
|
+
const key = why === 'money cap' || why === 'time cap' ? 'capped' : why === 'you stopped it' ? 'stopped' : why === 'died' ? 'died' : 'failed';
|
|
71
|
+
return { key, ...STATUS[key] };
|
|
72
|
+
}
|
|
73
|
+
return statusFor({ outcome: 'green' });
|
|
74
|
+
}
|
package/src/planrun.js
CHANGED
|
@@ -970,10 +970,10 @@ ${scoutBlob || '(no scout notes)'}`;
|
|
|
970
970
|
* the resume-plan-red option, the fix-loop terminal) say "resume is `run-u`-only
|
|
971
971
|
* in v1" instead of naming a flag that would fail if typed.
|
|
972
972
|
* @param {string|null} [opts.stopFile=null] P5 item 5 — the run's STOP REQUEST file
|
|
973
|
-
* (`stopFilePath(spine)`, src/legs.js). Read at ONE seam, the round boundary of
|
|
974
|
-
* (the `metered` callback, where the money cap binds): present = emit `stop-requested`, consume
|
|
973
|
+
* (`stopFilePath(spine)`, src/legs.js). Read at ONE seam, the round boundary of EVERY worker
|
|
974
|
+
* (scout, plan/replan drafter, step, close-fix: the `metered` callback, where the money cap binds): present = emit `stop-requested`, consume
|
|
975
975
|
* the file, end the Loop after that round, and end the leg `stopped` (a checkpoint outcome, filed
|
|
976
|
-
* the way a mid-step cap-halt is; resume re-enters that step). Nothing is in flight at that
|
|
976
|
+
* the way a mid-step cap-halt is; resume re-enters that step, or continues the phase). Nothing is in flight at that
|
|
977
977
|
* point, so the spend is exact. `null` = no stop surface (the default).
|
|
978
978
|
* @returns {Promise<string>} 'green' | 'already-green' | 'escalated' | 'plan-red' |
|
|
979
979
|
* 'check-red' | 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' |
|
|
@@ -2745,11 +2745,12 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2745
2745
|
// (nothing in flight, the round just metered). It is the ONE owner of the stop request. The file is
|
|
2746
2746
|
// consumed here; `self.stop()` ends the Loop after this round exactly as a bound does; `ask`/`askFrom`
|
|
2747
2747
|
// then throw category `stopped`, which the step loop files as the checkpoint outcome `stopped` the way a
|
|
2748
|
-
// mid-step cap-halt is filed.
|
|
2749
|
-
|
|
2748
|
+
// mid-step cap-halt is filed. EVERY phase that runs rounds reads it here (scout, plan/replan drafter, step,
|
|
2749
|
+
// close-fix) — the one seam the money cap binds at; `relay` files the non-ralph phases' throw as `stopped`.
|
|
2750
|
+
if (stopFile !== null && !stopHit && existsSync(stopFile)) {
|
|
2750
2751
|
try { unlinkSync(stopFile); } catch { /* already consumed */ }
|
|
2751
2752
|
stopHit = true;
|
|
2752
|
-
emit('stop-requested', { phase, step: phase.slice('step:'.length), iteration: roundIteration, round: roundsThisAttempt, meaning: 'the person asked to stop; the leg ends after this round and a resume
|
|
2753
|
+
emit('stop-requested', { phase, ...(phase.startsWith('step:') ? { step: phase.slice('step:'.length) } : {}), iteration: roundIteration, round: roundsThisAttempt, meaning: 'the person asked to stop; the leg ends after this round and a resume continues the run' });
|
|
2753
2754
|
self.stop();
|
|
2754
2755
|
} else if (clock.expired()) {
|
|
2755
2756
|
attemptBounded = { iteration: roundIteration, cause: 'wall', reason: null };
|
|
@@ -2903,6 +2904,8 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2903
2904
|
// instead and every such fault came back `provider-red` — a broken runner
|
|
2904
2905
|
// reported as the provider's fault, and an upstream ask aimed at a library
|
|
2905
2906
|
// for our own missing wiring (the step-stalled lesson).
|
|
2907
|
+
// P5 item 5 — the PERSON's stop, honoured at a round boundary outside a step (scout, plan drafting, replan): nothing failed
|
|
2908
|
+
'stopped': [`You stopped the run during ${phase}. Nothing is discarded: the work on disk stands, and a resume continues the run.`, ['resume (the same run continues)', 'abandon the task']],
|
|
2906
2909
|
'interpreter-red': [`The ${phase} phase could not be set up — the runner or a primitive it was handed is not correctly bound.`, ['fix the wiring the detail names', 'retry the run', 'abandon the run']],
|
|
2907
2910
|
};
|
|
2908
2911
|
const [decision, options] = (Object.hasOwn(DECIDE, category) ? DECIDE[category] : undefined)
|
|
@@ -2914,7 +2917,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2914
2917
|
// `step-stalled` rides out as ITSELF, not as provider-red. Naming the
|
|
2915
2918
|
// escalation while returning a casualty label would launder a governance stop
|
|
2916
2919
|
// into transport noise at exactly the layer the readout reads.
|
|
2917
|
-
return category === 'cap-halt' || category === 'wall-halt' || category === 'step-stalled' || category === 'interpreter-red'
|
|
2920
|
+
return category === 'cap-halt' || category === 'wall-halt' || category === 'step-stalled' || category === 'interpreter-red' || category === 'stopped'
|
|
2918
2921
|
? category : 'provider-red';
|
|
2919
2922
|
};
|
|
2920
2923
|
|
|
@@ -4105,7 +4108,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
4105
4108
|
// is not passed — it survives only inside the governor, as the bound for the
|
|
4106
4109
|
// case the trend instrument is blind.
|
|
4107
4110
|
middle, judge: async () => judgeClose(), ladder: fixGovernor, emit: emitL, redact: scrub,
|
|
4108
|
-
closeTimeoutMs, cwd: workdir, workerWrites: w.workerWrites, closeDir,
|
|
4111
|
+
closeTimeoutMs, cwd: workdir, workerWrites: w.workerWrites, closeDir, phase: 'fix',
|
|
4109
4112
|
});
|
|
4110
4113
|
} catch (e) {
|
|
4111
4114
|
planExecuted();
|
package/src/ralph.js
CHANGED
|
@@ -562,9 +562,13 @@ export const CLOSE_FAULTS = Object.freeze({
|
|
|
562
562
|
* (PRD item 27/M3 Part B), forwarded to the raw `close` path's `runClose`
|
|
563
563
|
* call unchanged; a caller supplying `judge` instead already closed over
|
|
564
564
|
* its own `closeDir` and this param does nothing for it.
|
|
565
|
+
* @param {string} [opts.phase] the phase this loop runs in, when it is not the step loop
|
|
566
|
+
* (the fix loop passes `'fix'`): the person's-stop text then names it ("during the fix
|
|
567
|
+
* phase") instead of saying a resume re-enters "this step", which only the step loop has.
|
|
568
|
+
* Text only — it changes no category, outcome or option.
|
|
565
569
|
* @returns {Promise<'green'|'escalated'>}
|
|
566
570
|
*/
|
|
567
|
-
export async function ralph({ middle, close, judge, capRuns, ladder, emit, redact, closeTimeoutMs, cwd, expect, judged, gapKeep, workerWrites, closeDir }) {
|
|
571
|
+
export async function ralph({ middle, close, judge, capRuns, ladder, emit, redact, closeTimeoutMs, cwd, expect, judged, gapKeep, workerWrites, closeDir, phase }) {
|
|
568
572
|
// PARAM GUARD (the BA-4 class — a caller's malformed argument, which is the one
|
|
569
573
|
// thing this shell throws for; a WORKER's bad reach comes back as a refusal
|
|
570
574
|
// RESULT instead, and that rule is about feedback, not about the API contract).
|
|
@@ -654,7 +658,9 @@ export async function ralph({ middle, close, judge, capRuns, ladder, emit, redac
|
|
|
654
658
|
'revise the goal/spec so the work fits the time (same re-approval)',
|
|
655
659
|
'abandon the task']],
|
|
656
660
|
// P5 item 5 — the PERSON asked to stop at a round boundary: nothing failed, nothing is discarded.
|
|
657
|
-
'stopped': [
|
|
661
|
+
'stopped': [phase
|
|
662
|
+
? `You stopped the run during the ${phase} phase. Nothing is discarded: the work on disk stands, and a resume continues the run.`
|
|
663
|
+
: 'You stopped the run. Nothing is discarded: the work on disk stands, and a resume re-enters this step.',
|
|
658
664
|
['resume (the same run continues)', 'abandon the task']],
|
|
659
665
|
// F66 — the STALL WATCHDOG gave up: no round completed for the stall window,
|
|
660
666
|
// three times over, and reissuing the call did not recover it. It borrows both
|
package/src/replay.js
CHANGED
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
import { runBehaviour, formatBehaviour } from './behaviour.js';
|
|
105
105
|
import { shortSha } from './codeversion.js';
|
|
106
106
|
import { SPEND_RECORD_TYPES, chainSpend } from './ledger.js';
|
|
107
|
-
import { legsOf } from './legs.js';
|
|
107
|
+
import { legsOf, capKindOf } from './legs.js';
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
110
|
* @param {unknown} e
|
|
@@ -234,6 +234,16 @@ const ALL_REASON_MAX = 60;
|
|
|
234
234
|
* record fired) — printed literally, never blank, never a fabricated 0. */
|
|
235
235
|
const NONE_RECORDED = 'none recorded';
|
|
236
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Did this leg end on a cap? Reads the leg's own last escalation and `money-halt` record.
|
|
239
|
+
* @param {import('./legs.js').Leg} leg
|
|
240
|
+
* @returns {'money'|'time'|null}
|
|
241
|
+
*/
|
|
242
|
+
function legCapKind(leg) {
|
|
243
|
+
const esc = leg.outcome === 'escalated' ? leg.records.findLast((r) => r && r.type === 'escalation') : null;
|
|
244
|
+
return capKindOf({ outcome: leg.outcome, category: esc?.category ?? null, moneyHalt: leg.records.some((r) => r && r.type === 'money-halt') });
|
|
245
|
+
}
|
|
246
|
+
|
|
237
247
|
/**
|
|
238
248
|
* `stopReason` for a non-green outcome: `category — decision — detail`, every
|
|
239
249
|
* part present that the escalation actually carries (see the field-site
|
|
@@ -737,11 +747,21 @@ export function replayRun(spineEvents, auditEvents = [], { runId = null, auditAv
|
|
|
737
747
|
// why this leg stopped the step (code-owned words); null for a step that ended itself or is still running
|
|
738
748
|
/** @type {string|null} */
|
|
739
749
|
let stopReason = null;
|
|
750
|
+
// a single-leg run has no leg boundary to cut a step, but a cap still ends it: its open step reads capped too
|
|
751
|
+
// (hamr 2026-10-08), from the same rule (`capKindOf`) the run's own status word uses
|
|
752
|
+
if (!end && myLeg === null) {
|
|
753
|
+
const only = legs[0];
|
|
754
|
+
const capKind = only ? legCapKind(only) : null;
|
|
755
|
+
if (capKind !== null) stopReason = capKind === 'money' ? 'money cap' : 'time cap';
|
|
756
|
+
}
|
|
740
757
|
if (!end && myLeg !== null) {
|
|
741
758
|
const o = myLeg.outcome;
|
|
742
759
|
const isLastLeg = myLegIdx === legs.length - 1;
|
|
743
760
|
if (cutByLeg || (isLastLeg && o !== null && o !== 'green' && o !== 'already-green')) {
|
|
744
|
-
|
|
761
|
+
// hamr 2026-10-08: a leg that ended `escalated` on a money-halt / wall-halt reads as the cap, exactly as the
|
|
762
|
+
// run's own status word does (src/panel/status.js statusFor) — derived HERE so every surface follows
|
|
763
|
+
const capKind = legCapKind(myLeg);
|
|
764
|
+
stopReason = capKind === 'money' ? 'money cap' : capKind === 'time' ? 'time cap' : o === 'provider-red' ? 'provider failed'
|
|
745
765
|
: o === 'step-stalled' ? 'step stalled' : o === 'stopped' ? 'you stopped it' : o === null ? 'died' : o;
|
|
746
766
|
}
|
|
747
767
|
}
|
|
@@ -832,7 +852,7 @@ export function replayRun(spineEvents, auditEvents = [], { runId = null, auditAv
|
|
|
832
852
|
return {
|
|
833
853
|
id,
|
|
834
854
|
occurrence: idOccurrence.get(id),
|
|
835
|
-
...(myLeg === null ? {} : { stopReason, continued, tryNumber }),
|
|
855
|
+
...(myLeg === null && stopReason === null ? {} : { stopReason, continued, tryNumber }),
|
|
836
856
|
outcome: end ? (end.outcome ?? null) : null,
|
|
837
857
|
...buildOccurrenceMetrics(startSeq, startTs, endSeq, endTs, roundsInWindow),
|
|
838
858
|
checks: { passed, failed },
|
package/src/source.js
CHANGED
|
@@ -1148,3 +1148,15 @@ export function frontDoorFromManifest(read) {
|
|
|
1148
1148
|
if (typeof destination !== 'string' || !destination) return null;
|
|
1149
1149
|
return { destination };
|
|
1150
1150
|
}
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* The $0 refusal for a non-repo source, worded by the kind the source door recorded (`prep.manifest.kind`) — a single
|
|
1154
|
+
* file is not a "folder", and Source must be the repo folder with the file named in Destination (hamr 2026-10-05).
|
|
1155
|
+
* @param {string} kind `prep.manifest.kind`: 'file' | 'folder' | 'url' (a 'repo' never reaches this)
|
|
1156
|
+
* @returns {string}
|
|
1157
|
+
*/
|
|
1158
|
+
export function nonRepoSourceMessage(kind) {
|
|
1159
|
+
if (kind === 'file') return 'Source is a single file. Source must be the repo folder — put the file in Destination, like src/digest.js. Nothing spent.';
|
|
1160
|
+
if (kind === 'folder') return "Source is a plain folder, not a code project (no git repo found). bareloop can't check this kind of job yet. Nothing spent.";
|
|
1161
|
+
return "Source is not a code project (no git repo found). bareloop can't check this kind of job yet. Nothing spent.";
|
|
1162
|
+
}
|
package/types/legs.d.ts
CHANGED
|
@@ -56,6 +56,18 @@ export function watchdogNotePath(spineFile: string, leg: number): string;
|
|
|
56
56
|
* @returns {string}
|
|
57
57
|
*/
|
|
58
58
|
export function stopFilePath(spineFile: string): string;
|
|
59
|
+
/**
|
|
60
|
+
* Did a run (or one leg) END on a cap? The ONE spelling of the rule every surface follows (hamr 2026-10-08): the
|
|
61
|
+
* outcome is `cap-halt` / `wall-halt`, or it is `escalated` and the last escalation's category names the cap —
|
|
62
|
+
* `wall-halt`, or `cap-halt` backed by a `money-halt` record (a strike-ladder `cap-halt` is not the money cap).
|
|
63
|
+
* @param {{outcome: string|null|undefined, category?: string|null, moneyHalt?: boolean}} o
|
|
64
|
+
* @returns {'money'|'time'|null}
|
|
65
|
+
*/
|
|
66
|
+
export function capKindOf(o: {
|
|
67
|
+
outcome: string | null | undefined;
|
|
68
|
+
category?: string | null;
|
|
69
|
+
moneyHalt?: boolean;
|
|
70
|
+
}): "money" | "time" | null;
|
|
59
71
|
/** The marker record's `type`. */
|
|
60
72
|
export const LEG_RESUME: "leg-resume";
|
|
61
73
|
export type Leg = {
|
|
@@ -41,7 +41,7 @@ export function checkHumanGuard(req: import("node:http").IncomingMessage, { toke
|
|
|
41
41
|
* @param {{ port: number, token: string, env?: Record<string,string|undefined>,
|
|
42
42
|
* sessionsRoot?: string, spawnFn?: typeof realSpawn, bareloopBin?: string,
|
|
43
43
|
* jobsDir?: string, fetchImpl?: typeof fetch, home?: string,
|
|
44
|
-
* startFrom?: {get: (runid: string, o?: {model?: string}) => any, getImport: (id: string, o?: {model?: string}) => any} }} opts
|
|
44
|
+
* startFrom?: {get: (runid: string, o?: {model?: string}) => any, getImport: (id: string, o?: {model?: string}) => any, listJobs?: (o?: {model?: string}) => any[]} }} opts
|
|
45
45
|
*/
|
|
46
46
|
export function createAuthorRoutes(opts: {
|
|
47
47
|
port: number;
|
|
@@ -60,6 +60,9 @@ export function createAuthorRoutes(opts: {
|
|
|
60
60
|
getImport: (id: string, o?: {
|
|
61
61
|
model?: string;
|
|
62
62
|
}) => any;
|
|
63
|
+
listJobs?: (o?: {
|
|
64
|
+
model?: string;
|
|
65
|
+
}) => any[];
|
|
63
66
|
};
|
|
64
67
|
}): {
|
|
65
68
|
handle: (req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse, pathname: string, body: any) => boolean;
|
|
@@ -67,13 +67,6 @@ export function validateReuseCard(card: any, opts?: {
|
|
|
67
67
|
ok: false;
|
|
68
68
|
error: string;
|
|
69
69
|
};
|
|
70
|
-
/**
|
|
71
|
-
* The $0 refusal for a non-repo source, worded by the kind the source door recorded (`prep.manifest.kind`) — a single
|
|
72
|
-
* file is not a "folder", and Source must be the repo folder with the file named in Destination (hamr 2026-10-05).
|
|
73
|
-
* @param {string} kind `prep.manifest.kind`: 'file' | 'folder' | 'url' (a 'repo' never reaches this)
|
|
74
|
-
* @returns {string}
|
|
75
|
-
*/
|
|
76
|
-
export function nonRepoSourceMessage(kind: string): string;
|
|
77
70
|
/**
|
|
78
71
|
* The ONE Destination rule for a REPO source (fresh card and Reuse card alike): there Destination is the write fence,
|
|
79
72
|
* relative to the repo, so an absolute path is refused — at $0, before any copy or model call. A FOLDER source (or a
|
|
@@ -153,9 +146,11 @@ export const CARD_FIELDS: readonly string[];
|
|
|
153
146
|
export const REUSE_OPEN_FIELDS: readonly string[];
|
|
154
147
|
/** every card field that is NOT open on a reuse card, in card order */
|
|
155
148
|
export const REUSE_LOCKED_FIELDS: readonly string[];
|
|
149
|
+
export { nonRepoSourceMessage };
|
|
156
150
|
export type StepLine = {
|
|
157
151
|
id: string;
|
|
158
152
|
label: string;
|
|
159
153
|
status: "running" | "done" | "failed";
|
|
160
154
|
detail: string;
|
|
161
155
|
};
|
|
156
|
+
import { nonRepoSourceMessage } from '../source.js';
|
package/types/panel/server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `[✓]`/`[✗]`/`[▶]` — the ONLY vocabulary a result is ever rendered in
|
|
2
|
+
* `[✓]`/`[✗]`/`[■]`/`[▶]` — the ONLY vocabulary a result is ever rendered in
|
|
3
3
|
* (auto-memory `ui-verdict-words.md`: never the words green/red/soft-green
|
|
4
4
|
* anywhere in the page). `null` (no `job-end` reached — a killed-mid-run or
|
|
5
5
|
* still-running spine) reads `▶` — the same "in progress / unresolved" glyph
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* running attempt and an archived spine that never reached its own end are
|
|
8
8
|
* the same fact from this read-only side: no verdict has been recorded yet.
|
|
9
9
|
* @param {string|null} outcome
|
|
10
|
-
* @returns {'✓'|'✗'|'▶'}
|
|
10
|
+
* @returns {'✓'|'✗'|'■'|'▶'}
|
|
11
11
|
*/
|
|
12
|
-
export function glyphForOutcome(outcome: string | null): "\u2713" | "\u2717" | "\u25B6";
|
|
12
|
+
export function glyphForOutcome(outcome: string | null): "\u2713" | "\u2717" | "\u25A0" | "\u25B6";
|
|
13
13
|
/**
|
|
14
14
|
* `deterministic` (green) / `rubric` (soft-green) / `unknown` (a class this
|
|
15
15
|
* reader doesn't recognize) — the settled UI wording (PANEL-BUILD.md §6),
|
|
@@ -381,7 +381,7 @@ export function reuseLine(record: {
|
|
|
381
381
|
* to copy: change the job: Clear the card). `null` when the runid is not listed.
|
|
382
382
|
* @param {string} runid
|
|
383
383
|
* @param {{ home?: string, model?: string }} [opts] `model` = the Model box's current Name (the estimate counts only runs on that worker); absent = the card's own Model
|
|
384
|
-
* @returns {{ok: true, origin: {runid: string, job: string}, card: Record<string, any>, from: 'signed job',
|
|
384
|
+
* @returns {{ok: true, origin: {runid: string, job: string}, outcome: string|null, card: Record<string, any>, from: 'signed job',
|
|
385
385
|
* locked: readonly string[], open: readonly string[], note: string|null,
|
|
386
386
|
* specHash: string, workflowKey: string, spec: any, specPath: string,
|
|
387
387
|
* trackRecord: ReturnType<typeof trackRecordFor>, line: string}|{ok: false, error: string}|null}
|
|
@@ -395,6 +395,7 @@ export function getStartFrom(runid: string, opts?: {
|
|
|
395
395
|
runid: string;
|
|
396
396
|
job: string;
|
|
397
397
|
};
|
|
398
|
+
outcome: string | null;
|
|
398
399
|
card: Record<string, any>;
|
|
399
400
|
from: "signed job";
|
|
400
401
|
locked: readonly string[];
|
|
@@ -410,6 +411,23 @@ export function getStartFrom(runid: string, opts?: {
|
|
|
410
411
|
ok: false;
|
|
411
412
|
error: string;
|
|
412
413
|
} | null;
|
|
414
|
+
/**
|
|
415
|
+
* The Reuse workflow picker's list: one entry per JOB (same-job identity = `workflowKey`, the one `getStartFrom` uses)
|
|
416
|
+
* that has at least one GREEN local run, newest green first. `runid` = that job's newest green run, so the page hands
|
|
417
|
+
* it to the existing start-from flow unchanged; `line` is `getStartFrom`'s own estimate line (never a second
|
|
418
|
+
* spelling). Local runs only — an imported job keeps its Reuse button in the Import view. $0, read-only.
|
|
419
|
+
* @param {{ home?: string, model?: string }} [opts] `model` passes through to the estimate line, as start-from's does
|
|
420
|
+
* @returns {{job: string, runid: string, checkType: 'deterministic'|'rubric', line: string}[]}
|
|
421
|
+
*/
|
|
422
|
+
export function listReuseJobs(opts?: {
|
|
423
|
+
home?: string;
|
|
424
|
+
model?: string;
|
|
425
|
+
}): {
|
|
426
|
+
job: string;
|
|
427
|
+
runid: string;
|
|
428
|
+
checkType: "deterministic" | "rubric";
|
|
429
|
+
line: string;
|
|
430
|
+
}[];
|
|
413
431
|
/**
|
|
414
432
|
* Reuse workflow, for an IMPORTED job: the bundle is read AGAIN now and must be clean (`readBundle` — its own hash
|
|
415
433
|
* covers every close script, so a swapped script is `bundle-tampered` here), must resolve its own `bareloop`
|
package/types/panel/status.d.ts
CHANGED
|
@@ -14,6 +14,26 @@ export function statusFor(o: {
|
|
|
14
14
|
sign: string;
|
|
15
15
|
word: string;
|
|
16
16
|
};
|
|
17
|
+
/**
|
|
18
|
+
* The word one PART of a run wears on the step map, the step cards and the Audit rows (hamr 2026-10-07: `[✓] passed` /
|
|
19
|
+
* `[✗] failed`, never "done"). Reads the same table; `running` is the page's call (it knows which part is live), so a
|
|
20
|
+
* part with no verdict and no cut-off is `passed` here.
|
|
21
|
+
* drafting, green/satisfied/already-green outcome, or a part that simply happened (scout/plan/replan/judge) -> passed
|
|
22
|
+
* a red outcome -> its run-outcome word (failed; a cap/stop outcome keeps capped/stopped)
|
|
23
|
+
* a part its leg's halt cut off (`stopReason`): 'money cap' / 'time cap' -> capped, 'you stopped it' -> stopped,
|
|
24
|
+
* 'died' -> died, anything else (provider failed, step stalled, a raw outcome) -> failed
|
|
25
|
+
* @param {{kind?: string, outcome?: string|null, stopReason?: string|null}} part
|
|
26
|
+
* @returns {{key: StatusKey, sign: string, word: string}}
|
|
27
|
+
*/
|
|
28
|
+
export function partStatusFor(part: {
|
|
29
|
+
kind?: string;
|
|
30
|
+
outcome?: string | null;
|
|
31
|
+
stopReason?: string | null;
|
|
32
|
+
}): {
|
|
33
|
+
key: StatusKey;
|
|
34
|
+
sign: string;
|
|
35
|
+
word: string;
|
|
36
|
+
};
|
|
17
37
|
/** the sentence a passed run carries as its reason (the Ended block's `line`) */
|
|
18
38
|
export const GOAL_MET_LINE: "goal met";
|
|
19
39
|
/** @typedef {'running'|'waiting'|'passed'|'failed'|'capped'|'stopped'|'died'} StatusKey */
|
|
@@ -40,7 +60,7 @@ export const STATUS: Readonly<{
|
|
|
40
60
|
word: "capped";
|
|
41
61
|
}>;
|
|
42
62
|
stopped: Readonly<{
|
|
43
|
-
sign: "
|
|
63
|
+
sign: "■";
|
|
44
64
|
word: "stopped";
|
|
45
65
|
}>;
|
|
46
66
|
died: Readonly<{
|
package/types/planrun.d.ts
CHANGED
|
@@ -397,10 +397,10 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
|
|
|
397
397
|
* the resume-plan-red option, the fix-loop terminal) say "resume is `run-u`-only
|
|
398
398
|
* in v1" instead of naming a flag that would fail if typed.
|
|
399
399
|
* @param {string|null} [opts.stopFile=null] P5 item 5 — the run's STOP REQUEST file
|
|
400
|
-
* (`stopFilePath(spine)`, src/legs.js). Read at ONE seam, the round boundary of
|
|
401
|
-
* (the `metered` callback, where the money cap binds): present = emit `stop-requested`, consume
|
|
400
|
+
* (`stopFilePath(spine)`, src/legs.js). Read at ONE seam, the round boundary of EVERY worker
|
|
401
|
+
* (scout, plan/replan drafter, step, close-fix: the `metered` callback, where the money cap binds): present = emit `stop-requested`, consume
|
|
402
402
|
* the file, end the Loop after that round, and end the leg `stopped` (a checkpoint outcome, filed
|
|
403
|
-
* the way a mid-step cap-halt is; resume re-enters that step). Nothing is in flight at that
|
|
403
|
+
* the way a mid-step cap-halt is; resume re-enters that step, or continues the phase). Nothing is in flight at that
|
|
404
404
|
* point, so the spend is exact. `null` = no stop surface (the default).
|
|
405
405
|
* @returns {Promise<string>} 'green' | 'already-green' | 'escalated' | 'plan-red' |
|
|
406
406
|
* 'check-red' | 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' |
|
package/types/ralph.d.ts
CHANGED
|
@@ -242,9 +242,13 @@ export function boundGap(s: string, keepPattern?: string): string;
|
|
|
242
242
|
* (PRD item 27/M3 Part B), forwarded to the raw `close` path's `runClose`
|
|
243
243
|
* call unchanged; a caller supplying `judge` instead already closed over
|
|
244
244
|
* its own `closeDir` and this param does nothing for it.
|
|
245
|
+
* @param {string} [opts.phase] the phase this loop runs in, when it is not the step loop
|
|
246
|
+
* (the fix loop passes `'fix'`): the person's-stop text then names it ("during the fix
|
|
247
|
+
* phase") instead of saying a resume re-enters "this step", which only the step loop has.
|
|
248
|
+
* Text only — it changes no category, outcome or option.
|
|
245
249
|
* @returns {Promise<'green'|'escalated'>}
|
|
246
250
|
*/
|
|
247
|
-
export function ralph({ middle, close, judge, capRuns, ladder, emit, redact, closeTimeoutMs, cwd, expect, judged, gapKeep, workerWrites, closeDir }: {
|
|
251
|
+
export function ralph({ middle, close, judge, capRuns, ladder, emit, redact, closeTimeoutMs, cwd, expect, judged, gapKeep, workerWrites, closeDir, phase }: {
|
|
248
252
|
middle: (iteration: number, gap?: string) => void | Promise<void>;
|
|
249
253
|
close?: string[] | undefined;
|
|
250
254
|
judge?: (() => Promise<{
|
|
@@ -270,6 +274,7 @@ export function ralph({ middle, close, judge, capRuns, ladder, emit, redact, clo
|
|
|
270
274
|
gapKeep?: string | undefined;
|
|
271
275
|
workerWrites?: (() => string[]) | undefined;
|
|
272
276
|
closeDir?: string | null | undefined;
|
|
277
|
+
phase?: string | undefined;
|
|
273
278
|
}): Promise<"green" | "escalated">;
|
|
274
279
|
/**
|
|
275
280
|
* What the close child must NOT be able to read (operator ruling, 2026-07-31:
|