bareloop 0.33.0 → 0.35.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 (56) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/bareloop.context.md +263 -27
  3. package/package.json +1 -1
  4. package/src/bundlerun.js +13 -4
  5. package/src/draftspend.js +82 -0
  6. package/src/exits.js +10 -1
  7. package/src/interviewrun.js +10 -9
  8. package/src/job.js +39 -2
  9. package/src/judged.js +25 -5
  10. package/src/kinds.js +2 -0
  11. package/src/ledger.js +104 -9
  12. package/src/legs.js +171 -0
  13. package/src/monthly.js +133 -87
  14. package/src/panel/authorroutes.js +95 -15
  15. package/src/panel/authorsession.js +408 -113
  16. package/src/panel/importroutes.js +359 -0
  17. package/src/panel/importrun.js +203 -0
  18. package/src/panel/index.html +1071 -184
  19. package/src/panel/runroutes.js +219 -0
  20. package/src/panel/server.js +855 -81
  21. package/src/panel/status.js +51 -0
  22. package/src/planrun.js +63 -31
  23. package/src/providerrows.js +21 -2
  24. package/src/providers.js +4 -4
  25. package/src/ralph.js +3 -0
  26. package/src/replay.js +139 -19
  27. package/src/replayio.js +11 -4
  28. package/src/reuse.js +20 -3
  29. package/src/run.js +6 -3
  30. package/src/runlist.js +90 -7
  31. package/src/text.js +20 -0
  32. package/src/u-readout.js +9 -1
  33. package/src/u-watchdog.mjs +4 -2
  34. package/src/userrun.js +166 -65
  35. package/types/draftspend.d.ts +53 -0
  36. package/types/job.d.ts +25 -0
  37. package/types/judged.d.ts +5 -2
  38. package/types/ledger.d.ts +44 -0
  39. package/types/legs.d.ts +103 -0
  40. package/types/monthly.d.ts +39 -22
  41. package/types/panel/authorroutes.d.ts +38 -1
  42. package/types/panel/authorsession.d.ts +109 -2
  43. package/types/panel/importroutes.d.ts +129 -0
  44. package/types/panel/importrun.d.ts +77 -0
  45. package/types/panel/runroutes.d.ts +32 -0
  46. package/types/panel/server.d.ts +255 -2
  47. package/types/panel/status.d.ts +51 -0
  48. package/types/planrun.d.ts +13 -21
  49. package/types/providerrows.d.ts +14 -1
  50. package/types/providers.d.ts +1 -2
  51. package/types/replay.d.ts +25 -1
  52. package/types/replayio.d.ts +2 -2
  53. package/types/reuse.d.ts +5 -0
  54. package/types/run.d.ts +6 -2
  55. package/types/runlist.d.ts +66 -2
  56. package/types/text.d.ts +19 -0
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The run routes' handler. `getResumeContext(runid)` is the server's own reader (the same
3
+ * `resumePlanFor` the Ended block uses, so the page never offers what this refuses):
4
+ * `null` = no such run, else `{row, plan}`.
5
+ * @param {{ port: number, token: string, env?: Record<string,string|undefined>, home?: string,
6
+ * spawnFn?: typeof realSpawn, bareloopBin?: string, settleMs?: number,
7
+ * getResumeContext: (runid: string) => ({row: any, plan: any}|null),
8
+ * getStopContext?: (runid: string) => ({row: any, live: boolean, spineExists: boolean}|null) }} opts
9
+ */
10
+ export function createRunRoutes(opts: {
11
+ port: number;
12
+ token: string;
13
+ env?: Record<string, string | undefined>;
14
+ home?: string;
15
+ spawnFn?: typeof realSpawn;
16
+ bareloopBin?: string;
17
+ settleMs?: number;
18
+ getResumeContext: (runid: string) => ({
19
+ row: any;
20
+ plan: any;
21
+ } | null);
22
+ getStopContext?: (runid: string) => ({
23
+ row: any;
24
+ live: boolean;
25
+ spineExists: boolean;
26
+ } | null);
27
+ }): {
28
+ handle: (req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse, pathname: string, body: any) => boolean;
29
+ };
30
+ /** how long a resume waits for an early engine exit before reporting "started" (ms) */
31
+ export const DEFAULT_SETTLE_MS: 6000;
32
+ import { spawn as realSpawn } from 'node:child_process';
@@ -50,6 +50,132 @@ 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}} [o]
123
+ * @returns {{reason: string, next: string, line: string, actions: {id: string, label: string}[]}|null}
124
+ */
125
+ export function endedFor(summary: {
126
+ outcome: string | null;
127
+ stopReason: string | null;
128
+ spentUsd: number | null;
129
+ budgetUsd: number | null;
130
+ lastEscalation?: any;
131
+ }, death: {
132
+ died: boolean;
133
+ lastThing: string | null;
134
+ }, o?: {
135
+ resume?: {
136
+ ok: boolean;
137
+ why?: string;
138
+ } | null;
139
+ destinationRefused?: string | null;
140
+ moneyHalt?: boolean;
141
+ }): {
142
+ reason: string;
143
+ next: string;
144
+ line: string;
145
+ actions: {
146
+ id: string;
147
+ label: string;
148
+ }[];
149
+ } | null;
150
+ /**
151
+ * The Resume route's reader (P5 item 2): the listed row plus the resume plan, from the
152
+ * ONE {@link resumePlanFor} the Ended block asks. `null` when the runid is not listed
153
+ * or its spine is gone.
154
+ * @param {string} runid
155
+ * @param {{ home?: string }} [opts]
156
+ * @returns {{row: any, plan: any}|null}
157
+ */
158
+ export function getResumeContext(runid: string, opts?: {
159
+ home?: string;
160
+ }): {
161
+ row: any;
162
+ plan: any;
163
+ } | null;
164
+ /**
165
+ * The Stop route's reader (P5 item 5): the listed row, whether its LATEST leg is live (`runIsAlive`: the row's pid
166
+ * is the latest leg's, folded from the run list's `leg-start`), and whether its spine exists yet. `null` when the
167
+ * runid is not listed.
168
+ * @param {string} runid
169
+ * @param {{ home?: string }} [opts]
170
+ * @returns {{row: any, live: boolean, spineExists: boolean}|null}
171
+ */
172
+ export function getStopContext(runid: string, opts?: {
173
+ home?: string;
174
+ }): {
175
+ row: any;
176
+ live: boolean;
177
+ spineExists: boolean;
178
+ } | null;
53
179
  /**
54
180
  * `GET /api/runs` — every listed run, NEWEST FIRST BY `at` (never by file/
55
181
  * append order — a backfill scan appends rows in sorted-PATH order, which
@@ -195,6 +321,127 @@ export function getRunRounds(runid: string, query?: {
195
321
  export function getRunJob(runid: string, opts?: {
196
322
  home?: string;
197
323
  }): any | null;
324
+ /**
325
+ * The track record of a WORKFLOW: every LISTED run whose signed spec (the one its latest leg ran under) has this
326
+ * `workflowKey` — the same goal, checks, guardrails, model and close, whatever source, destination or caps. One run =
327
+ * ONE entry (a resumed run is one run, its FINAL outcome — `legsOf`), and a run that is still live is neither green
328
+ * nor not-green yet. `avgSpendUsd` / `avgWallMs` average the FINISHED runs (a recorded terminal) whose figure is exact
329
+ * (chain spend priced; working time from readable stamps, resume gaps excluded) — `null` when no run qualifies, never 0.
330
+ * Read from the runs the panel already lists: no registry, no plan handover (hamr ruling A).
331
+ * `opts.worker` narrows the record to runs on the same WORKER (provider + baseUrl + model — Model is open on a reuse, so
332
+ * the key no longer separates models): `undefined` = every model, a worker = only runs whose signed spec ran that one,
333
+ * `null` = no model chosen, so no run matches.
334
+ * @param {string} key a {@link workflowKey}
335
+ * @param {{ home?: string, worker?: {provider: string, baseUrl?: string, model: string}|null }} [opts]
336
+ * @returns {{runs: number, green: number, notGreen: number, live: number, finished: number, avgSpendUsd: number|null, avgWallMs: number|null}}
337
+ */
338
+ export function trackRecordFor(key: string, opts?: {
339
+ home?: string;
340
+ worker?: {
341
+ provider: string;
342
+ baseUrl?: string;
343
+ model: string;
344
+ } | null;
345
+ }): {
346
+ runs: number;
347
+ green: number;
348
+ notGreen: number;
349
+ live: number;
350
+ finished: number;
351
+ avgSpendUsd: number | null;
352
+ avgWallMs: number | null;
353
+ };
354
+ /**
355
+ * The estimate line above a reuse card (code-owned fixed text): `Same job — G green · N not green · about $X and
356
+ * M min a run`, from the workflow's own listed runs. What is unknown is said, never rendered as $0 or 0 min.
357
+ * @param {{runs?: number, green: number, notGreen: number, finished?: number, avgSpendUsd: number|null, avgWallMs?: number|null}|null} record
358
+ * @returns {string}
359
+ */
360
+ export function reuseLine(record: {
361
+ runs?: number;
362
+ green: number;
363
+ notGreen: number;
364
+ finished?: number;
365
+ avgSpendUsd: number | null;
366
+ avgWallMs?: number | null;
367
+ } | null): string;
368
+ /**
369
+ * `start-from` (the route keeps its name) for one listed run: the card prefilled from the run's SIGNED job, which
370
+ * boxes are locked and which are open, and the track record. Locked boxes carry the signed values, read by the
371
+ * SAME readers the Job tab uses (`successFromSpec`/`guardrailsFromSpec`); the open four — Source, Destination, the
372
+ * $ cap and the Time cap — start at what the run had. `ok:false` when the run's signed job is not on disk (nothing
373
+ * to copy: change the job: Clear the card). `null` when the runid is not listed.
374
+ * @param {string} runid
375
+ * @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
376
+ * @returns {{ok: true, origin: {runid: string, job: string}, card: Record<string, any>, from: 'signed job',
377
+ * locked: readonly string[], open: readonly string[], note: string|null,
378
+ * specHash: string, workflowKey: string, spec: any, specPath: string,
379
+ * trackRecord: ReturnType<typeof trackRecordFor>, line: string}|{ok: false, error: string}|null}
380
+ */
381
+ export function getStartFrom(runid: string, opts?: {
382
+ home?: string;
383
+ model?: string;
384
+ }): {
385
+ ok: true;
386
+ origin: {
387
+ runid: string;
388
+ job: string;
389
+ };
390
+ card: Record<string, any>;
391
+ from: "signed job";
392
+ locked: readonly string[];
393
+ open: readonly string[];
394
+ note: string | null;
395
+ specHash: string;
396
+ workflowKey: string;
397
+ spec: any;
398
+ specPath: string;
399
+ trackRecord: ReturnType<typeof trackRecordFor>;
400
+ line: string;
401
+ } | {
402
+ ok: false;
403
+ error: string;
404
+ } | null;
405
+ /**
406
+ * Reuse workflow, for an IMPORTED job: the bundle is read AGAIN now and must be clean (`readBundle` — its own hash
407
+ * covers every close script, so a swapped script is `bundle-tampered` here), must resolve its own `bareloop`
408
+ * dependency (`checkBundleDeps`), and every close stage's signed sha256 must match the script's bytes on disk
409
+ * (`checkCloseByteSignature`, over the spec with `$BARELOOP_BUNDLE` resolved to this folder — the same in-memory
410
+ * substitution `bareloop run <bundle>` makes). The reuse spec is that resolved spec; the close scripts stay where
411
+ * they were just verified (the engine re-verifies their bytes at run start and before every close run). The person
412
+ * signs it on this machine at [Sign & run]. Same card, same locked/open boxes as a run's reuse. `null` when no such
413
+ * imported job.
414
+ * @param {string} id
415
+ * @param {{ home?: string, model?: string }} [opts] `model` as for {@link getStartFrom}
416
+ * @returns {{ok: true, origin: {runid: null, job: string, importId: string}, card: Record<string, any>, from: 'imported job',
417
+ * locked: readonly string[], open: readonly string[], note: string|null, specHash: string, workflowKey: string, spec: any,
418
+ * specPath: null, trackRecord: ReturnType<typeof trackRecordFor>, line: string}|{ok: false, error: string}|null}
419
+ */
420
+ export function getStartFromImport(id: string, opts?: {
421
+ home?: string;
422
+ model?: string;
423
+ }): {
424
+ ok: true;
425
+ origin: {
426
+ runid: null;
427
+ job: string;
428
+ importId: string;
429
+ };
430
+ card: Record<string, any>;
431
+ from: "imported job";
432
+ locked: readonly string[];
433
+ open: readonly string[];
434
+ note: string | null;
435
+ specHash: string;
436
+ workflowKey: string;
437
+ spec: any;
438
+ specPath: null;
439
+ trackRecord: ReturnType<typeof trackRecordFor>;
440
+ line: string;
441
+ } | {
442
+ ok: false;
443
+ error: string;
444
+ } | null;
198
445
  /**
199
446
  * Handle one request against the read-only API + the page. Exported
200
447
  * separately from {@link createPanelServer} so tests can drive it without a
@@ -211,7 +458,7 @@ export function getRunJob(runid: string, opts?: {
211
458
  * only `createPanelServer` below always supplies one.
212
459
  * @param {import('node:http').IncomingMessage} req
213
460
  * @param {import('node:http').ServerResponse} res
214
- * @param {{ home?: string, port: number, token?: string, authorRoutes?: ReturnType<typeof createAuthorRoutes>, settingsRoutes?: ReturnType<typeof createSettingsRoutes> }} opts
461
+ * @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
462
  */
216
463
  export function handleRequest(req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse, opts: {
217
464
  home?: string;
@@ -219,6 +466,8 @@ export function handleRequest(req: import("node:http").IncomingMessage, res: imp
219
466
  token?: string;
220
467
  authorRoutes?: ReturnType<typeof createAuthorRoutes>;
221
468
  settingsRoutes?: ReturnType<typeof createSettingsRoutes>;
469
+ runRoutes?: ReturnType<typeof createRunRoutes>;
470
+ importRoutes?: ReturnType<typeof createImportRoutes>;
222
471
  }): void;
223
472
  /**
224
473
  * Start the panel server. Binds `127.0.0.1` ONLY. A taken port is a LOUD,
@@ -238,7 +487,7 @@ export function handleRequest(req: import("node:http").IncomingMessage, res: imp
238
487
  * this package's own `bin/bareloop.mjs`).
239
488
  * @param {{ port?: number, home?: string, env?: Record<string,string|undefined>,
240
489
  * sessionsRoot?: string, spawnFn?: (...a: any[]) => any, bareloopBin?: string,
241
- * fetchImpl?: typeof fetch }} [opts]
490
+ * fetchImpl?: typeof fetch, settleMs?: number, userHome?: string }} [opts]
242
491
  * @returns {Promise<{ server: import('node:http').Server, port: number, token: string, close: () => Promise<void> }>}
243
492
  */
244
493
  export function createPanelServer(opts?: {
@@ -249,6 +498,8 @@ export function createPanelServer(opts?: {
249
498
  spawnFn?: (...a: any[]) => any;
250
499
  bareloopBin?: string;
251
500
  fetchImpl?: typeof fetch;
501
+ settleMs?: number;
502
+ userHome?: string;
252
503
  }): Promise<{
253
504
  server: import("node:http").Server;
254
505
  port: number;
@@ -299,4 +550,6 @@ export const ROUNDS_PAGE_DEFAULT: 50;
299
550
  export const ROUNDS_PAGE_MAX: 200;
300
551
  import { createAuthorRoutes } from './authorroutes.js';
301
552
  import { createSettingsRoutes } from './settingsroutes.js';
553
+ import { createRunRoutes } from './runroutes.js';
554
+ import { createImportRoutes } from './importroutes.js';
302
555
  import { DIED_MTIME_MS } from '../runlist.js';
@@ -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
@@ -127,7 +139,8 @@ export function keyNameFor(provider: string | null | undefined, baseUrl: string
127
139
  export function applyConfiguredKey(env: Record<string, string | undefined>, provider: string | null | undefined, baseUrl: string | null | undefined, rows: readonly KeyRow[], model?: string | null | undefined): Record<string, string | undefined>;
128
140
  /**
129
141
  * The one readout line for a resolved customer price, preview and run tail alike:
130
- * `yours: in $0.006 / out $1.20 per 1M tokens (DEEPSEEK_API_KEY row)`.
142
+ * `yours: in $0.006 / out $1.20 per 1M tokens (DEEPSEEK_API_KEY row)`. A 0 / 0 price is accepted (the price
143
+ * is the customer's) but the line says so: every round then books $0, so spend and limits read $0.
131
144
  * @param {{ inPerM: number, outPerM: number, envName: string }} price a {@link ratesFor} result
132
145
  * @returns {string}
133
146
  */
@@ -52,8 +52,7 @@ export function makeProvider(providerName: string, { apiKey, model, baseUrl }?:
52
52
  }): any;
53
53
  /**
54
54
  * Build the `{provider, providerFor, judgeProvider}` triple both runners
55
- * need (`src/cli.js`'s `buildProviders`, `scripts/run-u.mjs`'s top-level
56
- * wiring) — ONE seam instead of two copies of the same construction and
55
+ * need (`src/authorrun.js` and `src/panel/authorsession.js` call it) — ONE seam instead of two copies of the same construction and
57
56
  * tier-memoization logic. `model`/`tierModels` are already-RESOLVED inputs
58
57
  * (the caller runs `resolveWorkerModel` itself, exactly as both runners did
59
58
  * before this factory existed — that decision stays where each runner's own
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
  /**