bareloop 0.42.0 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/bareloop.context.md +103 -41
  3. package/package.json +2 -2
  4. package/src/authorflow.js +26 -11
  5. package/src/authoring.js +1 -1
  6. package/src/authorjob.js +33 -15
  7. package/src/authorreadout.js +7 -6
  8. package/src/declaredclose.js +6 -3
  9. package/src/draftspend.js +20 -2
  10. package/src/facts.js +54 -9
  11. package/src/factwords.js +58 -6
  12. package/src/inputlook.js +38 -3
  13. package/src/interviewrun.js +0 -1
  14. package/src/job.js +39 -12
  15. package/src/kinds.js +5 -0
  16. package/src/pagedropwords.js +64 -0
  17. package/src/pagemap.js +264 -0
  18. package/src/pagerules.js +319 -0
  19. package/src/panel/authorroutes.js +138 -60
  20. package/src/panel/authorsession.js +88 -32
  21. package/src/panel/index.html +1251 -522
  22. package/src/panel/jobplan.js +4 -2
  23. package/src/panel/runroutes.js +188 -6
  24. package/src/panel/scheduler.js +156 -0
  25. package/src/panel/schedules.js +667 -0
  26. package/src/panel/schedulestart.js +243 -0
  27. package/src/panel/server.js +1234 -68
  28. package/src/panel/status.js +33 -0
  29. package/src/panelmoney.js +29 -0
  30. package/src/planrun.js +92 -11
  31. package/src/promptregisters.js +4 -0
  32. package/src/providerrows.js +3 -3
  33. package/src/replay.js +118 -7
  34. package/src/reviewdoor.js +37 -0
  35. package/src/rubricstep.js +123 -11
  36. package/src/runlist.js +37 -1
  37. package/src/schedulewords.js +299 -0
  38. package/src/source.js +18 -0
  39. package/src/thinking.js +63 -2
  40. package/src/trigger.js +48 -0
  41. package/src/triggerclock.js +127 -0
  42. package/src/triggerfiles.js +215 -0
  43. package/src/triggerledger.js +79 -0
  44. package/src/userrun.js +34 -21
  45. package/types/authorflow.d.ts +42 -6
  46. package/types/authorjob.d.ts +10 -2
  47. package/types/declaredclose.d.ts +3 -1
  48. package/types/draftspend.d.ts +27 -1
  49. package/types/facts.d.ts +15 -0
  50. package/types/factwords.d.ts +26 -3
  51. package/types/inputlook.d.ts +15 -0
  52. package/types/job.d.ts +8 -1
  53. package/types/kinds.d.ts +5 -0
  54. package/types/pagedropwords.d.ts +26 -0
  55. package/types/pagemap.d.ts +154 -0
  56. package/types/pagerules.d.ts +86 -0
  57. package/types/panel/authorroutes.d.ts +50 -14
  58. package/types/panel/authorsession.d.ts +37 -12
  59. package/types/panel/runroutes.d.ts +32 -1
  60. package/types/panel/scheduler.d.ts +17 -0
  61. package/types/panel/schedules.d.ts +412 -0
  62. package/types/panel/schedulestart.d.ts +106 -0
  63. package/types/panel/server.d.ts +297 -15
  64. package/types/panel/status.d.ts +51 -0
  65. package/types/panelmoney.d.ts +5 -0
  66. package/types/planrun.d.ts +2 -0
  67. package/types/providerrows.d.ts +2 -2
  68. package/types/replay.d.ts +21 -1
  69. package/types/reviewdoor.d.ts +27 -0
  70. package/types/rubricstep.d.ts +57 -1
  71. package/types/runlist.d.ts +24 -1
  72. package/types/schedulewords.d.ts +137 -0
  73. package/types/source.d.ts +11 -0
  74. package/types/thinking.d.ts +39 -0
  75. package/types/trigger.d.ts +37 -0
  76. package/types/triggerclock.d.ts +52 -0
  77. package/types/triggerfiles.d.ts +93 -0
  78. package/types/triggerledger.d.ts +35 -0
  79. package/types/userrun.d.ts +4 -0
@@ -116,7 +116,7 @@ export function legDividersFor(spineRecords: any[], parts: any[]): {
116
116
  *
117
117
  * The table (outcome → reason / next / buttons) is PANEL-BUILD.md P5 item 1. The
118
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.
119
+ * (replaces P5 item 3, 2026-10-03; one door on every ending since 2026-10-15) starts a NEW run.
120
120
  * @param {{outcome: string|null, stopReason: string|null, spentUsd: number|null, budgetUsd: number|null, lastEscalation?: any}} summary
121
121
  * @param {{died: boolean, lastThing: string|null}} death
122
122
  * @param {{resume?: {ok: boolean, why?: string}|null, destinationRefused?: string|null, moneyHalt?: boolean,
@@ -155,6 +155,28 @@ export function endedFor(summary: {
155
155
  label: string;
156
156
  }[];
157
157
  } | null;
158
+ /**
159
+ * RULED 2026-10-15 (Remove from Needs you): the ONE place a run's Needs-you state is decided. `Accept` (a live door) is never removable and never ages out.
160
+ * A `Resume` tag leaves when the person removed it AFTER (or at) the stop it is for — a removal covers only the ending it was made on, so a later stop
161
+ * brings the run back — or after `needsYouDays`. `removedMs` is the newest removal that covers the current ending (or null), `expired` is true when only
162
+ * the age rule took a Resume run out.
163
+ * @param {{live: boolean, records: any[], resume: {ok?: boolean}|null|undefined, stoppedAt: string|null, nowMs: number, removedMs?: number}} f
164
+ * @returns {{needs: 'Accept'|'Resume'|null, removedMs: number|null, expired: boolean}}
165
+ */
166
+ export function needsStateFor(f: {
167
+ live: boolean;
168
+ records: any[];
169
+ resume: {
170
+ ok?: boolean;
171
+ } | null | undefined;
172
+ stoppedAt: string | null;
173
+ nowMs: number;
174
+ removedMs?: number;
175
+ }): {
176
+ needs: "Accept" | "Resume" | null;
177
+ removedMs: number | null;
178
+ expired: boolean;
179
+ };
158
180
  /**
159
181
  * The Resume route's reader (P5 item 2): the listed row plus the resume plan, from the
160
182
  * ONE {@link resumePlanFor} the Ended block asks. `null` when the runid is not listed
@@ -184,6 +206,43 @@ export function getStopContext(runid: string, opts?: {
184
206
  live: boolean;
185
207
  spineExists: boolean;
186
208
  } | null;
209
+ /**
210
+ * The Remove-from-Needs-you route's reader (RULED 2026-10-15): the listed row and the Needs-you tag the runs list shows for it right now
211
+ * (`summarizeRow`, the one rule). `null` when the runid is not listed.
212
+ * @param {string} runid
213
+ * @param {{ home?: string, now?: number }} [opts]
214
+ * @returns {{row: any, needs: 'Accept'|'Resume'|null}|null}
215
+ */
216
+ export function getNeedsContext(runid: string, opts?: {
217
+ home?: string;
218
+ now?: number;
219
+ }): {
220
+ row: any;
221
+ needs: "Accept" | "Resume" | null;
222
+ } | null;
223
+ /**
224
+ * The ONE rule for a run's Needs-you tag (P9 v2 E): `Resume` when the engine would accept a resume (the SAME `resumePlanFor` answer the Run
225
+ * tab's Resume card shows, already asked once per row by `endedForRow`), until the run has been stopped `needsYouDays` (exactly that old is out;
226
+ * no readable stop time stays in). `Accept` comes from {@link doorWaiting} (T Part 8), asked in `summarizeRow`, never here.
227
+ * @param {{ok?: boolean}|null|undefined} resume @param {string|null} stoppedAt @param {number} nowMs
228
+ * @returns {'Accept'|'Resume'|null}
229
+ */
230
+ export function needsTag(resume: {
231
+ ok?: boolean;
232
+ } | null | undefined, stoppedAt: string | null, nowMs: number): "Accept" | "Resume" | null;
233
+ /**
234
+ * One entry per job, newest activity first: `headRunid` is the job's newest run BY TIME (never "the first row in server order"), `activityMs` the
235
+ * newest KNOWN time over its runs (`stoppedAt`, else `at`), `quiet` only when that time is known and older than the quiet limit. A job with a live
236
+ * run is never quiet; an unknown age is never guessed into quiet.
237
+ * @param {any[]} rows `listRuns` rows @param {number} nowMs
238
+ * @returns {{job: string, headRunid: string|null, quiet: boolean, activityMs: number|null}[]}
239
+ */
240
+ export function summarizeJobs(rows: any[], nowMs: number): {
241
+ job: string;
242
+ headRunid: string | null;
243
+ quiet: boolean;
244
+ activityMs: number | null;
245
+ }[];
187
246
  /**
188
247
  * `GET /api/runs` — every listed run, NEWEST FIRST BY `at` (never by file/
189
248
  * append order — a backfill scan appends rows in sorted-PATH order, which
@@ -197,12 +256,116 @@ export function getStopContext(runid: string, opts?: {
197
256
  * SAME payload by job client-side (`groupRunsByJob` in `index.html`) rather
198
257
  * than a second server-side endpoint, so the glyph/checkType mapping is
199
258
  * still computed in exactly one place.
200
- * @param {{ home?: string }} [opts]
259
+ * @param {{ home?: string, now?: number }} [opts] `now` (ms) is injectable for the Needs-you age rule
201
260
  * @returns {any[]}
202
261
  */
203
262
  export function listRuns(opts?: {
204
263
  home?: string;
264
+ now?: number;
205
265
  }): any[];
266
+ /**
267
+ * `GET /api/runs` (P9 v2 F): `{runs, jobs, limits}`. The server decides every state the left list draws — `live`, `needs`, each job's head run and
268
+ * quiet flag, the limits — and the page works out nothing. `scheduled` is the triggers piece (PRD v1.99, T Part 7).
269
+ * @param {{ home?: string, now?: number, schedules?: ReturnType<typeof createScheduleDoor> }} [opts]
270
+ */
271
+ export function runsListPayload(opts?: {
272
+ home?: string;
273
+ now?: number;
274
+ schedules?: ReturnType<typeof createScheduleDoor>;
275
+ }): {
276
+ runs: any[];
277
+ jobs: {
278
+ job: string;
279
+ headRunid: string | null;
280
+ quiet: boolean;
281
+ activityMs: number | null;
282
+ }[];
283
+ scheduled: any[];
284
+ gaveUp: any[];
285
+ limits: Readonly<{
286
+ quietDays: 30;
287
+ olderRuns: 7;
288
+ needsYouDays: 7;
289
+ }>;
290
+ };
291
+ /**
292
+ * T Part 8 — what the Accept route needs: the run's row, whether its runner is alive, whether it waits at its review door
293
+ * ({@link doorWaiting}), and the signed spec it was judged by (the file and hash `run-u --door --approve` takes).
294
+ * @param {string} runid @param {{ home?: string }} opts
295
+ * @returns {{row: any, live: boolean, waiting: boolean, specPath: string|null, specHash: string|null}|null}
296
+ */
297
+ export function getAcceptContext(runid: string, opts?: {
298
+ home?: string;
299
+ }): {
300
+ row: any;
301
+ live: boolean;
302
+ waiting: boolean;
303
+ specPath: string | null;
304
+ specHash: string | null;
305
+ } | null;
306
+ /**
307
+ * The files a run delivered: its own `destination-written` spine records (userrun.js, on a minted green only), one per path, the last record of a
308
+ * path winning. This is the ONLY source of a delivered path anywhere in the panel — no request ever names one.
309
+ * @param {any[]} records
310
+ * @returns {{path: string, bytes: number|null, sha256: string|null}[]}
311
+ */
312
+ export function deliveredOf(records: any[]): {
313
+ path: string;
314
+ bytes: number | null;
315
+ sha256: string | null;
316
+ }[];
317
+ /**
318
+ * `GET /api/runs/:runid/result` — the Result tab: for each rubric job line, its words and `~` rules from the signed spec, the facts it requires
319
+ * (worded by {@link factSentence}), and — read from the run's own delivered step file, sha256-checked against its `destination-written`
320
+ * record — one row per result with each fact's cell. `passed` is read from the run's own `check-run` record; the tab never re-decides. A red run
321
+ * delivers nothing, so it has no rows: it carries the check's own first gap instead. `null` = no such run or no result tab for it.
322
+ * @param {string} runid @param {{ home?: string }} opts
323
+ */
324
+ export function getRunResult(runid: string, opts?: {
325
+ home?: string;
326
+ sessionsRoot?: string;
327
+ }): {
328
+ runid: string;
329
+ lines: any[];
330
+ pages: string[];
331
+ pageNote: string | null;
332
+ notChecked: string[];
333
+ door: {
334
+ head: "waiting for you";
335
+ ask: string;
336
+ };
337
+ delivered: {
338
+ i: number;
339
+ path: string;
340
+ name: string;
341
+ bytes: number | null;
342
+ state: "ok" | "changed" | "gone";
343
+ words: string | null;
344
+ }[];
345
+ gap: {
346
+ line: string;
347
+ more: number;
348
+ moreText: string | null;
349
+ } | null;
350
+ } | null;
351
+ /**
352
+ * `GET /api/runs/:runid/download?i=<n>` — delivered file `n` (an index into the run's own `destination-written` list), or why not. The path is
353
+ * the record's, never the request's; the sha256 must still match the record.
354
+ * @param {string} runid @param {unknown} index @param {{ home?: string }} opts
355
+ * @returns {{ok: true, name: string, type: string, buf: Buffer}|{ok: false, code: number, error: string}}
356
+ */
357
+ export function getRunDownload(runid: string, index: unknown, opts?: {
358
+ home?: string;
359
+ }): {
360
+ ok: true;
361
+ name: string;
362
+ type: string;
363
+ buf: Buffer;
364
+ } | {
365
+ ok: false;
366
+ code: number;
367
+ error: string;
368
+ };
206
369
  /**
207
370
  * `GET /api/runs/:runid` — the full replay for the Run tab: steps (or
208
371
  * iterations), counters, summary-box fields. Looks `runid` up in the run
@@ -210,11 +373,12 @@ export function listRuns(opts?: {
210
373
  * header); `null` when the runid is not in the list at all (caller renders
211
374
  * 404).
212
375
  * @param {string} runid
213
- * @param {{ home?: string }} [opts]
376
+ * @param {{ home?: string, schedules?: ReturnType<typeof createScheduleDoor> }} [opts]
214
377
  * @returns {any|null}
215
378
  */
216
379
  export function getRunDetail(runid: string, opts?: {
217
380
  home?: string;
381
+ schedules?: ReturnType<typeof createScheduleDoor>;
218
382
  }): any | null;
219
383
  /**
220
384
  * `GET /api/runs/:runid/audit` — the Audit tab's rows, off the run's own
@@ -286,11 +450,12 @@ export function getRunAudit(runid: string, opts?: {
286
450
  * resolve to a real window, or the spine file is missing — the caller
287
451
  * renders 404, same posture as {@link getRunAudit}/{@link getRunJob}.
288
452
  * @param {string} runid
289
- * @param {{ part?: number, attempt?: number, offset?: number, limit?: number }} query
453
+ * @param {{ all?: boolean, part?: number, attempt?: number, offset?: number, limit?: number }} query
290
454
  * @param {{ home?: string }} [opts]
291
455
  * @returns {any|null}
292
456
  */
293
457
  export function getRunRounds(runid: string, query?: {
458
+ all?: boolean;
294
459
  part?: number;
295
460
  attempt?: number;
296
461
  offset?: number;
@@ -298,6 +463,78 @@ export function getRunRounds(runid: string, query?: {
298
463
  }, opts?: {
299
464
  home?: string;
300
465
  }): any | null;
466
+ /** the Round table's check row: the sign and the plain words, ONE place (PRD v1.106 Part 5 C) @param {number} passed @param {number} failed */
467
+ export function checkRowWords(passed: number, failed: number): {
468
+ sign: string;
469
+ text: string;
470
+ };
471
+ /**
472
+ * "71 model · 91 tool · 0 denied" — the Run summary's `calls:` row (PRD v1.106 Part 5 F). Model calls are the run's own spend records
473
+ * (`worker-round`, `judge-round`; the attempt-level `worker-result` echoes are not rounds and drafting is not the run's), tool and denied come
474
+ * from the gate-audit rows scoped to this run (the same figure `tools:` shows). No tool log saved says so, never a zero.
475
+ * @param {any[]} records the run's raw spine @param {{totalCalls: number, denied: number}|null} behaviour
476
+ * @returns {string}
477
+ */
478
+ export function callsLineOf(records: any[], behaviour: {
479
+ totalCalls: number;
480
+ denied: number;
481
+ } | null): string;
482
+ /**
483
+ * "16 run · 8 passed · 8 failed (write-flight-facts tries 1-5, then tries 1-3)" — counts of what ran, never a fraction (the "checks N/M"
484
+ * headline stays withdrawn). Every `exit-eval` result plus every graded `outer-close` stage; the parenthesis names the steps and try ranges
485
+ * that failed, from the same `tries[]` the Audit reads. A run with neither says `not recorded`.
486
+ * @param {any[]} records the run's raw spine @param {any[]|null|undefined} parts the enriched parts
487
+ * @returns {string}
488
+ */
489
+ export function checksLineOf(records: any[], parts: any[] | null | undefined): string;
490
+ /**
491
+ * The step card's extra line (PRD v1.106 Part 5 G): "5 tries · last try: 1 check passed, 1 failed", with the cut-off try named when there is one
492
+ * ("4 tries (the 4th cut off before its checks ran) · last try: no checks ran"). Words for what passed and failed, never a fraction. `null`
493
+ * when the step has no graded try (an old spine).
494
+ * @param {Array<{n: number, cutOff: boolean, passed: number, failed: number, ran: number}>} tries
495
+ * @returns {string|null}
496
+ */
497
+ export function triesLineOf(tries: Array<{
498
+ n: number;
499
+ cutOff: boolean;
500
+ passed: number;
501
+ failed: number;
502
+ ran: number;
503
+ }>): string | null;
504
+ /**
505
+ * "tools: read 16 · grep 22 · write 5 · denied 0" — the step's tool calls by kind, then how many the gate denied (PRD v1.106 Part 5 C). The
506
+ * `shell_`/`ctx_` prefixes are stripped like the rest of the page. `null` when no tool log was saved (nothing honest to count).
507
+ * @param {Record<string, number>|null|undefined} byTool @param {number|null|undefined} blocked
508
+ * @returns {string|null}
509
+ */
510
+ export function toolsTallyLine(byTool: Record<string, number> | null | undefined, blocked: number | null | undefined): string | null;
511
+ /**
512
+ * The try line's gap: the first failing reason, and how many more there are (PRD v1.106 Part 5 C). ONE function, beside {@link stageQuestionText}.
513
+ * @param {string|null|undefined} detail
514
+ * @returns {{ line: string, more: number }|null}
515
+ */
516
+ export function gapOneLine(detail: string | null | undefined): {
517
+ line: string;
518
+ more: number;
519
+ } | null;
520
+ /**
521
+ * "from line 1 ~ under $450; not a red-eye" — the job line(s) a check serves and the `~` rules written under them (the signed `jobLines`).
522
+ * `null` when the stage serves no job line (a mandatory guard).
523
+ * @param {number[]} fromLine @param {any} spec
524
+ * @returns {string|null}
525
+ */
526
+ export function fromLineText(fromLine: number[], spec: any): string | null;
527
+ /**
528
+ * "output judged: output/steps/1.json · 2.1 KB · sha256 674b…30" — the file a try's check read (PRD v1.106 Part 5 choice 14). ONE place owns
529
+ * these words. `size` is bytes under 1 KB, else KB with one decimal; the hash shows its first 4 and last 2 characters.
530
+ * @param {{path: string|null, bytes: number|null, sha256: string|null}} o
531
+ * @returns {string}
532
+ */
533
+ export function judgedFileLine(o: {
534
+ path: string | null;
535
+ bytes: number | null;
536
+ sha256: string | null;
537
+ }): string;
301
538
  /**
302
539
  * The Signed row of the Job tab: `YYYY-MM-DD HH:MM · <first 6 of specHash>`, the time being the run's FIRST `job-start`
303
540
  * in local time (hamr, P7). `null` when either part is not on record — never a made-up stamp.
@@ -343,11 +580,12 @@ export function inputsTextFromSpec(spec: any, source: string): string;
343
580
  * from the resolved SPEC only (a) (b) (c) — a job-start record and `none()`
344
581
  * carry no close/writeScope/tools at all, so those stay `'not recorded'`.
345
582
  * @param {string} runid
346
- * @param {{ home?: string }} [opts]
583
+ * @param {{ home?: string, schedules?: ReturnType<typeof createScheduleDoor> }} [opts]
347
584
  * @returns {any|null}
348
585
  */
349
586
  export function getRunJob(runid: string, opts?: {
350
587
  home?: string;
588
+ schedules?: ReturnType<typeof createScheduleDoor>;
351
589
  }): any | null;
352
590
  /**
353
591
  * The track record of a WORKFLOW: every LISTED run whose signed spec (the one its latest leg ran under) has this
@@ -395,14 +633,14 @@ export function reuseLine(record: {
395
633
  } | null): string;
396
634
  /**
397
635
  * `start-from` (the route keeps its name) for one listed run: the card prefilled from the run's SIGNED job, which
398
- * boxes are locked and which are open, and the track record. Locked boxes carry the signed values, read by the
636
+ * the track record (v1.101: nothing is locked, every box is open). The job boxes carry the signed values, read by the
399
637
  * SAME readers the Job tab uses (`successFromSpec`/`guardrailsFromSpec`); the open four — Source, Destination, the
400
638
  * $ cap and the Time cap — start at what the run had. `ok:false` when the run's signed job is not on disk (nothing
401
639
  * to copy: change the job: Clear the card). `null` when the runid is not listed.
402
640
  * @param {string} runid
403
641
  * @param {{ home?: string, model?: string, provider?: string }} [opts] `model` = the Model box's current Name (the estimate counts only runs on that worker); absent = the card's own Model
404
642
  * @returns {{ok: true, origin: {runid: string, job: string}, outcome: string|null, card: Record<string, any>, from: 'signed job',
405
- * locked: readonly string[], open: readonly string[], note: string|null,
643
+ * note: string|null,
406
644
  * specHash: string, workflowKey: string, spec: any, specPath: string, specNamesModel: boolean,
407
645
  * specWorker: {provider: string, endpoint: string, label: string},
408
646
  * trackRecord: ReturnType<typeof trackRecordFor>, line: string}|{ok: false, error: string}|null}
@@ -420,8 +658,6 @@ export function getStartFrom(runid: string, opts?: {
420
658
  outcome: string | null;
421
659
  card: Record<string, any>;
422
660
  from: "signed job";
423
- locked: readonly string[];
424
- open: readonly string[];
425
661
  note: string | null;
426
662
  specHash: string;
427
663
  workflowKey: string;
@@ -439,6 +675,21 @@ export function getStartFrom(runid: string, opts?: {
439
675
  ok: false;
440
676
  error: string;
441
677
  } | null;
678
+ /**
679
+ * T Part 3: a job's listed runs, newest first, each with its signed spec (null when not on disk), its terminal outcome and
680
+ * its parsed spine. The schedule door's gate (`greenFor`) reads this; nothing here builds a run context.
681
+ * @param {string} job
682
+ * @param {{ home?: string }} [opts]
683
+ * @returns {{runid: string, outcome: string|null, spec: any, records: any[]}[]}
684
+ */
685
+ export function jobRunsOf(job: string, opts?: {
686
+ home?: string;
687
+ }): {
688
+ runid: string;
689
+ outcome: string | null;
690
+ spec: any;
691
+ records: any[];
692
+ }[];
442
693
  /**
443
694
  * The Reuse workflow picker's list: one entry per JOB (same-job identity = `workflowKey`, the one `getStartFrom` uses)
444
695
  * that has at least one GREEN local run, newest green first. `runid` = that job's newest green run, so the page hands
@@ -469,7 +720,7 @@ export function listReuseJobs(opts?: {
469
720
  * @param {string} id
470
721
  * @param {{ home?: string, model?: string, provider?: string }} [opts] `model` as for {@link getStartFrom}
471
722
  * @returns {{ok: true, origin: {runid: null, job: string, importId: string}, card: Record<string, any>, from: 'imported job',
472
- * locked: readonly string[], open: readonly string[], note: string|null, specHash: string, workflowKey: string, spec: any,
723
+ * note: string|null, specHash: string, workflowKey: string, spec: any,
473
724
  * specPath: null, trackRecord: ReturnType<typeof trackRecordFor>, line: string}|{ok: false, error: string}|null}
474
725
  */
475
726
  export function getStartFromImport(id: string, opts?: {
@@ -485,8 +736,6 @@ export function getStartFromImport(id: string, opts?: {
485
736
  };
486
737
  card: Record<string, any>;
487
738
  from: "imported job";
488
- locked: readonly string[];
489
- open: readonly string[];
490
739
  note: string | null;
491
740
  specHash: string;
492
741
  workflowKey: string;
@@ -514,10 +763,11 @@ export function getStartFromImport(id: string, opts?: {
514
763
  * only `createPanelServer` below always supplies one.
515
764
  * @param {import('node:http').IncomingMessage} req
516
765
  * @param {import('node:http').ServerResponse} res
517
- * @param {{ home?: string, packageJsonPath?: string, port: number, token?: string, authorRoutes?: ReturnType<typeof createAuthorRoutes>, settingsRoutes?: ReturnType<typeof createSettingsRoutes>, runRoutes?: ReturnType<typeof createRunRoutes>, importRoutes?: ReturnType<typeof createImportRoutes> }} opts
766
+ * @param {{ home?: string, sessionsRoot?: string, packageJsonPath?: string, port: number, token?: string, authorRoutes?: ReturnType<typeof createAuthorRoutes>, settingsRoutes?: ReturnType<typeof createSettingsRoutes>, runRoutes?: ReturnType<typeof createRunRoutes>, importRoutes?: ReturnType<typeof createImportRoutes>, schedules?: ReturnType<typeof createScheduleDoor> }} opts
518
767
  */
519
768
  export function handleRequest(req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse, opts: {
520
769
  home?: string;
770
+ sessionsRoot?: string;
521
771
  packageJsonPath?: string;
522
772
  port: number;
523
773
  token?: string;
@@ -525,6 +775,7 @@ export function handleRequest(req: import("node:http").IncomingMessage, res: imp
525
775
  settingsRoutes?: ReturnType<typeof createSettingsRoutes>;
526
776
  runRoutes?: ReturnType<typeof createRunRoutes>;
527
777
  importRoutes?: ReturnType<typeof createImportRoutes>;
778
+ schedules?: ReturnType<typeof createScheduleDoor>;
528
779
  }): void;
529
780
  /**
530
781
  * Start the panel server. Binds `127.0.0.1` ONLY. A taken port is a LOUD,
@@ -545,8 +796,9 @@ export function handleRequest(req: import("node:http").IncomingMessage, res: imp
545
796
  * @param {{ port?: number, home?: string, env?: Record<string,string|undefined>,
546
797
  * sessionsRoot?: string, spawnFn?: (...a: any[]) => any, bareloopBin?: string,
547
798
  * fetchImpl?: typeof fetch, settleMs?: number, userHome?: string,
548
- * openFolderSpawn?: (cmd: string, args: string[], o: object) => any, platform?: string, packageJsonPath?: string }} [opts]
549
- * @returns {Promise<{ server: import('node:http').Server, port: number, token: string, close: () => Promise<void> }>}
799
+ * openFolderSpawn?: (cmd: string, args: string[], o: object) => any, platform?: string, packageJsonPath?: string,
800
+ * scheduleNow?: () => number, sessionDeps?: Record<string, any>, schedulerIntervalMs?: number, acceptWaitMs?: number }} [opts] `scheduleNow`, `sessionDeps` and `schedulerIntervalMs` are TEST SEAMS (the schedule clock; createSession's stubs; the scheduler's timer, 0 = none)
801
+ * @returns {Promise<{ server: import('node:http').Server, port: number, token: string, schedules: ReturnType<typeof createScheduleDoor>, scheduler: ReturnType<typeof createScheduler>, close: () => Promise<void> }>}
550
802
  */
551
803
  export function createPanelServer(opts?: {
552
804
  port?: number;
@@ -561,10 +813,16 @@ export function createPanelServer(opts?: {
561
813
  openFolderSpawn?: (cmd: string, args: string[], o: object) => any;
562
814
  platform?: string;
563
815
  packageJsonPath?: string;
816
+ scheduleNow?: () => number;
817
+ sessionDeps?: Record<string, any>;
818
+ schedulerIntervalMs?: number;
819
+ acceptWaitMs?: number;
564
820
  }): Promise<{
565
821
  server: import("node:http").Server;
566
822
  port: number;
567
823
  token: string;
824
+ schedules: ReturnType<typeof createScheduleDoor>;
825
+ scheduler: ReturnType<typeof createScheduler>;
568
826
  close: () => Promise<void>;
569
827
  }>;
570
828
  /**
@@ -604,13 +862,37 @@ export const DEFAULT_PORT: 4700;
604
862
  */
605
863
  export const SOFTGREEN_ADMITTED_ISO: "2026-08-18T00:00:00.000Z";
606
864
  export { DIED_MTIME_MS };
865
+ /** The named limits of the runs list, in ONE place (P9 v2 D.13): quiet after 30 days, 7 older runs shown, a Resume tag ages out after 7 days. Sent in `GET /api/runs`; the page reads them from there. */
866
+ export const RUNS_LIST_LIMITS: Readonly<{
867
+ quietDays: 30;
868
+ olderRuns: 7;
869
+ needsYouDays: 7;
870
+ }>;
871
+ /** what the Result tab's open door asks, after the door's own head words */
872
+ export const RESULT_DOOR_ASK: "the checks passed; is this what you wanted?";
873
+ /** the two sentences for a delivered file that is no longer what the run wrote (code-owned, one spelling) */
874
+ export const RESULT_FILE_GONE: "the saved file is gone";
875
+ export const RESULT_FILE_CHANGED: "the saved file changed after the run";
876
+ /** the same two states for a red run's file, which is the last one the check read (not a delivered file) */
877
+ export const RESULT_JUDGED_GONE: "the file is gone";
878
+ export const RESULT_JUDGED_CHANGED: "the file changed after the check";
879
+ /** what a red run's rows are, said once above them */
880
+ export const RESULT_JUDGED_NOTE: "a red run delivers nothing; these rows are the last file the check read";
881
+ /** the words beside a Result-tab page link: the page is read again from the web, so it can differ from what the run read */
882
+ export const RESULT_PAGE_MAY_CHANGE: "The page may have changed since this run.";
883
+ /** the words after a fact whose limit the person agreed to in a Part 5b look ask */
884
+ export const RESULT_YOU_CONFIRMED: "(you confirmed)";
607
885
  /** The default/max page size for {@link getRunRounds} — tighten-only,
608
886
  * named so a future change is a deliberate, visible edit (item 5,
609
887
  * 2026-09-25). */
610
888
  export const ROUNDS_PAGE_DEFAULT: 50;
611
889
  export const ROUNDS_PAGE_MAX: 200;
890
+ /** the Audit step table's page size (PRD v1.106 Part 5 choice 3): rows, not rounds */
891
+ export const STEP_ROWS_PAGE: 20;
892
+ import { createScheduleDoor } from './schedules.js';
612
893
  import { createAuthorRoutes } from './authorroutes.js';
613
894
  import { createSettingsRoutes } from './settingsroutes.js';
614
895
  import { createRunRoutes } from './runroutes.js';
615
896
  import { createImportRoutes } from './importroutes.js';
897
+ import { createScheduler } from './scheduler.js';
616
898
  import { DIED_MTIME_MS } from '../runlist.js';
@@ -34,6 +34,26 @@ export function partStatusFor(part: {
34
34
  sign: string;
35
35
  word: string;
36
36
  };
37
+ /**
38
+ * The words one door wears: header phrase, the `last answer:` line (with the time `HH:MM` once decided) and the table rows.
39
+ * @param {{at: string|null, paths: string[], decision: string|null, text: string|null, decidedAt: string|null}} door
40
+ * @returns {{ head: string, last: string, rows: Array<{path: string|null, text: string, time: string|null}> }}
41
+ */
42
+ export function doorViewOf(door: {
43
+ at: string | null;
44
+ paths: string[];
45
+ decision: string | null;
46
+ text: string | null;
47
+ decidedAt: string | null;
48
+ }): {
49
+ head: string;
50
+ last: string;
51
+ rows: Array<{
52
+ path: string | null;
53
+ text: string;
54
+ time: string | null;
55
+ }>;
56
+ };
37
57
  /** the sentence a passed run carries as its reason (the Ended block's `line`) */
38
58
  export const GOAL_MET_LINE: "goal met";
39
59
  /** @typedef {'running'|'waiting'|'passed'|'failed'|'capped'|'stopped'|'died'} StatusKey */
@@ -68,4 +88,35 @@ export const STATUS: Readonly<{
68
88
  word: "died";
69
89
  }>;
70
90
  }>;
91
+ /**
92
+ * PRD v1.106 Part 5 (C, choice 9): the review door's words, ONE table. The door is the only human step bareloop has; there is no "you
93
+ * cancelled" (cancel is deleted). `decision` is the spine's `door-decision.decision`, or null while nothing has been decided.
94
+ */
95
+ export const DOOR_WORDS: Readonly<{
96
+ waiting: Readonly<{
97
+ head: "waiting for you";
98
+ row: "paused for you";
99
+ last: "none yet";
100
+ }>;
101
+ accept: Readonly<{
102
+ head: "you accepted";
103
+ row: "you accepted";
104
+ last: "you accepted";
105
+ }>;
106
+ rerun: Readonly<{
107
+ head: "you said rerun";
108
+ row: "you said rerun";
109
+ last: "you said rerun";
110
+ }>;
111
+ pause: Readonly<{
112
+ head: "paused";
113
+ row: "paused";
114
+ last: "paused";
115
+ }>;
116
+ }>;
117
+ /** the marker a part wears for who did its work (PRD v1.106 Part 5 C/G): the model, or a person asked */
118
+ export const ACTOR_MARKER: Readonly<{
119
+ model: "[model]";
120
+ human: "[human · ask]";
121
+ }>;
71
122
  export type StatusKey = "running" | "waiting" | "passed" | "failed" | "capped" | "stopped" | "died";
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @param {number|null|undefined} n a dollar amount
3
+ * @returns {string}
4
+ */
5
+ export function panelMoney2(n: number | null | undefined): string;
@@ -523,6 +523,8 @@ export const FORBIDDEN_WRITE_SEGMENT_PATTERN: RegExp;
523
523
  export const BOUND_REASON_MAX: 200;
524
524
  /** the recorded reason for a step attempt whose round was cut at the output cap (F122 2A b) */
525
525
  export const TRUNCATED_REASON: "output cut at the token limit (max_tokens)";
526
+ /** the plain sentence for a replan answer cut off at its length limit (hamr 2026-10-16, run mv9gm796) */
527
+ export const REPLAN_CUT_WORDS: "The replan answer was cut off at its length limit, so no new plan came back.";
526
528
  export type CategorizedError = Error & {
527
529
  category?: string;
528
530
  lib?: string;
@@ -251,8 +251,8 @@ export function judgeRowFor(rows: readonly KeyRow[], judge: {
251
251
  * the worker's own provider it calls the worker's endpoint, so a row on that endpoint with a line
252
252
  * NAMED for the judge's model prices it ({@link ratesFor}); a judge model that is absent or names
253
253
  * no line there keeps the worker's price (the worker's row, or none = the guess). A different
254
- * provider resolves with no endpoint. The judge's KEY is chosen elsewhere (`applyConfiguredKey`,
255
- * by the worker's model), so the two can come from different rows.
254
+ * provider resolves with no endpoint. The judge's KEY is chosen elsewhere (`judgeRowFor`:
255
+ * the worker's own row when the judge shares its provider), so the two can come from different rows.
256
256
  * @param {{ provider: string, model?: string|null }} judge
257
257
  * @param {string|null|undefined} workerProvider
258
258
  * @param {string|null|undefined} workerBaseUrl
package/types/replay.d.ts CHANGED
@@ -180,7 +180,26 @@ export function replayRun(spineEvents: any[], auditEvents?: any[], { runId, audi
180
180
  startTs: number | null;
181
181
  endTs: number | null;
182
182
  exitEvalDetail: string | null;
183
+ checks: Array<{
184
+ type: string;
185
+ pass: boolean;
186
+ detail: string | null;
187
+ name: string | null;
188
+ }>;
189
+ outputJudged: Array<{
190
+ path: string | null;
191
+ bytes: number | null;
192
+ sha256: string | null;
193
+ }>;
183
194
  }[];
195
+ cutOffTry: {
196
+ n: number;
197
+ iteration: any;
198
+ startSeq: any;
199
+ endSeq: any;
200
+ startTs: number | null;
201
+ endTs: number | null;
202
+ } | null;
184
203
  stepStartSeq: any;
185
204
  stepEndSeq: any;
186
205
  stepStartTs: number | null;
@@ -291,9 +310,10 @@ export function replayRun(spineEvents: any[], auditEvents?: any[], { runId, audi
291
310
  * @param {boolean|null} [draftSpendComplete] whether the drafting fold above was EXACT;
292
311
  * `null`/`undefined`/`true` all read as exact (never a new enum value elsewhere) —
293
312
  * only an explicit `false` triggers the floor wording.
313
+ * @param {number|null} [runUsd] the run's own spend as a number (excluding drafting), so the sum can be written
294
314
  * @returns {string}
295
315
  */
296
- export function moneyWithDraft(spendCore: string, draftSpentUsd: number | null, budgetUsd?: number | null, draftSpendComplete?: boolean | null): string;
316
+ export function moneyWithDraft(spendCore: string, draftSpentUsd: number | null, budgetUsd?: number | null, draftSpendComplete?: boolean | null, runUsd?: number | null): string;
297
317
  /**
298
318
  * Render a {@link replayRun} summary as one printable page: a header block
299
319
  * (goal/shape/signed/branch/outcome/spent), a timeline (steps or iterations,
@@ -4,6 +4,33 @@
4
4
  * @param {any[]} events the run's spine, parsed
5
5
  * @returns {any|null} */
6
6
  export function doorRecordOf(events: any[]): any | null;
7
+ /**
8
+ * Was this run's door ACCEPTED, honoured? A `door-decision` record with `decision: 'accept'` and `honoured: true`
9
+ * (written by {@link answerReviewDoor}, whichever surface asked: the CLI, or the panel's Accept). Pure, off the
10
+ * run's spine. A refused accept (`honoured: false`) does not count.
11
+ * @param {any[]} events the run's spine, parsed
12
+ * @returns {boolean}
13
+ */
14
+ export function doorAccepted(events: any[]): boolean;
15
+ /**
16
+ * T D.2 — a SCHEDULED run's green is held until the person accepts it (T10): a run that carries a `trigger.json` (a scheduled
17
+ * start wrote it) and holds no honoured accept. One shared predicate: `getStartFrom` reads its outcome as `'green-held'` (so
18
+ * the Reuse start refuses it unchanged), `listReuseJobs` skips it, and the schedule gate does not count it. A hand run is never held.
19
+ * @param {any[]} events the run's spine, parsed @param {any} triggerRec the run's `trigger.json`, or null
20
+ * @returns {boolean}
21
+ */
22
+ export function heldScheduled(events: any[], triggerRec: any): boolean;
23
+ /**
24
+ * Is this run WAITING for the person's Accept? Its spine holds a door record, the door has not expired ({@link doorAgeGate}),
25
+ * and no honoured `accept` or `rerun` decision follows it (a `pause` keeps the same three doors open). One owner: the runs
26
+ * list's Needs-you `Accept` tag, the Run tab's Accept button and the scheduled NEXT line all read this.
27
+ * @param {any[]} events the run's spine, parsed
28
+ * @param {{now?: () => number}} [opts]
29
+ * @returns {boolean}
30
+ */
31
+ export function doorWaiting(events: any[], opts?: {
32
+ now?: () => number;
33
+ }): boolean;
7
34
  /**
8
35
  * IS THE DOOR STILL OPEN? The same 60-day TTL a hitl checkpoint keeps
9
36
  * (`PAUSE_TTL_MS`) and deliberately the same NUMBER — the door replaced `cancel`