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.
- package/CHANGELOG.md +46 -0
- package/bareloop.context.md +53 -30
- package/package.json +1 -1
- package/src/authorjob.js +6 -0
- package/src/declaredclose.js +2 -2
- package/src/interviewrun.js +0 -1
- package/src/job.js +38 -11
- package/src/panel/authorroutes.js +107 -33
- package/src/panel/authorsession.js +59 -27
- package/src/panel/index.html +813 -484
- package/src/panel/runroutes.js +163 -5
- package/src/panel/scheduler.js +156 -0
- package/src/panel/schedules.js +667 -0
- package/src/panel/schedulestart.js +243 -0
- package/src/panel/server.js +451 -48
- package/src/planrun.js +39 -7
- package/src/providerrows.js +3 -3
- package/src/reviewdoor.js +37 -0
- package/src/runlist.js +35 -0
- package/src/schedulewords.js +296 -0
- package/src/trigger.js +48 -0
- package/src/triggerclock.js +127 -0
- package/src/triggerfiles.js +215 -0
- package/src/triggerledger.js +79 -0
- package/src/userrun.js +6 -7
- package/types/declaredclose.d.ts +1 -1
- package/types/job.d.ts +8 -1
- package/types/panel/authorroutes.d.ts +34 -1
- package/types/panel/authorsession.d.ts +37 -12
- package/types/panel/runroutes.d.ts +32 -1
- package/types/panel/scheduler.d.ts +17 -0
- package/types/panel/schedules.d.ts +412 -0
- package/types/panel/schedulestart.d.ts +106 -0
- package/types/panel/server.d.ts +143 -14
- package/types/planrun.d.ts +2 -0
- package/types/providerrows.d.ts +2 -2
- package/types/reviewdoor.d.ts +27 -0
- package/types/runlist.d.ts +20 -0
- package/types/schedulewords.d.ts +135 -0
- package/types/trigger.d.ts +37 -0
- package/types/triggerclock.d.ts +52 -0
- package/types/triggerfiles.d.ts +93 -0
- 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
|
-
*
|
|
72
|
-
*
|
|
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 {
|
|
87
|
+
* @returns {boolean}
|
|
76
88
|
*/
|
|
77
|
-
export function
|
|
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 (
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
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
|
|
204
|
-
/**
|
|
205
|
-
export const
|
|
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)
|
|
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
|
+
};
|