bareloop 0.42.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 (43) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/bareloop.context.md +53 -30
  3. package/package.json +1 -1
  4. package/src/authorjob.js +6 -0
  5. package/src/declaredclose.js +2 -2
  6. package/src/interviewrun.js +0 -1
  7. package/src/job.js +38 -11
  8. package/src/panel/authorroutes.js +107 -33
  9. package/src/panel/authorsession.js +59 -27
  10. package/src/panel/index.html +813 -484
  11. package/src/panel/runroutes.js +163 -5
  12. package/src/panel/scheduler.js +156 -0
  13. package/src/panel/schedules.js +667 -0
  14. package/src/panel/schedulestart.js +243 -0
  15. package/src/panel/server.js +451 -48
  16. package/src/planrun.js +39 -7
  17. package/src/providerrows.js +3 -3
  18. package/src/reviewdoor.js +37 -0
  19. package/src/runlist.js +35 -0
  20. package/src/schedulewords.js +296 -0
  21. package/src/trigger.js +48 -0
  22. package/src/triggerclock.js +127 -0
  23. package/src/triggerfiles.js +215 -0
  24. package/src/triggerledger.js +79 -0
  25. package/src/userrun.js +6 -7
  26. package/types/declaredclose.d.ts +1 -1
  27. package/types/job.d.ts +8 -1
  28. package/types/panel/authorroutes.d.ts +34 -1
  29. package/types/panel/authorsession.d.ts +37 -12
  30. package/types/panel/runroutes.d.ts +32 -1
  31. package/types/panel/scheduler.d.ts +17 -0
  32. package/types/panel/schedules.d.ts +412 -0
  33. package/types/panel/schedulestart.d.ts +106 -0
  34. package/types/panel/server.d.ts +143 -14
  35. package/types/planrun.d.ts +2 -0
  36. package/types/providerrows.d.ts +2 -2
  37. package/types/reviewdoor.d.ts +27 -0
  38. package/types/runlist.d.ts +20 -0
  39. package/types/schedulewords.d.ts +135 -0
  40. package/types/trigger.d.ts +37 -0
  41. package/types/triggerclock.d.ts +52 -0
  42. package/types/triggerfiles.d.ts +93 -0
  43. package/types/triggerledger.d.ts +35 -0
@@ -54,6 +54,18 @@ export function planMessage(p: {
54
54
  export function jobNameTaken(name: string, opts?: {
55
55
  jobsDir?: string;
56
56
  }): boolean;
57
+ /**
58
+ * The next free name for a NEW workflow made from `origin` (RULED 2026-10-15, v1.101): `<base>-N`, N the next free number from 2.
59
+ * Base: when the origin ends in `-<digits>` AND the part before it is itself a job (in the run list or jobs/), that part (so `x-2`
60
+ * suggests `x-3`, never `x-2-2`); else the origin's own name. Free = not a run-list job and not {@link jobNameTaken}.
61
+ * @param {string} origin
62
+ * @param {Iterable<string>} runJobs job names in the panel's run list
63
+ * @param {{jobsDir?: string}} [opts] test seam for {@link jobNameTaken}
64
+ * @returns {string}
65
+ */
66
+ export function nextFreeJobName(origin: string, runJobs: Iterable<string>, opts?: {
67
+ jobsDir?: string;
68
+ }): string;
57
69
  /**
58
70
  * The repo a card names: Inputs line 1 (P7 — the Source box is gone; the first input IS the repo). `''` when the Inputs
59
71
  * box does not parse (the card validator refuses that before anything uses this).
@@ -68,13 +80,13 @@ export function cardSource(card: any): string;
68
80
  */
69
81
  export function cardFields(card: any): Record<string, any>;
70
82
  /**
71
- * The first LOCKED box on `card` that differs from what the signed workflow prefilled (`origin`), or `null` when
72
- * every locked box is untouched. Blank and absent read alike; whitespace is not compared.
83
+ * Did any JOB box on `card` change from what the origin run prefilled (`origin`)? Whitespace is not compared (a one-line input drops
84
+ * a text's newlines); an absent thinking tick reads as off.
73
85
  * @param {Record<string, any>} card the form as it was submitted
74
86
  * @param {Record<string, any>} origin the form as the server prefilled it
75
- * @returns {string|null} the offending field's label
87
+ * @returns {boolean}
76
88
  */
77
- export function lockedFieldChanged(card: Record<string, any>, origin: Record<string, any>): string | null;
89
+ export function jobBoxesChanged(card: Record<string, any>, origin: Record<string, any>): boolean;
78
90
  /**
79
91
  * The spec a reuse signs: a COPY of the origin's signed spec with ONLY the open fields set from the card —
80
92
  * `writeScope` from Destination, `inputs` from the Inputs box (P7), `budgetUsd` from the $ cap, `maxWallMs` from the Time cap (absent when blank), and the
@@ -106,6 +118,18 @@ export function validateReuseCard(card: any, opts?: {
106
118
  ok: false;
107
119
  error: string;
108
120
  };
121
+ /**
122
+ * THE ONE Destination check of a session start, at $0, before any session, copy or model call. The session's own pipeline, the HTTP
123
+ * start (`startSession`), the schedule's two clicks and a scheduled fire's `wouldAccept` all call THIS (never a copy of it), so a start
124
+ * that would be refused for its Destination is refused before anything is made.
125
+ * @param {{inputs?: unknown, destination?: unknown, checkType?: unknown}} card the Inputs text (line 1 = the source), the Destination and the check type
126
+ * @returns {string|null} the plain refusal, or null when the Destination is fine
127
+ */
128
+ export function startDestinationProblem(card: {
129
+ inputs?: unknown;
130
+ destination?: unknown;
131
+ checkType?: unknown;
132
+ }): string | null;
109
133
  /**
110
134
  * The ONE Destination rule for a REPO source (fresh card and Reuse card alike): there Destination is the write fence,
111
135
  * relative to the repo, so an absolute path is refused — at $0, before any copy or model call. A FOLDER source (or a
@@ -120,11 +144,12 @@ export function repoDestinationProblem(destination: string, isRepoLike: boolean)
120
144
  * creates a session or spends anything — never inside the async pipeline,
121
145
  * so a bad card refuses synchronously, in the same HTTP response.
122
146
  * @param {any} card
123
- * @param {{jobsDir?: string, rows?: readonly import('../providerrows.js').KeyRow[]}} [opts] `rows` = the Settings rows (the Model menu); jobsDir is a server-side test seam — NEVER read off the client card
147
+ * @param {{jobsDir?: string, allowName?: string, rows?: readonly import('../providerrows.js').KeyRow[]}} [opts] `rows` = the Settings rows (the Model menu); jobsDir is a server-side test seam — NEVER read off the client card; `allowName` = the origin job's own name on an unchanged red Reuse (it is the same job, not a collision)
124
148
  * @returns {{ok: true}|{ok: false, error: string}}
125
149
  */
126
150
  export function validateJobCard(card: any, opts?: {
127
151
  jobsDir?: string;
152
+ allowName?: string;
128
153
  rows?: readonly import("../providerrows.js").KeyRow[];
129
154
  }): {
130
155
  ok: true;
@@ -195,14 +220,14 @@ export const PLAN_FOOTER: "Sign & run to confirm this plan, or Revise to edit th
195
220
  /** the form's fields, in card order — what `card.json` stores, verbatim (P5 item 3) */
196
221
  export const CARD_FIELDS: readonly string[];
197
222
  /**
198
- * REUSE WORKFLOW (hamr 2026-10-03, replaces P5 item 3's same-job rule; Model opened 2026-10-04): a reuse card has exactly
199
- * FIVE open boxes — Inputs, Destination, Model, $ cap, Time cap — and every other box is the signed workflow, shown greyed and never
200
- * editable. The page greys them; THIS is what refuses (the server never trusts the page). Changing any locked box
201
- * is a different job: Clear the card, which drafts.
223
+ * REUSE WORKFLOW, ONE DOOR (RULED 2026-10-15, v1.101; replaces v1.88's five open boxes). Every box on the card is open. What decides
224
+ * the path is the CARD compared with the origin run's card, never the drafted spec: the JOB boxes are the job text (numbered lines,
225
+ * `~` rules, `~ PASS`/`~ FAIL`), the check type and the thinking tick. The RUN boxes (Inputs, Destination, Model, $ cap, Time cap)
226
+ * never decide it. A green origin with no job box changed reuses the signed plan verbatim (no drafting); anything else drafts.
202
227
  */
203
- export const REUSE_OPEN_FIELDS: readonly string[];
204
- /** every card field that is NOT open on a reuse card, in card order */
205
- export const REUSE_LOCKED_FIELDS: readonly string[];
228
+ export const JOB_BOXES: readonly string[];
229
+ /** the refusal for a plain-files rubric job whose Destination is not an absolute folder (ends with the words the session start shows) */
230
+ export const FILES_DESTINATION_WORDS: "Destination must be the full path of a folder, like /home/me/out \u2014 the finished files are delivered there. Nothing spent.";
206
231
  export type StepLine = {
207
232
  id: string;
208
233
  label: string;
@@ -1,3 +1,15 @@
1
+ /**
2
+ * RULED 2026-10-16: the person never sees raw engine output. Of the engine's log tail, only a line it
3
+ * owns as a plain sentence passes through (a `Nothing spent.` / `Nothing was spent.` refusal, `DOOR REFUSED (...) — nothing was
4
+ * recorded...`, `CHECKPOINT EXPIRED — ...`); anything else (validator reds, exit statuses, stack traces, argv) becomes ONE fixed
5
+ * sentence naming the log. The one owner for the Resume and Accept routes.
6
+ * @param {string} tail the log tail
7
+ * @param {'resume'|'accept'} kind
8
+ * @param {string} logFile
9
+ * @param {(stage: string) => string|null} [questionOf] stage name to the panel's plain question (`null` = none)
10
+ * @returns {string}
11
+ */
12
+ export function engineRefusalWords(tail: string, kind: "resume" | "accept", logFile: string, questionOf?: (stage: string) => string | null): string;
1
13
  /**
2
14
  * The run routes' handler. `getResumeContext(runid)` is the server's own reader (the same
3
15
  * `resumePlanFor` the Ended block uses, so the page never offers what this refuses):
@@ -5,7 +17,11 @@
5
17
  * @param {{ port: number, token: string, env?: Record<string,string|undefined>, home?: string,
6
18
  * spawnFn?: typeof realSpawn, bareloopBin?: string, settleMs?: number,
7
19
  * getResumeContext: (runid: string) => ({row: any, plan: any}|null),
8
- * getStopContext?: (runid: string) => ({row: any, live: boolean, spineExists: boolean}|null) }} opts
20
+ * getStopContext?: (runid: string) => ({row: any, live: boolean, spineExists: boolean}|null),
21
+ * getNeedsContext?: (runid: string) => ({row: any, needs: 'Accept'|'Resume'|null}|null),
22
+ * getAcceptContext?: (runid: string) => ({row: any, live: boolean, waiting: boolean, specPath: string|null, specHash: string|null}|null),
23
+ * stageQuestionsOfSpec?: (spec: any) => Map<string, string|null>,
24
+ * acceptWaitMs?: number }} opts
9
25
  */
10
26
  export function createRunRoutes(opts: {
11
27
  port: number;
@@ -24,9 +40,24 @@ export function createRunRoutes(opts: {
24
40
  live: boolean;
25
41
  spineExists: boolean;
26
42
  } | null);
43
+ getNeedsContext?: (runid: string) => ({
44
+ row: any;
45
+ needs: "Accept" | "Resume" | null;
46
+ } | null);
47
+ getAcceptContext?: (runid: string) => ({
48
+ row: any;
49
+ live: boolean;
50
+ waiting: boolean;
51
+ specPath: string | null;
52
+ specHash: string | null;
53
+ } | null);
54
+ stageQuestionsOfSpec?: (spec: any) => Map<string, string | null>;
55
+ acceptWaitMs?: number;
27
56
  }): {
28
57
  handle: (req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse, pathname: string, body: any) => boolean;
29
58
  };
30
59
  /** how long a resume waits for an early engine exit before reporting "started" (ms) */
31
60
  export const DEFAULT_SETTLE_MS: 6000;
61
+ /** how long an Accept waits for the engine's answer (it re-runs the mechanical close stages) before saying it is still checking (ms) */
62
+ export const DEFAULT_ACCEPT_WAIT_MS: number;
32
63
  import { spawn as realSpawn } from 'node:child_process';
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @param {{ door: ReturnType<typeof import('./schedules.js').createScheduleDoor>, now?: () => number, intervalMs?: number }} opts
3
+ * `intervalMs` 0 = no timer (a test drives `tick`).
4
+ */
5
+ export function createScheduler(opts: {
6
+ door: ReturnType<typeof import("./schedules.js").createScheduleDoor>;
7
+ now?: () => number;
8
+ intervalMs?: number;
9
+ }): {
10
+ tick: () => Promise<void>;
11
+ /** Start the real timer (idempotent). */
12
+ start(): void;
13
+ /** Stop it (idempotent). The server's close calls this. */
14
+ stop(): void;
15
+ /** whether the real timer is running (a test reads this) */
16
+ readonly running: boolean;
17
+ };
@@ -0,0 +1,412 @@
1
+ /**
2
+ * When a process started, in epoch ms, from `/proc/<pid>/stat` field 22 (clock ticks since boot) and `/proc/stat` btime
3
+ * (whole seconds, so good to about a second). `null` where /proc cannot be read: the caller then trusts `isLiveRunner` alone.
4
+ * @param {number} pid
5
+ * @param {(path: string) => string} [read]
6
+ * @returns {number|null}
7
+ */
8
+ export function procStartMs(pid: number, read?: (path: string) => string): number | null;
9
+ /**
10
+ * Is this listed run's runner alive? `runIsAlive` (the pid is alive AND a bareloop runner) plus the PID REUSE GUARD: a
11
+ * pid whose process started MORE than 5 s after the row was written is a recycled pid, not that run. A resumed run's row
12
+ * keeps its first leg's time while its pid is the latest leg's, so the guard is skipped for it (the cmdline check stands).
13
+ * @param {any} row @param {number} nowMs
14
+ */
15
+ export function runnerAlive(row: any, nowMs: number): boolean;
16
+ /**
17
+ * @typedef {{runid: string, outcome: string|null, spec: any, records: any[]}} JobRun
18
+ * @typedef {{job: string, trigger: import('../trigger.js').Trigger, spec: any, specHash: string, workflowKey: string,
19
+ * originRunid: string, inputsText: string, destination?: string, signedBy: string, signedAt: string}} Schedule
20
+ */
21
+ /**
22
+ * @param {{
23
+ * home?: string,
24
+ * sessionsRoot?: string,
25
+ * now?: () => number,
26
+ * jobRuns: (job: string) => JobRun[],
27
+ * startFrom: (runid: string) => any,
28
+ * jobOfRun: (runid: string) => string|null,
29
+ * rowsFor: () => readonly import('../providerrows.js').KeyRow[],
30
+ * runRows?: () => any[],
31
+ * installRetrySteps?: readonly number[], installGiveUpMs?: number,
32
+ * userHome?: string,
33
+ * }} deps `userHome` = the folder shown as `~` in words (default the OS home). `installRetrySteps` (ms) and `installGiveUpMs` = the install-needed back-off (tests inject them). `runRows` = a test seam for the one run-list read the busy check does. `jobRuns(job)` = the job's listed runs, newest first, each with its signed spec when on disk and its terminal
34
+ * outcome off the spine; `startFrom(runid)` = the panel's `getStartFrom`; `jobOfRun` = the listed run's job name.
35
+ */
36
+ export function createScheduleDoor(deps: {
37
+ home?: string;
38
+ sessionsRoot?: string;
39
+ now?: () => number;
40
+ jobRuns: (job: string) => JobRun[];
41
+ startFrom: (runid: string) => any;
42
+ jobOfRun: (runid: string) => string | null;
43
+ rowsFor: () => readonly import("../providerrows.js").KeyRow[];
44
+ runRows?: () => any[];
45
+ installRetrySteps?: readonly number[];
46
+ installGiveUpMs?: number;
47
+ userHome?: string;
48
+ }): {
49
+ /** The starter is built after the author routes (which need this door), so it is attached here. @param {ReturnType<typeof import('./schedulestart.js').createStarter>} s */
50
+ attachStarter(s: ReturnType<typeof import("./schedulestart.js").createStarter>): void;
51
+ busy: (job: string) => boolean;
52
+ paused: (job: string) => boolean;
53
+ /**
54
+ * RULED 2026-10-15 (v1.104): the fires that started NOTHING (busy, month used up, refused) of ALL signings of the job's schedule
55
+ * (the job's ledger is the job's, so every signing's rows are in it; cancelled-and-set-again rows stay). Newest first. Lines are
56
+ * `MM/DD HH:MM <words>`. The caller shows the first 7 and counts them all.
57
+ * @param {string} job
58
+ * @returns {{at: number, line: string}[]}
59
+ */
60
+ skippedFor(job: string): {
61
+ at: number;
62
+ line: string;
63
+ }[];
64
+ /**
65
+ * One fire, in this order (T15): busy, then monthly, then wouldAccept, then the starter (which calls `beforeSpawn`, the
66
+ * ONE consuming row, only after every "no" is behind it). Each outcome is ONE row in ledger.jsonl, told once per
67
+ * `key|outcome|why` (read back from disk, so a restart does not repeat it). RULED 2026-10-14: a busy, monthly or refused
68
+ * fire consumes NOTHING, so the next tick tries again; a spawn error after `beforeSpawn` writes a `released` row (bounded).
69
+ * @param {string} job
70
+ * @param {{kind: 'clock', slot: number, catchUp: boolean, missed: number} | {kind: 'file', path: string, sha: string, size?: number}} cause
71
+ * @param {{beforeSpawn?: () => void, busy?: boolean}} [hooks] `busy` = the scheduler's one read of the run list this tick
72
+ * @returns {Promise<{outcome: 'busy'|'monthly-exhausted'|'refused'|'started', why?: string, told: boolean, sessionId?: string}>}
73
+ */
74
+ fire(job: string, cause: {
75
+ kind: "clock";
76
+ slot: number;
77
+ catchUp: boolean;
78
+ missed: number;
79
+ } | {
80
+ kind: "file";
81
+ path: string;
82
+ sha: string;
83
+ size?: number;
84
+ }, hooks?: {
85
+ beforeSpawn?: () => void;
86
+ busy?: boolean;
87
+ }): Promise<{
88
+ outcome: "busy" | "monthly-exhausted" | "refused" | "started";
89
+ why?: string;
90
+ told: boolean;
91
+ sessionId?: string;
92
+ }>;
93
+ greenFor: (job: string) => {
94
+ ok: true;
95
+ run: JobRun;
96
+ key: string;
97
+ } | {
98
+ ok: false;
99
+ say: string;
100
+ };
101
+ armFor: (job: string, schedule: Schedule, o?: {
102
+ baseline?: boolean;
103
+ }) => {
104
+ armed: boolean;
105
+ already: boolean;
106
+ };
107
+ readSchedule: (job: string) => Schedule | null;
108
+ ledgerOf: (job: string) => string;
109
+ /**
110
+ * A failure the scheduler met on a job (a folder that went away, a file refused), written as ONE `refused` row per
111
+ * `tag`, read back from disk so a restart does not repeat it (T13). Returns whether it wrote.
112
+ * @param {string} job @param {string} sig @param {string} tag @param {string} why
113
+ */
114
+ tellOnce(job: string, sig: string, tag: string, why: string): boolean;
115
+ /** Click 1. All $0 checks; signs and writes nothing; returns `{words, hash, monthlyNote}`. @param {any} body */
116
+ prepare(body: any): {
117
+ status: number;
118
+ body: {
119
+ ok: boolean;
120
+ };
121
+ } | {
122
+ status: number;
123
+ body: {
124
+ ok: boolean;
125
+ job: any;
126
+ words: string | null;
127
+ hash: string;
128
+ monthlyNote: string | null;
129
+ cancel: boolean;
130
+ };
131
+ };
132
+ /** Click 2, carrying the hash it was shown. $0: no drafter, no run. @param {any} body */
133
+ set(body: any): {
134
+ status: number;
135
+ body: {
136
+ ok: boolean;
137
+ };
138
+ } | {
139
+ status: number;
140
+ body: {
141
+ ok: boolean;
142
+ refused: string;
143
+ say: "What you were shown is not what is on the card now. Nothing was signed. Look again, then sign.";
144
+ job?: undefined;
145
+ cancelled?: undefined;
146
+ words?: undefined;
147
+ };
148
+ } | {
149
+ status: number;
150
+ body: {
151
+ ok: boolean;
152
+ job: any;
153
+ cancelled: boolean;
154
+ words: "Cancel schedule";
155
+ refused?: undefined;
156
+ say?: undefined;
157
+ };
158
+ } | {
159
+ status: number;
160
+ body: {
161
+ say?: string | undefined;
162
+ ok: boolean;
163
+ job: any;
164
+ words: string | null;
165
+ hash: string;
166
+ armed: boolean;
167
+ refused?: undefined;
168
+ cancelled?: undefined;
169
+ };
170
+ };
171
+ /** Cancel a job's schedule, never refused. @param {string} job @returns {boolean} there was one */
172
+ cancel(job: string): boolean;
173
+ /** Every job with a schedule.json: `{job, dir, schedule}`. One readdir; nothing else read when there is none. */
174
+ scan(): {
175
+ job: string;
176
+ dir: string;
177
+ schedule: Schedule;
178
+ }[];
179
+ /**
180
+ * What a card shows of a job's schedule, as DATA (no words): the trigger, whether it is armed under the CURRENT
181
+ * signing, the next slot (clock kinds), the last outcome (rows under the current signing only, never a wall-clock
182
+ * compare, T19), whether the job has a green run, and the refusal sentence when it has none.
183
+ * @param {string} job
184
+ */
185
+ read(job: string): {
186
+ schedule: null;
187
+ trigger: null;
188
+ armed: boolean;
189
+ armedAt: null;
190
+ nextSlotMs: null;
191
+ lastOutcome: null;
192
+ retryInMs: null;
193
+ gaveUp: null;
194
+ job: string;
195
+ hasGreen: boolean;
196
+ refusal: string | null;
197
+ } | {
198
+ schedule: Schedule;
199
+ trigger: import("../trigger.js").Trigger;
200
+ armed: boolean;
201
+ armedAt: number | null | undefined;
202
+ nextSlotMs: number | null;
203
+ lastOutcome: import("../triggerledger.js").LedgerRow | null;
204
+ retryInMs: number | null;
205
+ gaveUp: import("../triggerledger.js").LedgerRow | null;
206
+ job: string;
207
+ hasGreen: boolean;
208
+ refusal: string | null;
209
+ } | null;
210
+ stateOf: (job: string) => {
211
+ schedule: null;
212
+ trigger: null;
213
+ armed: boolean;
214
+ armedAt: null;
215
+ nextSlotMs: null;
216
+ lastOutcome: null;
217
+ retryInMs: null;
218
+ gaveUp: null;
219
+ } | {
220
+ schedule: Schedule;
221
+ trigger: import("../trigger.js").Trigger;
222
+ armed: boolean;
223
+ armedAt: number | null | undefined;
224
+ nextSlotMs: number | null;
225
+ lastOutcome: import("../triggerledger.js").LedgerRow | null;
226
+ retryInMs: number | null;
227
+ gaveUp: import("../triggerledger.js").LedgerRow | null;
228
+ };
229
+ userHome: string;
230
+ /**
231
+ * What the Run tab shows of a job's schedule (A.12: one read of the schedule files and the ledger; the green gate is
232
+ * memoised per run-list state, so a poll never re-parses every run). The SERVER decides: `scheduled` shows Cancel /
233
+ * modify, else `canSchedule` shows Schedule run, else no button. `fromRunid` = the green run the card opens from. `waitingRunNo` = the job's newest run when it waits
234
+ * for the person's Accept (the next scheduled run does NOT wait for it, T7).
235
+ * @param {string} job @param {{waitingRunNo?: number|null}} [o]
236
+ */
237
+ view(job: string, o?: {
238
+ waitingRunNo?: number | null;
239
+ }): {
240
+ fromRunid?: string | undefined;
241
+ scheduled: boolean;
242
+ canSchedule: boolean;
243
+ words: Readonly<{
244
+ byHand: "By hand";
245
+ cancelSchedule: "Cancel schedule";
246
+ cancelWords: "Cancel schedule";
247
+ scheduleRun: "Schedule run";
248
+ cancelModify: "Cancel / modify schedule";
249
+ signSchedule: "Sign schedule";
250
+ nothingChanged: "Nothing changed yet. This card only sets or adjusts the schedule; it never runs the job.";
251
+ confirmPrefix: "Confirm: ";
252
+ whenLabel: "When";
253
+ nowPrefix: "Now: ";
254
+ scheduledLabel: "SCHEDULE";
255
+ nextLabel: "NEXT";
256
+ options: Readonly<{
257
+ hand: "By hand";
258
+ daily: "Every day at HH:MM";
259
+ weekdays: "Weekdays at HH:MM";
260
+ weekly: "Every <day> at HH:MM";
261
+ 'file-updated': "When <file> is updated";
262
+ 'new-file': "When a new file arrives in <folder>";
263
+ }>;
264
+ gaveUpPill: "Check";
265
+ problemNext: "next: fix it in Cancel / modify schedule";
266
+ monthNext: "next: raise the limit in Settings, or it starts on the 1st";
267
+ acceptWaiting: "waiting for your Accept (the next run does not wait)";
268
+ scheduleGroup: "schedule";
269
+ notGreen: "This job has not had a good run yet. Run it by hand and get it right first; then you can set when it runs. A rubric job also needs your Accept once. Nothing was changed.";
270
+ stale: "What you were shown is not what is on the card now. Nothing was signed. Look again, then sign.";
271
+ }>;
272
+ text?: undefined;
273
+ next?: undefined;
274
+ nextRed?: undefined;
275
+ lastRow?: undefined;
276
+ problem?: undefined;
277
+ nextWords?: undefined;
278
+ accept?: undefined;
279
+ } | {
280
+ scheduled: boolean;
281
+ canSchedule: boolean;
282
+ words: Readonly<{
283
+ byHand: "By hand";
284
+ cancelSchedule: "Cancel schedule";
285
+ cancelWords: "Cancel schedule";
286
+ scheduleRun: "Schedule run";
287
+ cancelModify: "Cancel / modify schedule";
288
+ signSchedule: "Sign schedule";
289
+ nothingChanged: "Nothing changed yet. This card only sets or adjusts the schedule; it never runs the job.";
290
+ confirmPrefix: "Confirm: ";
291
+ whenLabel: "When";
292
+ nowPrefix: "Now: ";
293
+ scheduledLabel: "SCHEDULE";
294
+ nextLabel: "NEXT";
295
+ options: Readonly<{
296
+ hand: "By hand";
297
+ daily: "Every day at HH:MM";
298
+ weekdays: "Weekdays at HH:MM";
299
+ weekly: "Every <day> at HH:MM";
300
+ 'file-updated': "When <file> is updated";
301
+ 'new-file': "When a new file arrives in <folder>";
302
+ }>;
303
+ gaveUpPill: "Check";
304
+ problemNext: "next: fix it in Cancel / modify schedule";
305
+ monthNext: "next: raise the limit in Settings, or it starts on the 1st";
306
+ acceptWaiting: "waiting for your Accept (the next run does not wait)";
307
+ scheduleGroup: "schedule";
308
+ notGreen: "This job has not had a good run yet. Run it by hand and get it right first; then you can set when it runs. A rubric job also needs your Accept once. Nothing was changed.";
309
+ stale: "What you were shown is not what is on the card now. Nothing was signed. Look again, then sign.";
310
+ }>;
311
+ fromRunid: string;
312
+ text: string | null;
313
+ next: string;
314
+ nextRed: boolean;
315
+ lastRow: import("../triggerledger.js").LedgerRow | null;
316
+ problem: boolean;
317
+ nextWords: string;
318
+ accept: boolean;
319
+ } | null;
320
+ /**
321
+ * The schedule card's data for `start-from?mode=schedule`: the current schedule in the When field's own terms, the
322
+ * words, the refusal that disables the field (a job with no good run and no schedule), and the edit note.
323
+ * Cancel is never refused, so a scheduled job is never disabled here.
324
+ * @param {string} job
325
+ */
326
+ cardFor(job: string): {
327
+ scheduled: boolean;
328
+ trigger: import("../trigger.js").Trigger | null;
329
+ now: string | null;
330
+ refusal: string | null;
331
+ words: Readonly<{
332
+ byHand: "By hand";
333
+ cancelSchedule: "Cancel schedule";
334
+ cancelWords: "Cancel schedule";
335
+ scheduleRun: "Schedule run";
336
+ cancelModify: "Cancel / modify schedule";
337
+ signSchedule: "Sign schedule";
338
+ nothingChanged: "Nothing changed yet. This card only sets or adjusts the schedule; it never runs the job.";
339
+ confirmPrefix: "Confirm: ";
340
+ whenLabel: "When";
341
+ nowPrefix: "Now: ";
342
+ scheduledLabel: "SCHEDULE";
343
+ nextLabel: "NEXT";
344
+ options: Readonly<{
345
+ hand: "By hand";
346
+ daily: "Every day at HH:MM";
347
+ weekdays: "Weekdays at HH:MM";
348
+ weekly: "Every <day> at HH:MM";
349
+ 'file-updated': "When <file> is updated";
350
+ 'new-file': "When a new file arrives in <folder>";
351
+ }>;
352
+ gaveUpPill: "Check";
353
+ problemNext: "next: fix it in Cancel / modify schedule";
354
+ monthNext: "next: raise the limit in Settings, or it starts on the 1st";
355
+ acceptWaiting: "waiting for your Accept (the next run does not wait)";
356
+ scheduleGroup: "schedule";
357
+ notGreen: "This job has not had a good run yet. Run it by hand and get it right first; then you can set when it runs. A rubric job also needs your Accept once. Nothing was changed.";
358
+ stale: "What you were shown is not what is on the card now. Nothing was signed. Look again, then sign.";
359
+ }>;
360
+ };
361
+ /**
362
+ * RULED 2026-10-15: the jobs whose schedule gave up, for the runs list's Needs you section (the SERVER decides; the page works out nothing):
363
+ * `[{job, pill: 'Check', words, at: 'MM/DD HH:MM', runId}]`, newest give-up first. `runId` = the job's HEAD run by time. Never ages out.
364
+ * @param {Map<string, string|null>|Record<string, string|null>} heads
365
+ */
366
+ listGaveUp(heads: Map<string, string | null> | Record<string, string | null>): {
367
+ job: string;
368
+ pill: "Check";
369
+ words: string;
370
+ at: string;
371
+ runId: string | null;
372
+ }[];
373
+ /**
374
+ * The Scheduled section of the runs list: `[{job, at, in, words, runId}]`, soonest first; a file trigger (no clock
375
+ * time) after those that have one, by name. `heads` = job -> its HEAD run id by time (summarizeJobs), so the click
376
+ * opens the job's newest run, never "the first in server order". A job that cannot be read is absent.
377
+ * @param {Map<string, string|null>|Record<string, string|null>} heads
378
+ */
379
+ listScheduled(heads: Map<string, string | null> | Record<string, string | null>): {
380
+ job: string;
381
+ at: string | null;
382
+ in: string | null;
383
+ words: string | null;
384
+ runId: string | null;
385
+ }[];
386
+ };
387
+ /** a spawn error after the consuming row releases it at most this many times; then it stays consumed (bounded rows) */
388
+ export const MAX_RELEASES: 3;
389
+ /** a live pid whose process started more than this after its run row was written is a RECYCLED pid, not that run */
390
+ export const PID_REUSE_SLACK_MS: 5000;
391
+ /** RULED 2026-10-15: waits (ms) between consecutive `install-needed` refusals of a job: Fibonacci 1, 2, 3, 5, 8, 13, 21 minutes. A test injects its own via `deps.installRetrySteps`. */
392
+ export const INSTALL_RETRY_STEPS_MS: readonly number[];
393
+ /** RULED 2026-10-15: the total wait a slot or file gets. A wait that would pass it is not taken: the schedule gives up and pauses (the job waits in Needs you). */
394
+ export const INSTALL_GIVE_UP_MS: number;
395
+ export type JobRun = {
396
+ runid: string;
397
+ outcome: string | null;
398
+ spec: any;
399
+ records: any[];
400
+ };
401
+ export type Schedule = {
402
+ job: string;
403
+ trigger: import("../trigger.js").Trigger;
404
+ spec: any;
405
+ specHash: string;
406
+ workflowKey: string;
407
+ originRunid: string;
408
+ inputsText: string;
409
+ destination?: string;
410
+ signedBy: string;
411
+ signedAt: string;
412
+ };