bareloop 0.34.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/bareloop.context.md +276 -20
  3. package/package.json +1 -1
  4. package/src/authorrun.js +3 -2
  5. package/src/bundlerun.js +17 -12
  6. package/src/draftspend.js +197 -0
  7. package/src/exits.js +10 -1
  8. package/src/index.js +3 -3
  9. package/src/interviewrun.js +7 -7
  10. package/src/job.js +37 -0
  11. package/src/judged.js +2 -2
  12. package/src/kinds.js +2 -1
  13. package/src/ledger.js +101 -5
  14. package/src/legs.js +171 -0
  15. package/src/monthly.js +111 -72
  16. package/src/movefile.js +22 -0
  17. package/src/npminstall.js +55 -0
  18. package/src/panel/authorroutes.js +84 -14
  19. package/src/panel/authorsession.js +490 -125
  20. package/src/panel/importroutes.js +359 -0
  21. package/src/panel/importrun.js +203 -0
  22. package/src/panel/index.html +1194 -213
  23. package/src/panel/runroutes.js +219 -0
  24. package/src/panel/server.js +964 -79
  25. package/src/panel/settingsroutes.js +52 -2
  26. package/src/panel/status.js +51 -0
  27. package/src/planrun.js +60 -26
  28. package/src/providerrows.js +17 -0
  29. package/src/ralph.js +4 -1
  30. package/src/replay.js +139 -19
  31. package/src/replayio.js +11 -4
  32. package/src/reuse.js +20 -3
  33. package/src/run.js +6 -3
  34. package/src/runlist.js +75 -7
  35. package/src/source.js +43 -4
  36. package/src/text.js +20 -0
  37. package/src/u-patient.js +3 -2
  38. package/src/u-readout.js +9 -1
  39. package/src/u-watchdog.mjs +4 -2
  40. package/src/userrun.js +192 -65
  41. package/src/worktree.js +120 -0
  42. package/types/draftspend.d.ts +141 -0
  43. package/types/job.d.ts +25 -0
  44. package/types/ledger.d.ts +44 -0
  45. package/types/legs.d.ts +103 -0
  46. package/types/monthly.d.ts +23 -18
  47. package/types/movefile.d.ts +9 -0
  48. package/types/npminstall.d.ts +30 -0
  49. package/types/panel/authorroutes.d.ts +23 -1
  50. package/types/panel/authorsession.d.ts +110 -2
  51. package/types/panel/importroutes.d.ts +129 -0
  52. package/types/panel/importrun.d.ts +77 -0
  53. package/types/panel/runroutes.d.ts +32 -0
  54. package/types/panel/server.d.ts +266 -2
  55. package/types/panel/settingsroutes.d.ts +6 -1
  56. package/types/panel/status.d.ts +51 -0
  57. package/types/planrun.d.ts +13 -21
  58. package/types/providerrows.d.ts +12 -0
  59. package/types/ralph.d.ts +19 -0
  60. package/types/replay.d.ts +25 -1
  61. package/types/replayio.d.ts +2 -2
  62. package/types/reuse.d.ts +5 -0
  63. package/types/run.d.ts +6 -2
  64. package/types/runlist.d.ts +52 -2
  65. package/types/source.d.ts +13 -5
  66. package/types/text.d.ts +19 -0
  67. package/types/userrun.d.ts +10 -0
  68. package/types/worktree.d.ts +48 -0
@@ -50,6 +50,140 @@ export function checkTypeTitle(verdictType: string | null, atIso?: string | null
50
50
  * @returns {string}
51
51
  */
52
52
  export function formatTimestamp(ts: string | number | Date): string;
53
+ /**
54
+ * P5 item 2 — may this run be resumed, and under which spec file? The ONE owner of
55
+ * "would the engine accept `--resume` for this run" on the panel side: it asks the SAME
56
+ * library readers the engine's own refusals come from (`readResume` with
57
+ * `CHECKPOINT_OUTCOMES`, `checkpointAgeGate`), plus the two facts the panel needs to
58
+ * spawn it (a signed spec file beside the run, whose hash is the one the run was signed
59
+ * under). `ok:false` means the panel declines to offer a button the engine would refuse
60
+ * by design; it carries a plain `why`.
61
+ *
62
+ * The spec file is the run's own `resolved-spec.json` or one of its
63
+ * `resolved-spec-r<k>.json` siblings (a resume under raised caps writes one), picked by
64
+ * HASH against the run's `job-start.specHash` — a run resumed under raised caps must be
65
+ * resumed again under ITS caps, never the original's. No match = no resume offered.
66
+ * @param {{ spine: string, job: string, pid?: number }} row
67
+ * @param {any[]} records the run's raw spine records
68
+ * @returns {{ok: true, specPath: string, spec: any, specHash: string, budgetUsd: number|null,
69
+ * maxWallMin: number|null, spentUsd: number|null, spendComplete: boolean,
70
+ * draftSpentUsd: number|null, draftSpendComplete: boolean|null, wallUsedMs: number|null}|{ok: false, why: string}}
71
+ */
72
+ export function resumePlanFor(row: {
73
+ spine: string;
74
+ job: string;
75
+ pid?: number;
76
+ }, records: any[]): {
77
+ ok: true;
78
+ specPath: string;
79
+ spec: any;
80
+ specHash: string;
81
+ budgetUsd: number | null;
82
+ maxWallMin: number | null;
83
+ spentUsd: number | null;
84
+ spendComplete: boolean;
85
+ draftSpentUsd: number | null;
86
+ draftSpendComplete: boolean | null;
87
+ wallUsedMs: number | null;
88
+ } | {
89
+ ok: false;
90
+ why: string;
91
+ };
92
+ /**
93
+ * P5-R — the Audit tab's LEG DIVIDERS (hamr 2026-10-02: "clear audit mention resume"). One per resume, CODE-OWNED
94
+ * fixed sentences (ui-verdict-words: never model text) built from how the previous leg ended (`after`) and what the
95
+ * run had spent when it stopped:
96
+ * `── stopped: money cap reached ($8.00) · resumed 2026-10-03 14:10 ──`.
97
+ * `beforePart` is the index of the first part of the leg that picked up (the parts list length when that leg has not
98
+ * started a part yet). Money is the chain's own spend at that leg's end on the ONE basis ({@link chainSpend}).
99
+ * @param {any[]} spineRecords the run's raw records
100
+ * @param {any[]} parts `summary.parts` (each carries `leg` on a resumed run)
101
+ * @returns {{leg: number, beforePart: number, after: string|null, at: string|null, text: string}[]}
102
+ */
103
+ export function legDividersFor(spineRecords: any[], parts: any[]): {
104
+ leg: number;
105
+ beforePart: number;
106
+ after: string | null;
107
+ at: string | null;
108
+ text: string;
109
+ }[];
110
+ /**
111
+ * P5 item 1 — the ENDED block: why a run ended, what to do next, and which buttons the
112
+ * engine would accept. CODE-OWNED FIXED SENTENCES (ui-verdict-words: never model text);
113
+ * the only slot-fills are numbers and the engine's own recorded detail. ONE owner,
114
+ * feeding both `getRunDetail` (`ended`) and `summarizeRow` (`endedLine`). `null` while
115
+ * a run is live (no Ended block while running).
116
+ *
117
+ * The table (outcome → reason / next / buttons) is PANEL-BUILD.md P5 item 1. The
118
+ * Resume button appears only when `o.resume` says the engine would accept it; "Reuse workflow"
119
+ * (replaces P5 item 3, 2026-10-03) is offered on green rows only, and starts a NEW run.
120
+ * @param {{outcome: string|null, stopReason: string|null, spentUsd: number|null, budgetUsd: number|null, lastEscalation?: any}} summary
121
+ * @param {{died: boolean, lastThing: string|null}} death
122
+ * @param {{resume?: {ok: boolean, why?: string}|null, destinationRefused?: string|null, moneyHalt?: boolean,
123
+ * worktree?: {folder: string, repo: string, branch: string|null, exists: boolean}|null}} [o]
124
+ * `worktree` (P6 item 1): the run's worktree in the person's repo, from {@link worktreeOfRun}; absent for every other run
125
+ * @returns {{reason: string, next: string, line: string, actions: {id: string, label: string}[]}|null}
126
+ */
127
+ export function endedFor(summary: {
128
+ outcome: string | null;
129
+ stopReason: string | null;
130
+ spentUsd: number | null;
131
+ budgetUsd: number | null;
132
+ lastEscalation?: any;
133
+ }, death: {
134
+ died: boolean;
135
+ lastThing: string | null;
136
+ }, o?: {
137
+ resume?: {
138
+ ok: boolean;
139
+ why?: string;
140
+ } | null;
141
+ destinationRefused?: string | null;
142
+ moneyHalt?: boolean;
143
+ worktree?: {
144
+ folder: string;
145
+ repo: string;
146
+ branch: string | null;
147
+ exists: boolean;
148
+ } | null;
149
+ }): {
150
+ reason: string;
151
+ next: string;
152
+ line: string;
153
+ actions: {
154
+ id: string;
155
+ label: string;
156
+ }[];
157
+ } | null;
158
+ /**
159
+ * The Resume route's reader (P5 item 2): the listed row plus the resume plan, from the
160
+ * ONE {@link resumePlanFor} the Ended block asks. `null` when the runid is not listed
161
+ * or its spine is gone.
162
+ * @param {string} runid
163
+ * @param {{ home?: string }} [opts]
164
+ * @returns {{row: any, plan: any}|null}
165
+ */
166
+ export function getResumeContext(runid: string, opts?: {
167
+ home?: string;
168
+ }): {
169
+ row: any;
170
+ plan: any;
171
+ } | null;
172
+ /**
173
+ * The Stop route's reader (P5 item 5): the listed row, whether its LATEST leg is live (`runIsAlive`: the row's pid
174
+ * is the latest leg's, folded from the run list's `leg-start`), and whether its spine exists yet. `null` when the
175
+ * runid is not listed.
176
+ * @param {string} runid
177
+ * @param {{ home?: string }} [opts]
178
+ * @returns {{row: any, live: boolean, spineExists: boolean}|null}
179
+ */
180
+ export function getStopContext(runid: string, opts?: {
181
+ home?: string;
182
+ }): {
183
+ row: any;
184
+ live: boolean;
185
+ spineExists: boolean;
186
+ } | null;
53
187
  /**
54
188
  * `GET /api/runs` — every listed run, NEWEST FIRST BY `at` (never by file/
55
189
  * append order — a backfill scan appends rows in sorted-PATH order, which
@@ -195,6 +329,127 @@ export function getRunRounds(runid: string, query?: {
195
329
  export function getRunJob(runid: string, opts?: {
196
330
  home?: string;
197
331
  }): any | null;
332
+ /**
333
+ * The track record of a WORKFLOW: every LISTED run whose signed spec (the one its latest leg ran under) has this
334
+ * `workflowKey` — the same goal, checks, guardrails, model and close, whatever source, destination or caps. One run =
335
+ * ONE entry (a resumed run is one run, its FINAL outcome — `legsOf`), and a run that is still live is neither green
336
+ * nor not-green yet. `avgSpendUsd` / `avgWallMs` average the FINISHED runs (a recorded terminal) whose figure is exact
337
+ * (chain spend priced; working time from readable stamps, resume gaps excluded) — `null` when no run qualifies, never 0.
338
+ * Read from the runs the panel already lists: no registry, no plan handover (hamr ruling A).
339
+ * `opts.worker` narrows the record to runs on the same WORKER (provider + baseUrl + model — Model is open on a reuse, so
340
+ * the key no longer separates models): `undefined` = every model, a worker = only runs whose signed spec ran that one,
341
+ * `null` = no model chosen, so no run matches.
342
+ * @param {string} key a {@link workflowKey}
343
+ * @param {{ home?: string, worker?: {provider: string, baseUrl?: string, model: string}|null }} [opts]
344
+ * @returns {{runs: number, green: number, notGreen: number, live: number, finished: number, avgSpendUsd: number|null, avgWallMs: number|null}}
345
+ */
346
+ export function trackRecordFor(key: string, opts?: {
347
+ home?: string;
348
+ worker?: {
349
+ provider: string;
350
+ baseUrl?: string;
351
+ model: string;
352
+ } | null;
353
+ }): {
354
+ runs: number;
355
+ green: number;
356
+ notGreen: number;
357
+ live: number;
358
+ finished: number;
359
+ avgSpendUsd: number | null;
360
+ avgWallMs: number | null;
361
+ };
362
+ /**
363
+ * The estimate line above a reuse card (code-owned fixed text): `Same job — G green · N not green · about $X and
364
+ * M min a run`, from the workflow's own listed runs. What is unknown is said, never rendered as $0 or 0 min.
365
+ * @param {{runs?: number, green: number, notGreen: number, finished?: number, avgSpendUsd: number|null, avgWallMs?: number|null}|null} record
366
+ * @returns {string}
367
+ */
368
+ export function reuseLine(record: {
369
+ runs?: number;
370
+ green: number;
371
+ notGreen: number;
372
+ finished?: number;
373
+ avgSpendUsd: number | null;
374
+ avgWallMs?: number | null;
375
+ } | null): string;
376
+ /**
377
+ * `start-from` (the route keeps its name) for one listed run: the card prefilled from the run's SIGNED job, which
378
+ * boxes are locked and which are open, and the track record. Locked boxes carry the signed values, read by the
379
+ * SAME readers the Job tab uses (`successFromSpec`/`guardrailsFromSpec`); the open four — Source, Destination, the
380
+ * $ cap and the Time cap — start at what the run had. `ok:false` when the run's signed job is not on disk (nothing
381
+ * to copy: change the job: Clear the card). `null` when the runid is not listed.
382
+ * @param {string} runid
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',
385
+ * locked: readonly string[], open: readonly string[], note: string|null,
386
+ * specHash: string, workflowKey: string, spec: any, specPath: string,
387
+ * trackRecord: ReturnType<typeof trackRecordFor>, line: string}|{ok: false, error: string}|null}
388
+ */
389
+ export function getStartFrom(runid: string, opts?: {
390
+ home?: string;
391
+ model?: string;
392
+ }): {
393
+ ok: true;
394
+ origin: {
395
+ runid: string;
396
+ job: string;
397
+ };
398
+ card: Record<string, any>;
399
+ from: "signed job";
400
+ locked: readonly string[];
401
+ open: readonly string[];
402
+ note: string | null;
403
+ specHash: string;
404
+ workflowKey: string;
405
+ spec: any;
406
+ specPath: string;
407
+ trackRecord: ReturnType<typeof trackRecordFor>;
408
+ line: string;
409
+ } | {
410
+ ok: false;
411
+ error: string;
412
+ } | null;
413
+ /**
414
+ * Reuse workflow, for an IMPORTED job: the bundle is read AGAIN now and must be clean (`readBundle` — its own hash
415
+ * covers every close script, so a swapped script is `bundle-tampered` here), must resolve its own `bareloop`
416
+ * dependency (`checkBundleDeps`), and every close stage's signed sha256 must match the script's bytes on disk
417
+ * (`checkCloseByteSignature`, over the spec with `$BARELOOP_BUNDLE` resolved to this folder — the same in-memory
418
+ * substitution `bareloop run <bundle>` makes). The reuse spec is that resolved spec; the close scripts stay where
419
+ * they were just verified (the engine re-verifies their bytes at run start and before every close run). The person
420
+ * signs it on this machine at [Sign & run]. Same card, same locked/open boxes as a run's reuse. `null` when no such
421
+ * imported job.
422
+ * @param {string} id
423
+ * @param {{ home?: string, model?: string }} [opts] `model` as for {@link getStartFrom}
424
+ * @returns {{ok: true, origin: {runid: null, job: string, importId: string}, card: Record<string, any>, from: 'imported job',
425
+ * locked: readonly string[], open: readonly string[], note: string|null, specHash: string, workflowKey: string, spec: any,
426
+ * specPath: null, trackRecord: ReturnType<typeof trackRecordFor>, line: string}|{ok: false, error: string}|null}
427
+ */
428
+ export function getStartFromImport(id: string, opts?: {
429
+ home?: string;
430
+ model?: string;
431
+ }): {
432
+ ok: true;
433
+ origin: {
434
+ runid: null;
435
+ job: string;
436
+ importId: string;
437
+ };
438
+ card: Record<string, any>;
439
+ from: "imported job";
440
+ locked: readonly string[];
441
+ open: readonly string[];
442
+ note: string | null;
443
+ specHash: string;
444
+ workflowKey: string;
445
+ spec: any;
446
+ specPath: null;
447
+ trackRecord: ReturnType<typeof trackRecordFor>;
448
+ line: string;
449
+ } | {
450
+ ok: false;
451
+ error: string;
452
+ } | null;
198
453
  /**
199
454
  * Handle one request against the read-only API + the page. Exported
200
455
  * separately from {@link createPanelServer} so tests can drive it without a
@@ -211,7 +466,7 @@ export function getRunJob(runid: string, opts?: {
211
466
  * only `createPanelServer` below always supplies one.
212
467
  * @param {import('node:http').IncomingMessage} req
213
468
  * @param {import('node:http').ServerResponse} res
214
- * @param {{ home?: string, port: number, token?: string, authorRoutes?: ReturnType<typeof createAuthorRoutes>, settingsRoutes?: ReturnType<typeof createSettingsRoutes> }} opts
469
+ * @param {{ home?: string, port: number, token?: string, authorRoutes?: ReturnType<typeof createAuthorRoutes>, settingsRoutes?: ReturnType<typeof createSettingsRoutes>, runRoutes?: ReturnType<typeof createRunRoutes>, importRoutes?: ReturnType<typeof createImportRoutes> }} opts
215
470
  */
216
471
  export function handleRequest(req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse, opts: {
217
472
  home?: string;
@@ -219,6 +474,8 @@ export function handleRequest(req: import("node:http").IncomingMessage, res: imp
219
474
  token?: string;
220
475
  authorRoutes?: ReturnType<typeof createAuthorRoutes>;
221
476
  settingsRoutes?: ReturnType<typeof createSettingsRoutes>;
477
+ runRoutes?: ReturnType<typeof createRunRoutes>;
478
+ importRoutes?: ReturnType<typeof createImportRoutes>;
222
479
  }): void;
223
480
  /**
224
481
  * Start the panel server. Binds `127.0.0.1` ONLY. A taken port is a LOUD,
@@ -238,7 +495,8 @@ export function handleRequest(req: import("node:http").IncomingMessage, res: imp
238
495
  * this package's own `bin/bareloop.mjs`).
239
496
  * @param {{ port?: number, home?: string, env?: Record<string,string|undefined>,
240
497
  * sessionsRoot?: string, spawnFn?: (...a: any[]) => any, bareloopBin?: string,
241
- * fetchImpl?: typeof fetch }} [opts]
498
+ * fetchImpl?: typeof fetch, settleMs?: number, userHome?: string,
499
+ * openFolderSpawn?: (cmd: string, args: string[], o: object) => any, platform?: string }} [opts]
242
500
  * @returns {Promise<{ server: import('node:http').Server, port: number, token: string, close: () => Promise<void> }>}
243
501
  */
244
502
  export function createPanelServer(opts?: {
@@ -249,6 +507,10 @@ export function createPanelServer(opts?: {
249
507
  spawnFn?: (...a: any[]) => any;
250
508
  bareloopBin?: string;
251
509
  fetchImpl?: typeof fetch;
510
+ settleMs?: number;
511
+ userHome?: string;
512
+ openFolderSpawn?: (cmd: string, args: string[], o: object) => any;
513
+ platform?: string;
252
514
  }): Promise<{
253
515
  server: import("node:http").Server;
254
516
  port: number;
@@ -299,4 +561,6 @@ export const ROUNDS_PAGE_DEFAULT: 50;
299
561
  export const ROUNDS_PAGE_MAX: 200;
300
562
  import { createAuthorRoutes } from './authorroutes.js';
301
563
  import { createSettingsRoutes } from './settingsroutes.js';
564
+ import { createRunRoutes } from './runroutes.js';
565
+ import { createImportRoutes } from './importroutes.js';
302
566
  import { DIED_MTIME_MS } from '../runlist.js';
@@ -1,6 +1,9 @@
1
1
  /**
2
+ * `openFolderSpawn` / `platform` are test seams for the Open keys folder button (a real caller passes
3
+ * neither: `child_process.spawn` and `process.platform`).
2
4
  * @param {{ port: number, token: string, home?: string, env?: Record<string,string|undefined>,
3
- * fetchImpl?: typeof fetch, now?: () => number }} opts
5
+ * fetchImpl?: typeof fetch, now?: () => number, openFolderSpawn?: (cmd: string, args: string[], o: object) => any,
6
+ * platform?: string }} opts
4
7
  */
5
8
  export function createSettingsRoutes(opts: {
6
9
  port: number;
@@ -9,6 +12,8 @@ export function createSettingsRoutes(opts: {
9
12
  env?: Record<string, string | undefined>;
10
13
  fetchImpl?: typeof fetch;
11
14
  now?: () => number;
15
+ openFolderSpawn?: (cmd: string, args: string[], o: object) => any;
16
+ platform?: string;
12
17
  }): {
13
18
  handle: (req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse, pathname: string, body: any) => boolean;
14
19
  };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The status of one run.
3
+ * @param {{outcome: string|null|undefined, died?: boolean, category?: string|null, moneyHalt?: boolean}} o
4
+ * `category` is the last escalation's category; `moneyHalt` is whether the spine carries a `money-halt` record
5
+ * @returns {{key: StatusKey, sign: string, word: string}}
6
+ */
7
+ export function statusFor(o: {
8
+ outcome: string | null | undefined;
9
+ died?: boolean;
10
+ category?: string | null;
11
+ moneyHalt?: boolean;
12
+ }): {
13
+ key: StatusKey;
14
+ sign: string;
15
+ word: string;
16
+ };
17
+ /** the sentence a passed run carries as its reason (the Ended block's `line`) */
18
+ export const GOAL_MET_LINE: "goal met";
19
+ /** @typedef {'running'|'waiting'|'passed'|'failed'|'capped'|'stopped'|'died'} StatusKey */
20
+ /** the table: key -> sign + word */
21
+ export const STATUS: Readonly<{
22
+ running: Readonly<{
23
+ sign: "▶";
24
+ word: "running";
25
+ }>;
26
+ waiting: Readonly<{
27
+ sign: "·";
28
+ word: "waiting";
29
+ }>;
30
+ passed: Readonly<{
31
+ sign: "✓";
32
+ word: "passed";
33
+ }>;
34
+ failed: Readonly<{
35
+ sign: "✗";
36
+ word: "failed";
37
+ }>;
38
+ capped: Readonly<{
39
+ sign: "✗";
40
+ word: "capped";
41
+ }>;
42
+ stopped: Readonly<{
43
+ sign: "✗";
44
+ word: "stopped";
45
+ }>;
46
+ died: Readonly<{
47
+ sign: "?";
48
+ word: "died";
49
+ }>;
50
+ }>;
51
+ export type StatusKey = "running" | "waiting" | "passed" | "failed" | "capped" | "stopped" | "died";
@@ -77,25 +77,6 @@
77
77
  * @returns {string} the labelled block, or '' when there is nothing to show
78
78
  */
79
79
  export function closeGapBlock(gap: string | null | undefined): string;
80
- /**
81
- * BA-21 — the WRITE side of the pricing-provenance signal (read side: `rateProvenance` /
82
- * `spendProvenance` in src/ledger.js). bare-agent >=0.37 rides `rateSource` beside
83
- * `pricing` on every metering payload; both metering callbacks below forward it onto the
84
- * spine through THIS one helper, because two sites spelling one rule are two instruments
85
- * (the ripgrep fix that landed in ci.yml but not publish.yml).
86
- *
87
- * Forwarded VERBATIM — the write side reports what upstream said, and src/ledger.js is
88
- * the one place that decides what counts as a guess. And an ABSENT provenance stays
89
- * absent: a payload carrying no `rateSource` (bare-agent <0.37, and every round already
90
- * in the archive) emits no field at all, so a reader sees UNKNOWN rather than a label we
91
- * invented. Never defaulted to `null` here — `null` is upstream saying "nothing was
92
- * priced", which is a different fact from "nobody told us".
93
- * @param {any} arg an `onLlmResult` / `onTurn` payload
94
- * @returns {{rateSource?: string|null}} the field to spread into the spine record
95
- */
96
- export function rateSourceFields(arg: any): {
97
- rateSource?: string | null;
98
- };
99
80
  /**
100
81
  * What the NEXT attempt is told about the bound that cut the previous one.
101
82
  *
@@ -415,12 +396,18 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
415
396
  * passes `false` so the three `--resume`-naming readouts below (`MONEY_OPTIONS`,
416
397
  * the resume-plan-red option, the fix-loop terminal) say "resume is `run-u`-only
417
398
  * in v1" instead of naming a flag that would fail if typed.
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 a step worker
401
+ * (the `metered` callback, where the money cap binds): present = emit `stop-requested`, consume
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
404
+ * point, so the spend is exact. `null` = no stop surface (the default).
418
405
  * @returns {Promise<string>} 'green' | 'already-green' | 'escalated' | 'plan-red' |
419
406
  * 'check-red' | 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' |
420
- * 'branch-red' | 'cap-halt' | 'wall-halt' | 'provider-red' | 'interpreter-red' |
407
+ * 'branch-red' | 'cap-halt' | 'wall-halt' | 'stopped' | 'provider-red' | 'interpreter-red' |
421
408
  * 'step-stalled' | 'hitl-pause' | 'hitl-decision-red' | `step-red:<id>`
422
409
  */
423
- export function runPlan(job: any, { workdir, provider, nativeProvider, providerFor, judgeProvider, judgeModel, rates, judgeRates, emit, remainingUsd, isUnpriced, spendComplete, capRuns, strikeLimit, closeTimeoutMs, closeDir, maxStepRounds, layerRoot, readShim, scout, scoutRounds, bridge, now, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, priorSpentUsd, reviewDoor, doorRerun, resumable }: {
410
+ export function runPlan(job: any, { workdir, provider, nativeProvider, providerFor, judgeProvider, judgeModel, rates, judgeRates, emit, remainingUsd, isUnpriced, spendComplete, capRuns, strikeLimit, closeTimeoutMs, closeDir, maxStepRounds, layerRoot, readShim, scout, scoutRounds, bridge, now, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, priorSpentUsd, reviewDoor, doorRerun, resumable, stopFile }: {
424
411
  workdir: string;
425
412
  provider: any;
426
413
  nativeProvider?: ((o: {
@@ -483,7 +470,9 @@ export function runPlan(job: any, { workdir, provider, nativeProvider, providerF
483
470
  receivedAt?: string | null;
484
471
  } | null | undefined;
485
472
  resumable?: boolean | undefined;
473
+ stopFile?: string | null | undefined;
486
474
  }): Promise<string>;
475
+ export { rateSourceFields };
487
476
  /**
488
477
  * The runtime belt for F178 (`.git`) and F177's follow-up (`node_modules`) —
489
478
  * hamr's option B ruling (2026-09-14): block WRITING to either, never reading.
@@ -532,7 +521,10 @@ export const FORBIDDEN_WRITE_SEGMENT_PATTERN: RegExp;
532
521
  * shipped path routinely rides through.
533
522
  */
534
523
  export const BOUND_REASON_MAX: 200;
524
+ /** the recorded reason for a step attempt whose round was cut at the output cap (F122 2A b) */
525
+ export const TRUNCATED_REASON: "output cut at the token limit (max_tokens)";
535
526
  export type CategorizedError = Error & {
536
527
  category?: string;
537
528
  lib?: string;
538
529
  };
530
+ import { rateSourceFields } from './text.js';
@@ -56,6 +56,18 @@ export function defaultUrlOf(provider: string): string;
56
56
  * @returns {string}
57
57
  */
58
58
  export function endpointOf(provider: string, baseUrl: string | null | undefined): string;
59
+ /**
60
+ * A job's provider in plain words for a refusal line: the host its Base URL points at (else the
61
+ * shape's own default host, else the shape's label), plus the model when the spec names one.
62
+ * Never a key, a path or a status code.
63
+ * @param {{provider?: string, baseUrl?: string|null, model?: string|null}} spec
64
+ * @returns {string}
65
+ */
66
+ export function providerInWords(spec: {
67
+ provider?: string;
68
+ baseUrl?: string | null;
69
+ model?: string | null;
70
+ }): string;
59
71
  /**
60
72
  * The URL a row talks to, for display ('' = the vendor's own host, which has no URL to show).
61
73
  * @param {KeyRow} row
package/types/ralph.d.ts CHANGED
@@ -1,3 +1,22 @@
1
+ /**
2
+ * @param {string} cmd
3
+ * @param {string[]} args
4
+ * @param {{env: NodeJS.ProcessEnv, cwd?: string, timeoutMs?: number}} o
5
+ * @returns {Promise<{error: (Error & {code?: string})|null, status: number|null, signal: string|null, stdout: string, stderr: string}>}
6
+ */
7
+ export function spawnClose(cmd: string, args: string[], { env, cwd, timeoutMs }: {
8
+ env: NodeJS.ProcessEnv;
9
+ cwd?: string;
10
+ timeoutMs?: number;
11
+ }): Promise<{
12
+ error: (Error & {
13
+ code?: string;
14
+ }) | null;
15
+ status: number | null;
16
+ signal: string | null;
17
+ stdout: string;
18
+ stderr: string;
19
+ }>;
1
20
  /**
2
21
  * Run a close command; its exit code is the verdict (hard green, PRD §4).
3
22
  * `expect` → satisfied; any other exit → needs_revision (gap fed back).
package/types/replay.d.ts CHANGED
@@ -36,6 +36,18 @@ export function auditWindow(jobStart?: {
36
36
  startTs: number;
37
37
  endTs: number;
38
38
  };
39
+ /**
40
+ * One window PER LEG of a (possibly resumed) run (P5-R) — the ONE owner the panel and {@link replayRun} share. A
41
+ * run with no `leg-resume` marker is one leg and this is `[auditWindow(jobStart, jobEnd)]`, exactly as before. A
42
+ * resumed run's quiet time between two legs is nobody's, so a row another run wrote into a shared sidecar during it
43
+ * is outside every window; a leg that recorded no terminal (it died) ends where the next leg begins.
44
+ * @param {any[]} spineRecords
45
+ * @returns {{startTs: number, endTs: number}[]}
46
+ */
47
+ export function auditWindowsOf(spineRecords: any[]): {
48
+ startTs: number;
49
+ endTs: number;
50
+ }[];
39
51
  /**
40
52
  * Reconstruct one run's whole story from its spine + gate-audit. Pure: no
41
53
  * IO, no spine writes, no verdict. Malformed/non-object entries in either
@@ -109,6 +121,15 @@ export function replayRun(spineEvents: any[], auditEvents?: any[], { runId, audi
109
121
  skipping: any;
110
122
  divergence: any;
111
123
  } | null;
124
+ legs: {
125
+ leg: number;
126
+ after: string | null;
127
+ outcome: string | null;
128
+ startTs: any;
129
+ endTs: any;
130
+ startSeq: any;
131
+ endSeq: any;
132
+ }[];
112
133
  thisFileSpend: {
113
134
  value: any;
114
135
  source: "engagementSpentUsd" | "summed rounds";
@@ -178,9 +199,12 @@ export function replayRun(spineEvents: any[], auditEvents?: any[], { runId, audi
178
199
  label: string;
179
200
  error: string | null;
180
201
  } | null;
202
+ outcome: any;
203
+ stopReason?: string | null | undefined;
204
+ continued?: boolean | undefined;
205
+ tryNumber?: number | undefined;
181
206
  id: any;
182
207
  occurrence: any;
183
- outcome: any;
184
208
  }[];
185
209
  iterations: {
186
210
  windowStartSeq: any;
@@ -43,7 +43,7 @@ export function resolveSiblings(spinePath: string): {
43
43
  * hand both to {@link replayRun}. Read-only, $0, mints no verdict.
44
44
  * @param {string} spinePath absolute or cwd-relative path to a `.jsonl` spine
45
45
  * @param {{preParsedSpine?: {records: any[], skipped: number}, skipAudit?: boolean,
46
- * auditPathOverride?: string|null}} [opts]
46
+ * auditPathOverride?: string|string[]|null}} [opts]
47
47
  * `preParsedSpine`: avoids a second parse of the same file when the caller
48
48
  * ({@link listSpines}) already read it once to run {@link looksLikeSpine}.
49
49
  * `skipAudit` (PR #23 review item 5, 2026-08-26, carried over): a
@@ -68,7 +68,7 @@ export function replayOne(spinePath: string, opts?: {
68
68
  skipped: number;
69
69
  };
70
70
  skipAudit?: boolean;
71
- auditPathOverride?: string | null;
71
+ auditPathOverride?: string | string[] | null;
72
72
  }): ReturnType<typeof replayRun>;
73
73
  /**
74
74
  * List every spine found directly in `dir` (not recursive — same as the
package/types/reuse.d.ts CHANGED
@@ -605,6 +605,11 @@ export const REUSE_GRADED_RED: readonly string[];
605
605
  * allowances the loop's remaining tries are meant to spend against, and the human
606
606
  * ones are the stop no further try can change (see `hardStop`).
607
607
  *
608
+ * `stopped` (PANEL-BUILD.md P5 item 5, hamr 2026-10-02): the PERSON ended the leg at a round
609
+ * boundary (a `<spine>.stop` request). Nothing was judged and nothing failed — finished steps stand on
610
+ * disk, the allowance is unspent, so it is a checkpoint exactly like a cap-halt. Arbiter-adjacent (it
611
+ * widens what resume may re-enter); ruled by hamr, not chosen by a builder.
612
+ *
608
613
  * `provider-red` folded in 2026-08-25 (PRD v1.80 TODO #4, F115 "Ruled: one transport
609
614
  * retry"; hamr's ruling verbatim: *"joins resume anyways and you decide but you are
610
615
  * given an honest readout and with 1 retry"*). It is a MACHINE halt, not a human one
package/types/run.d.ts CHANGED
@@ -195,10 +195,13 @@
195
195
  * passes `false` so the escalation tail's option strings say "resume is
196
196
  * `run-u`-only in v1" instead of naming a flag that would fail if typed. Forwarded
197
197
  * verbatim to the plan flow, which owns the three `--resume`-naming readouts.
198
+ * @param {string|null} [opts.stopFile=null] P5 item 5 — the run's STOP REQUEST file
199
+ * (`stopFilePath(spine)`, src/legs.js), forwarded verbatim to the plan flow, which reads it at the
200
+ * round-boundary seam (where the money cap binds) and ends the leg `stopped`. `null` = no stop surface.
198
201
  * @returns {Promise<string>} outcome: 'green' | 'already-green' | 'escalated' |
199
202
  * 'unapproved-spec' | 'job-red' | 'smoke-red' | 'plan-red' | 'check-red' |
200
203
  * 'close-red' | 'close-unsupported' | 'recipe-stale' | 'branch-red' | 'pricing-red' | 'provider-red' |
201
- * 'interpreter-red' | 'cap-halt' | 'wall-halt' | 'step-stalled' |
204
+ * 'interpreter-red' | 'cap-halt' | 'wall-halt' | 'stopped' | 'step-stalled' |
202
205
  * 'hitl-pause' | 'hitl-decision-red' | `step-red:<id>`
203
206
  * NOTE: `job-end` can ALSO land with outcome 'runner-drained' (F140/PRD item
204
207
  * 28(c)) — the `beforeExit` backstop's own terminal, emitted from OUTSIDE this
@@ -209,7 +212,7 @@
209
212
  * deliberately NOT in the resumable/checkpoint set (`src/reuse.js`) — an
210
213
  * unknown in-flight state is not a known-safe resume point.
211
214
  */
212
- export function runJob(rawSpec: object | string, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider, judgeModel, rates, judgeRates, emit, capRuns, strikeLimit, shellCapUsd, closeTimeoutMs, closeDir, layerRoot, readShim, scout, bridge, draftSpentUsd, draftSpendComplete, priorSpentUsd, priorSpendComplete, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun, resumable }: {
215
+ export function runJob(rawSpec: object | string, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider, judgeModel, rates, judgeRates, emit, capRuns, strikeLimit, shellCapUsd, closeTimeoutMs, closeDir, layerRoot, readShim, scout, bridge, draftSpentUsd, draftSpendComplete, priorSpentUsd, priorSpendComplete, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun, resumable, stopFile }: {
213
216
  approvals: unknown;
214
217
  workdir: string;
215
218
  provider: any;
@@ -271,6 +274,7 @@ export function runJob(rawSpec: object | string, { approvals, workdir, provider,
271
274
  receivedAt?: string | null;
272
275
  } | null | undefined;
273
276
  resumable?: boolean | undefined;
277
+ stopFile?: string | null | undefined;
274
278
  }): Promise<string>;
275
279
  /** @typedef {{code: string, path: string, detail?: string}} Red */
276
280
  /**