bareloop 0.41.0 → 0.43.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 (75) hide show
  1. package/CHANGELOG.md +109 -1
  2. package/README.md +13 -21
  3. package/bareloop.context.md +97 -34
  4. package/package.json +4 -3
  5. package/src/authorflow.js +337 -29
  6. package/src/authoring.js +92 -7
  7. package/src/authorjob.js +84 -16
  8. package/src/authorreadout.js +89 -5
  9. package/src/authorrun.js +14 -5
  10. package/src/authorscout.js +108 -0
  11. package/src/declaredclose.js +25 -14
  12. package/src/facts.js +317 -0
  13. package/src/factwords.js +129 -0
  14. package/src/inputlook.js +301 -0
  15. package/src/interviewrun.js +0 -1
  16. package/src/job.js +40 -12
  17. package/src/jobblock.js +7 -2
  18. package/src/kinds.js +84 -3
  19. package/src/labelscan.js +170 -0
  20. package/src/pagefetch.js +125 -0
  21. package/src/panel/authorroutes.js +121 -34
  22. package/src/panel/authorsession.js +154 -45
  23. package/src/panel/index.html +845 -490
  24. package/src/panel/jobplan.js +34 -11
  25. package/src/panel/runroutes.js +163 -5
  26. package/src/panel/scheduler.js +156 -0
  27. package/src/panel/schedules.js +667 -0
  28. package/src/panel/schedulestart.js +243 -0
  29. package/src/panel/server.js +467 -48
  30. package/src/planrun.js +42 -9
  31. package/src/providerrows.js +3 -3
  32. package/src/reviewdoor.js +37 -0
  33. package/src/rubricstep.js +804 -0
  34. package/src/runlist.js +35 -0
  35. package/src/schedulewords.js +296 -0
  36. package/src/source.js +166 -1
  37. package/src/trigger.js +48 -0
  38. package/src/triggerclock.js +127 -0
  39. package/src/triggerfiles.js +215 -0
  40. package/src/triggerledger.js +79 -0
  41. package/src/u-readout.js +1 -1
  42. package/src/userrun.js +20 -9
  43. package/types/authorflow.d.ts +267 -13
  44. package/types/authoring.d.ts +29 -1
  45. package/types/authorjob.d.ts +7 -5
  46. package/types/authorreadout.d.ts +27 -2
  47. package/types/authorscout.d.ts +70 -0
  48. package/types/declaredclose.d.ts +5 -3
  49. package/types/facts.d.ts +71 -0
  50. package/types/factwords.d.ts +42 -0
  51. package/types/inputlook.d.ts +238 -0
  52. package/types/job.d.ts +8 -1
  53. package/types/jobblock.d.ts +8 -1
  54. package/types/labelscan.d.ts +38 -0
  55. package/types/pagefetch.d.ts +71 -0
  56. package/types/panel/authorroutes.d.ts +34 -1
  57. package/types/panel/authorsession.d.ts +55 -14
  58. package/types/panel/jobplan.d.ts +12 -4
  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 +145 -14
  64. package/types/planrun.d.ts +2 -0
  65. package/types/providerrows.d.ts +2 -2
  66. package/types/reviewdoor.d.ts +27 -0
  67. package/types/rubricstep.d.ts +286 -0
  68. package/types/runlist.d.ts +20 -0
  69. package/types/schedulewords.d.ts +135 -0
  70. package/types/source.d.ts +94 -0
  71. package/types/trigger.d.ts +37 -0
  72. package/types/triggerclock.d.ts +52 -0
  73. package/types/triggerfiles.d.ts +93 -0
  74. package/types/triggerledger.d.ts +35 -0
  75. package/types/userrun.d.ts +11 -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,53 @@ 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;
206
306
  /**
207
307
  * `GET /api/runs/:runid` — the full replay for the Run tab: steps (or
208
308
  * iterations), counters, summary-box fields. Looks `runid` up in the run
@@ -210,11 +310,12 @@ export function listRuns(opts?: {
210
310
  * header); `null` when the runid is not in the list at all (caller renders
211
311
  * 404).
212
312
  * @param {string} runid
213
- * @param {{ home?: string }} [opts]
313
+ * @param {{ home?: string, schedules?: ReturnType<typeof createScheduleDoor> }} [opts]
214
314
  * @returns {any|null}
215
315
  */
216
316
  export function getRunDetail(runid: string, opts?: {
217
317
  home?: string;
318
+ schedules?: ReturnType<typeof createScheduleDoor>;
218
319
  }): any | null;
219
320
  /**
220
321
  * `GET /api/runs/:runid/audit` — the Audit tab's rows, off the run's own
@@ -343,11 +444,12 @@ export function inputsTextFromSpec(spec: any, source: string): string;
343
444
  * from the resolved SPEC only (a) (b) (c) — a job-start record and `none()`
344
445
  * carry no close/writeScope/tools at all, so those stay `'not recorded'`.
345
446
  * @param {string} runid
346
- * @param {{ home?: string }} [opts]
447
+ * @param {{ home?: string, schedules?: ReturnType<typeof createScheduleDoor> }} [opts]
347
448
  * @returns {any|null}
348
449
  */
349
450
  export function getRunJob(runid: string, opts?: {
350
451
  home?: string;
452
+ schedules?: ReturnType<typeof createScheduleDoor>;
351
453
  }): any | null;
352
454
  /**
353
455
  * The track record of a WORKFLOW: every LISTED run whose signed spec (the one its latest leg ran under) has this
@@ -395,14 +497,14 @@ export function reuseLine(record: {
395
497
  } | null): string;
396
498
  /**
397
499
  * `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
500
+ * the track record (v1.101: nothing is locked, every box is open). The job boxes carry the signed values, read by the
399
501
  * SAME readers the Job tab uses (`successFromSpec`/`guardrailsFromSpec`); the open four — Source, Destination, the
400
502
  * $ 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
503
  * to copy: change the job: Clear the card). `null` when the runid is not listed.
402
504
  * @param {string} runid
403
505
  * @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
506
  * @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,
507
+ * note: string|null,
406
508
  * specHash: string, workflowKey: string, spec: any, specPath: string, specNamesModel: boolean,
407
509
  * specWorker: {provider: string, endpoint: string, label: string},
408
510
  * trackRecord: ReturnType<typeof trackRecordFor>, line: string}|{ok: false, error: string}|null}
@@ -420,8 +522,6 @@ export function getStartFrom(runid: string, opts?: {
420
522
  outcome: string | null;
421
523
  card: Record<string, any>;
422
524
  from: "signed job";
423
- locked: readonly string[];
424
- open: readonly string[];
425
525
  note: string | null;
426
526
  specHash: string;
427
527
  workflowKey: string;
@@ -439,6 +539,21 @@ export function getStartFrom(runid: string, opts?: {
439
539
  ok: false;
440
540
  error: string;
441
541
  } | null;
542
+ /**
543
+ * T Part 3: a job's listed runs, newest first, each with its signed spec (null when not on disk), its terminal outcome and
544
+ * its parsed spine. The schedule door's gate (`greenFor`) reads this; nothing here builds a run context.
545
+ * @param {string} job
546
+ * @param {{ home?: string }} [opts]
547
+ * @returns {{runid: string, outcome: string|null, spec: any, records: any[]}[]}
548
+ */
549
+ export function jobRunsOf(job: string, opts?: {
550
+ home?: string;
551
+ }): {
552
+ runid: string;
553
+ outcome: string | null;
554
+ spec: any;
555
+ records: any[];
556
+ }[];
442
557
  /**
443
558
  * The Reuse workflow picker's list: one entry per JOB (same-job identity = `workflowKey`, the one `getStartFrom` uses)
444
559
  * that has at least one GREEN local run, newest green first. `runid` = that job's newest green run, so the page hands
@@ -469,7 +584,7 @@ export function listReuseJobs(opts?: {
469
584
  * @param {string} id
470
585
  * @param {{ home?: string, model?: string, provider?: string }} [opts] `model` as for {@link getStartFrom}
471
586
  * @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,
587
+ * note: string|null, specHash: string, workflowKey: string, spec: any,
473
588
  * specPath: null, trackRecord: ReturnType<typeof trackRecordFor>, line: string}|{ok: false, error: string}|null}
474
589
  */
475
590
  export function getStartFromImport(id: string, opts?: {
@@ -485,8 +600,6 @@ export function getStartFromImport(id: string, opts?: {
485
600
  };
486
601
  card: Record<string, any>;
487
602
  from: "imported job";
488
- locked: readonly string[];
489
- open: readonly string[];
490
603
  note: string | null;
491
604
  specHash: string;
492
605
  workflowKey: string;
@@ -514,16 +627,18 @@ export function getStartFromImport(id: string, opts?: {
514
627
  * only `createPanelServer` below always supplies one.
515
628
  * @param {import('node:http').IncomingMessage} req
516
629
  * @param {import('node:http').ServerResponse} res
517
- * @param {{ home?: string, port: number, token?: string, authorRoutes?: ReturnType<typeof createAuthorRoutes>, settingsRoutes?: ReturnType<typeof createSettingsRoutes>, runRoutes?: ReturnType<typeof createRunRoutes>, importRoutes?: ReturnType<typeof createImportRoutes> }} opts
630
+ * @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>, schedules?: ReturnType<typeof createScheduleDoor> }} opts
518
631
  */
519
632
  export function handleRequest(req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse, opts: {
520
633
  home?: string;
634
+ packageJsonPath?: string;
521
635
  port: number;
522
636
  token?: string;
523
637
  authorRoutes?: ReturnType<typeof createAuthorRoutes>;
524
638
  settingsRoutes?: ReturnType<typeof createSettingsRoutes>;
525
639
  runRoutes?: ReturnType<typeof createRunRoutes>;
526
640
  importRoutes?: ReturnType<typeof createImportRoutes>;
641
+ schedules?: ReturnType<typeof createScheduleDoor>;
527
642
  }): void;
528
643
  /**
529
644
  * Start the panel server. Binds `127.0.0.1` ONLY. A taken port is a LOUD,
@@ -544,8 +659,9 @@ export function handleRequest(req: import("node:http").IncomingMessage, res: imp
544
659
  * @param {{ port?: number, home?: string, env?: Record<string,string|undefined>,
545
660
  * sessionsRoot?: string, spawnFn?: (...a: any[]) => any, bareloopBin?: string,
546
661
  * fetchImpl?: typeof fetch, settleMs?: number, userHome?: string,
547
- * openFolderSpawn?: (cmd: string, args: string[], o: object) => any, platform?: string }} [opts]
548
- * @returns {Promise<{ server: import('node:http').Server, port: number, token: string, close: () => Promise<void> }>}
662
+ * openFolderSpawn?: (cmd: string, args: string[], o: object) => any, platform?: string, packageJsonPath?: string,
663
+ * 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)
664
+ * @returns {Promise<{ server: import('node:http').Server, port: number, token: string, schedules: ReturnType<typeof createScheduleDoor>, scheduler: ReturnType<typeof createScheduler>, close: () => Promise<void> }>}
549
665
  */
550
666
  export function createPanelServer(opts?: {
551
667
  port?: number;
@@ -559,10 +675,17 @@ export function createPanelServer(opts?: {
559
675
  userHome?: string;
560
676
  openFolderSpawn?: (cmd: string, args: string[], o: object) => any;
561
677
  platform?: string;
678
+ packageJsonPath?: string;
679
+ scheduleNow?: () => number;
680
+ sessionDeps?: Record<string, any>;
681
+ schedulerIntervalMs?: number;
682
+ acceptWaitMs?: number;
562
683
  }): Promise<{
563
684
  server: import("node:http").Server;
564
685
  port: number;
565
686
  token: string;
687
+ schedules: ReturnType<typeof createScheduleDoor>;
688
+ scheduler: ReturnType<typeof createScheduler>;
566
689
  close: () => Promise<void>;
567
690
  }>;
568
691
  /**
@@ -602,13 +725,21 @@ export const DEFAULT_PORT: 4700;
602
725
  */
603
726
  export const SOFTGREEN_ADMITTED_ISO: "2026-08-18T00:00:00.000Z";
604
727
  export { DIED_MTIME_MS };
728
+ /** 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. */
729
+ export const RUNS_LIST_LIMITS: Readonly<{
730
+ quietDays: 30;
731
+ olderRuns: 7;
732
+ needsYouDays: 7;
733
+ }>;
605
734
  /** The default/max page size for {@link getRunRounds} — tighten-only,
606
735
  * named so a future change is a deliberate, visible edit (item 5,
607
736
  * 2026-09-25). */
608
737
  export const ROUNDS_PAGE_DEFAULT: 50;
609
738
  export const ROUNDS_PAGE_MAX: 200;
739
+ import { createScheduleDoor } from './schedules.js';
610
740
  import { createAuthorRoutes } from './authorroutes.js';
611
741
  import { createSettingsRoutes } from './settingsroutes.js';
612
742
  import { createRunRoutes } from './runroutes.js';
613
743
  import { createImportRoutes } from './importroutes.js';
744
+ import { createScheduler } from './scheduler.js';
614
745
  import { DIED_MTIME_MS } from '../runlist.js';
@@ -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
@@ -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`
@@ -0,0 +1,286 @@
1
+ /**
2
+ * The one statement of the step-file shape, in plain words. The worker's goal and the composer's prompt both carry it.
3
+ * @returns {string}
4
+ */
5
+ export function stepFileContract(): string;
6
+ /**
7
+ * What the worker is told about each line, from the close that will grade it (PART 1b item 7): a FACTS line gets its
8
+ * facts and its count; a TEXT line that follows a facts line is told its numbers come from that line's quotes. Written by code from
9
+ * the signed parts, never by a model. A line with no rubric-step stage gets nothing here.
10
+ * @param {any} closeDecl @returns {{line: number, text: string}[]}
11
+ */
12
+ export function lineBriefs(closeDecl: any): {
13
+ line: number;
14
+ text: string;
15
+ }[];
16
+ /**
17
+ * The worker's goal for a rubric job on local files — written by code from the person's own numbered lines, never by a
18
+ * model. It states everything the close will judge about the FORM of the answer (the file, the keys, the quotes); what
19
+ * each line asks for stays in the person's own words, with its `~` rules beneath it.
20
+ * @param {{jobLines: {n: number, text: string, rule: string}[], files: {label: string, rel: string}[], confirmedGoal?: string|null, closeDecl?: any}} o
21
+ * `confirmedGoal` — the one goal sentence the person confirmed in the confirm turn, kept as the plan line
22
+ * `closeDecl` — the drafted close: with it, each line is briefed by what its stage checks (a facts line or a text line, `lineBriefs`)
23
+ * @returns {string}
24
+ */
25
+ export function rubricGoal({ jobLines, files, confirmedGoal, closeDecl }: {
26
+ jobLines: {
27
+ n: number;
28
+ text: string;
29
+ rule: string;
30
+ }[];
31
+ files: {
32
+ label: string;
33
+ rel: string;
34
+ }[];
35
+ confirmedGoal?: string | null;
36
+ closeDecl?: any;
37
+ }): string;
38
+ /**
39
+ * The SHAPE of a `rubric-step` stage's parameters — what a part may carry — and nothing about which class is allowed in
40
+ * this build (that is `checkRubricParts`, which also sees the whole job). Pure, never throws.
41
+ * @param {unknown} p the stage's `params`
42
+ * @param {string} at the path prefix for reds, e.g. `stages[1].params`
43
+ * @returns {ShapeRed[]}
44
+ */
45
+ export function rubricStepShapeReds(p: unknown, at: string): ShapeRed[];
46
+ /** the labels of the inputs a step's `cited` parts name (each once, in order) @param {any} params @returns {string[]} */
47
+ export function citedLabels(params: any): string[];
48
+ /**
49
+ * The rubric spec for ONE step, built from its parameters and the sha256 of each input it cites (taken from the source
50
+ * manifest at run start — a rubric is rebuilt per run because the inputs belong to the run, not to the signed job).
51
+ * `happened` is bareguard's own foundational check; the `text` floor below it stops a step file whose only key is junk
52
+ * from passing a rubric that has no countable part.
53
+ * @param {{step: number, parts: any[]}} params
54
+ * @param {Record<string, string>} inputShas label -> sha256 hex, for every label the step cites
55
+ * @returns {Record<string, any>} a spec for bareguard's `createRubric`
56
+ */
57
+ export function buildStepSpec(params: {
58
+ step: number;
59
+ parts: any[];
60
+ }, inputShas: Record<string, string>): Record<string, any>;
61
+ /**
62
+ * The step file's bytes as the close reads them: the parsed JSON object, or WHY there is none. A file cut off mid-write
63
+ * parses to nothing (reason `not-json`); text cut off INSIDE an otherwise valid JSON object cannot be told from a short
64
+ * answer — that limit is documented, not hidden.
65
+ * @param {string|null} bytes the file's text, or null when it does not exist
66
+ * @returns {{obj: Record<string, any>, why: null}|{obj: null, why: 'missing'|'not-json'|'not-object'}}
67
+ */
68
+ export function parseStepFile(bytes: string | null): {
69
+ obj: Record<string, any>;
70
+ why: null;
71
+ } | {
72
+ obj: null;
73
+ why: "missing" | "not-json" | "not-object";
74
+ };
75
+ /**
76
+ * The step file as bareguard's rubric reads it: a flat object. `text` stays `text`; each label in `cites` becomes the field
77
+ * `cites:<label>` (the name a `cited` check reads its claims from). Nothing else in the file is read.
78
+ * @param {Record<string, any>} obj
79
+ * @param {string[]} [countLabels] labels whose claim COUNT a minClaims/maxClaims part reads
80
+ * @param {any} [factPart] the step's `fact` part: its `results` become claims (text built by code) under `cites:<source>`
81
+ * @returns {Record<string, any>}
82
+ */
83
+ export function flattenStep(obj: Record<string, any>, countLabels?: string[], factPart?: any): Record<string, any>;
84
+ /**
85
+ * One bareguard gap as a sentence the worker can act on. The numbers and names are bareguard's own measurements and the
86
+ * person's signed limits, never anything this module infers; a gap this table does not know falls back to bareguard's own
87
+ * rendering, so nothing is ever dropped.
88
+ * @param {number} step @param {string} why why there was no object, when there was none
89
+ * @param {any} g a bareguard gap
90
+ * @param {any} [params] the stage's parameters (a fact step words its gaps by fact)
91
+ * @returns {string}
92
+ */
93
+ export function plainStepGap(step: number, why: string, g: any, params?: any): string;
94
+ /**
95
+ * Grade ONE step: build the rubric, run it over the step file's fields, and say what happened in the close's own terms.
96
+ * Pure of the filesystem (the runner in src/kinds.js reads the files). `stopped` = the instrument failed (never a red).
97
+ * @param {{params: {step: number, parts: any[]}, bytes: string|null, outputBytes: string|null, inputs: Record<string, string>,
98
+ * factQuotes?: Record<number, string[]>, factMissing?: number[]}} o
99
+ * `factQuotes` maps an earlier fact line's number to the quotes its (already checked) step file holds — what a prose step's numbers are tied to
100
+ * `factMissing` lists the earlier fact lines whose step file is not there (yet): a normal red, never an instrument fault
101
+ * `inputs` maps each cited label to its frozen TEXT; its sha256 is taken here from that text, and bareguard compares it to
102
+ * the manifest's recorded sha via `inputShas`.
103
+ * @param {Record<string, string>} inputShas the manifest's recorded sha256 per cited label
104
+ * @returns {Promise<{verdict: 'green'|'red'|'stopped', gapLines: string[], fault: string|null, detail: Record<string, any>}>}
105
+ */
106
+ export function gradeStep({ params, bytes, outputBytes, inputs, factQuotes, factMissing }: {
107
+ params: {
108
+ step: number;
109
+ parts: any[];
110
+ };
111
+ bytes: string | null;
112
+ outputBytes: string | null;
113
+ inputs: Record<string, string>;
114
+ factQuotes?: Record<number, string[]>;
115
+ factMissing?: number[];
116
+ }, inputShas: Record<string, string>): Promise<{
117
+ verdict: "green" | "red" | "stopped";
118
+ gapLines: string[];
119
+ fault: string | null;
120
+ detail: Record<string, any>;
121
+ }>;
122
+ /**
123
+ * The labels of a job's numbered inputs, or null when it carries none (a caller that has no inputs in hand skips the label rule).
124
+ * @param {unknown} inputs @returns {string[]|null}
125
+ */
126
+ export function inputLabelsOf(inputs: unknown): string[] | null;
127
+ /**
128
+ * R11 — the drafting-time rules for the `rubric-step` stages of a declaration, each a red in the person's plain words that
129
+ * names the job line and ends "Nothing spent." where nothing was. Pure; never throws. Judged parts are RED in this build
130
+ * (the judge arrives in the next one): the sorting is still asked for, so a part that needs the judge is named as such
131
+ * instead of being approximated by a count.
132
+ * - a stage serves exactly the one job line it names (`step` = its only `fromLine`), and no line has two stages;
133
+ * - a part with no class (`none`) cannot be counted or checked against a quote;
134
+ * - a `judged` part needs the judge; a `guard` part is about web sources and the job has no web input;
135
+ * - two countable parts of ONE line that no text can satisfy together (a minimum above a maximum) are a contradiction,
136
+ * naming both parts and both numbers;
137
+ * - a `cited` part names a label the person's inputs do not have.
138
+ * @param {any[]} stages the declaration's stages
139
+ * @param {{inputLabels?: string[]|null, red: (code: string, path: string, detail: string, extra?: object) => void}} o
140
+ * `inputLabels` absent = the label rule is not run (a caller with no inputs in hand)
141
+ */
142
+ export function checkRubricParts(stages: any[], { inputLabels, red }: {
143
+ inputLabels?: string[] | null;
144
+ red: (code: string, path: string, detail: string, extra?: object) => void;
145
+ }): void;
146
+ /** the ~ rules of one job line, one string each (`rules` when the line has the array, else the joined `rule` split on "; ") @param {any} l @returns {string[]} */
147
+ export function lineRules(l: any): string[];
148
+ /**
149
+ * PART 1b, RF16 + RF17 — the rules that need the person's own job lines. Only a close built for REQUIRED FACTS is held to them (a
150
+ * close with a fact part, or any part that says which `~` rules it carries out): an older Part 1a close is left as it was signed.
151
+ * - every `~` rule of every job line is carried out by a part (its number in the part's `rules`), or is listed in `refused` with
152
+ * its `rule` number, or its whole line is refused: a rule that is none of those was silently dropped, and that is a red;
153
+ * - a number the AI made out of the person's words (a bound the person did not write) must be marked `aiProposal`, and a time
154
+ * limit the AI proposes must be a range (`between`), never one-sided — "not a red-eye" must not let a 10 PM flight through.
155
+ * @param {any[]} stages @param {{jobLines?: any[]|null, refused?: any, red: (code: string, path: string, detail: string, extra?: object) => void}} o
156
+ */
157
+ export function checkRubricRules(stages: any[], { jobLines, refused, red }: {
158
+ jobLines?: any[] | null;
159
+ refused?: any;
160
+ red: (code: string, path: string, detail: string, extra?: object) => void;
161
+ }): void;
162
+ /**
163
+ * PART 1c L3 - the person's own words win over the AI's reading of them, settled by CODE before anything is validated or signed:
164
+ * - a limit on a fact whose bounds are all in the words the person wrote (the line, its `~` rules, an answer they gave) is the
165
+ * person's, so it is NOT marked "AI's proposal" (smv710w8c3wdh: the plan called the person's own range the AI's);
166
+ * - a time range the AI proposed while the person wrote ONE explicit positive time range ("between 07:00 and 23:00") on that line
167
+ * is replaced by exactly that range (smv710w8c3wdh: the plan said 5:00 AM). Only when it is unambiguous: one such range in the
168
+ * line's rules and one AI-proposed `between` time fact on the line; a rule with a negation ("not between ...") is never read as a range;
169
+ * - a `~` rule is a check or under Not checked, never both: a `refused` entry for a rule that a part of its own line carries is dropped.
170
+ * Pure; returns a new declaration, the input untouched. The model is told the same in the brief; this makes it true either way.
171
+ * @param {any} declaration @param {any[]|null|undefined} jobLines @returns {any}
172
+ */
173
+ export function settlePersonsWords(declaration: any, jobLines: any[] | null | undefined): any;
174
+ /**
175
+ * PART 1b — put the number tie on every prose step. A rubric-step stage with no fact part that comes AFTER one or more fact
176
+ * stages gets exactly one `numbersFromFacts` part naming those lines; one the model wrote is replaced. Pure; returns a new
177
+ * declaration (the input is untouched). The model never writes this part: it is code's, so a draft cannot omit it.
178
+ * @param {any} declaration @returns {any}
179
+ */
180
+ export function withFactBindings(declaration: any): any;
181
+ /**
182
+ * The ~ rules the drafter listed under Not checked (`refused` entries that name a `rule`), each as one line in the person's own
183
+ * words: `line N: "<the rule as written>" — <the drafter's reason>` (PART 1b, RF6 / RF16).
184
+ * @param {any} closeDecl @param {any[]|null|undefined} jobLines @returns {string[]}
185
+ */
186
+ export function ruleNotChecked(closeDecl: any, jobLines: any[] | null | undefined): string[];
187
+ /**
188
+ * The Not-checked lines code owns for the rubric-step stages of a close: a prose step's CONTENT is judged by the person at
189
+ * Accept, and the card says so (PART 1b, RF5). One line per prose step (a stage with no fact part).
190
+ * @param {any} closeDecl @returns {string[]}
191
+ */
192
+ export function proseNotChecked(closeDecl: any): string[];
193
+ /**
194
+ * One countable part as a plain sentence, written by code from the part's signed fields (never from the model's words), so the
195
+ * plan card and the close are one thing seen twice.
196
+ * @param {any} part @returns {string}
197
+ */
198
+ export function partSentence(part: any): string;
199
+ /**
200
+ * The checks a signed close actually runs, per job line, in plain code-written sentences: one per countable part of every
201
+ * rubric-step stage. This is the ONE reader both the plan card and the sign-time invariant use.
202
+ * @param {any} closeDecl
203
+ * @returns {{line: number, text: string, covers: number[]}[]}
204
+ */
205
+ export function checksOfClose(closeDecl: any): {
206
+ line: number;
207
+ text: string;
208
+ covers: number[];
209
+ }[];
210
+ /**
211
+ * Reconcile the CONFIRMED plan with the close the composer actually wrote. The plan card after drafting shows exactly
212
+ * the close's own checks (`checksOfClose`); every confirmed check that no part carries out (no part lists its number in
213
+ * `covers`) is moved to Not checked, in the person's words, with the reason. A check can therefore never be shown and then
214
+ * silently dropped (run mv5bk400: "lists 3 releases" was confirmed, then left out of the signed close without a word).
215
+ * @param {{confirmed: {text: string, fromLine: number[]|null}[], notChecked: string[], closeDecl: any, jobLines?: any[]|null}} o
216
+ * `jobLines` — the person's numbered lines, so a ~ rule the drafter listed as Not checked reads in the person's own words
217
+ * @returns {{checkItems: {text: string, fromLine: number[]}[], notChecked: string[], moved: {text: string, fromLine: number[]|null}[]}}
218
+ */
219
+ export function reconcileRubricPlan({ confirmed, notChecked, closeDecl, jobLines }: {
220
+ confirmed: {
221
+ text: string;
222
+ fromLine: number[] | null;
223
+ }[];
224
+ notChecked: string[];
225
+ closeDecl: any;
226
+ jobLines?: any[] | null;
227
+ }): {
228
+ checkItems: {
229
+ text: string;
230
+ fromLine: number[];
231
+ }[];
232
+ notChecked: string[];
233
+ moved: {
234
+ text: string;
235
+ fromLine: number[] | null;
236
+ }[];
237
+ };
238
+ /**
239
+ * The sign-time invariant: the checks the person was shown (`shown`, from the plan card) are exactly the checks the signed
240
+ * close runs (`checksOfClose`), per line. Pure.
241
+ * @param {{text: string, fromLine: number[]|null}[]} shown @param {any} closeDecl
242
+ * @returns {{ok: true}|{ok: false, error: string}}
243
+ */
244
+ export function planMatchesClose(shown: {
245
+ text: string;
246
+ fromLine: number[] | null;
247
+ }[], closeDecl: any): {
248
+ ok: true;
249
+ } | {
250
+ ok: false;
251
+ error: string;
252
+ };
253
+ /**
254
+ * The frozen TEXT of the named inputs, read the way the runner reads them: the source door's record (`source.json`, one level
255
+ * above the tree) names each input's file and sha256, and a file whose bytes no longer match is not returned. Used at DRAFTING
256
+ * (the drafter reads the input, the early tests run on it) and at run start (the backstop). Never throws.
257
+ * @param {string} workdir the frozen tree @param {string[]} labels
258
+ * @returns {Promise<{ok: true, texts: Record<string, string>}|{ok: false, why: string}>}
259
+ */
260
+ export function readFrozenInputs(workdir: string, labels: string[]): Promise<{
261
+ ok: true;
262
+ texts: Record<string, string>;
263
+ } | {
264
+ ok: false;
265
+ why: string;
266
+ }>;
267
+ /** where the per-line step files live, inside the job's tree; `copyOut` delivers them with the rest of `output/` */
268
+ export const STEP_DIR: "output/steps";
269
+ export function stepFileRel(n: number): string;
270
+ /** the largest step file the close reads: a bigger one is a stop, never silently truncated (bareguard's own source cap) */
271
+ export const STEP_MAX_BYTES: number;
272
+ /** the closed set of classes a part is sorted into; `none` = "I could not sort this one" (a red, in plain words, never a guess) */
273
+ export const PART_CLASSES: readonly string[];
274
+ /**
275
+ * The counted/quoted checks a `countable` part may name, with the fields each needs — read off bareguard's own rule table
276
+ * (`rubricVocabulary`): the names are bareguard's, so a check this build cannot run is inexpressible, not rejected late.
277
+ * (`max`/`min` need a numeric field the step file does not have, so they are not offered.)
278
+ * @type {Readonly<Record<string, readonly string[]>>}
279
+ */
280
+ export const COUNT_CHECKS: Readonly<Record<string, readonly string[]>>;
281
+ export function sha256Of(b: string | Buffer): string;
282
+ export type ShapeRed = {
283
+ code: string;
284
+ path: string;
285
+ detail: string;
286
+ };