@ai-dossier/sched 0.2.0 → 0.2.1

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 (53) hide show
  1. package/dist/dispatch.d.ts +103 -0
  2. package/dist/dispatch.d.ts.map +1 -0
  3. package/dist/dispatch.js +262 -0
  4. package/dist/dispatch.js.map +1 -0
  5. package/dist/engine.d.ts +71 -0
  6. package/dist/engine.d.ts.map +1 -0
  7. package/dist/engine.js +590 -0
  8. package/dist/engine.js.map +1 -0
  9. package/dist/enqueue.d.ts +38 -0
  10. package/dist/enqueue.d.ts.map +1 -0
  11. package/dist/enqueue.js +220 -0
  12. package/dist/enqueue.js.map +1 -0
  13. package/dist/groundtruth.d.ts +74 -0
  14. package/dist/groundtruth.d.ts.map +1 -0
  15. package/dist/groundtruth.js +114 -0
  16. package/dist/groundtruth.js.map +1 -0
  17. package/dist/index.d.ts +13 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +86 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/journal.d.ts +32 -0
  22. package/dist/journal.d.ts.map +1 -0
  23. package/dist/journal.js +113 -0
  24. package/dist/journal.js.map +1 -0
  25. package/dist/persist.d.ts +54 -0
  26. package/dist/persist.d.ts.map +1 -0
  27. package/dist/persist.js +334 -0
  28. package/dist/persist.js.map +1 -0
  29. package/dist/project.d.ts +41 -0
  30. package/dist/project.d.ts.map +1 -0
  31. package/dist/project.js +124 -0
  32. package/dist/project.js.map +1 -0
  33. package/dist/readiness.d.ts +43 -0
  34. package/dist/readiness.d.ts.map +1 -0
  35. package/dist/readiness.js +102 -0
  36. package/dist/readiness.js.map +1 -0
  37. package/dist/scheduler.d.ts +66 -0
  38. package/dist/scheduler.d.ts.map +1 -0
  39. package/dist/scheduler.js +174 -0
  40. package/dist/scheduler.js.map +1 -0
  41. package/dist/state.d.ts +39 -0
  42. package/dist/state.d.ts.map +1 -0
  43. package/dist/state.js +360 -0
  44. package/dist/state.js.map +1 -0
  45. package/dist/status.d.ts +32 -0
  46. package/dist/status.d.ts.map +1 -0
  47. package/dist/status.js +76 -0
  48. package/dist/status.js.map +1 -0
  49. package/dist/types.d.ts +214 -0
  50. package/dist/types.d.ts.map +1 -0
  51. package/dist/types.js +87 -0
  52. package/dist/types.js.map +1 -0
  53. package/package.json +2 -2
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Agent dispatch machinery (#464, AC1 — "a runnable queue unit is dispatched
3
+ * as a spawned agent process … with `--model` per its tier; pid + phase +
4
+ * last-progress timestamp tracked in state.json"; RFC-0001 §C.1 "spawns agent
5
+ * processes via the existing `run` machinery").
6
+ *
7
+ * The command is a template (default `claude -p --output-format json --model
8
+ * {model}` — the same headless invocation `ai-dossier run` builds in
9
+ * cli/src/helpers.ts) with `{model}`/`{issue}` placeholders; the prompt
10
+ * travels on stdin exactly like the run machinery's headless path. Children
11
+ * spawn DETACHED and unref'd: an agent must survive a sched crash (RFC F.10 —
12
+ * restart reconciles by pid, it never owns the agent's lifetime).
13
+ *
14
+ * All process I/O is injectable (`SpawnDeps`) so tests spawn fake agents, and
15
+ * the package itself never invokes an LLM — it spawns a process the operator
16
+ * configured.
17
+ */
18
+ import { type ModelTier, type SchedConfig } from './types';
19
+ /** Default tier → model mapping (claude aliases; override in config.json). */
20
+ export declare const DEFAULT_TIER_MODELS: Readonly<Record<ModelTier, string>>;
21
+ /** Default headless agent command template (claude, the run machinery's first choice). */
22
+ export declare const DEFAULT_DISPATCH_COMMAND: readonly string[];
23
+ /** opencode equivalent (the run machinery's second agent, #459). */
24
+ export declare const OPENCODE_DISPATCH_COMMAND: readonly string[];
25
+ /** Default prompt sent on the child's stdin. */
26
+ export declare const DEFAULT_PROMPT_TEMPLATE: string;
27
+ /** Fully-resolved dispatch settings the engine runs with. */
28
+ export interface ResolvedDispatch {
29
+ /** Command template with `{model}`/`{issue}` placeholders. */
30
+ command: string[];
31
+ /** Prompt template with `{issue}` placeholder. */
32
+ prompt: string;
33
+ /** Model per tier; null means "no model flag" (the command's `--model {model}` pair drops). */
34
+ tierModels: Record<ModelTier, string | null>;
35
+ stallTimeoutMs: number;
36
+ reconcileIntervalMs: number;
37
+ }
38
+ /** Resolve engine dispatch settings from the (possibly sparse) config. */
39
+ export declare function resolveDispatch(config: SchedConfig): ResolvedDispatch;
40
+ /**
41
+ * Build the concrete agent argv for one dispatch: substitute `{issue}` and
42
+ * `{model}`. When the tier's model is null, the `{model}` item AND its
43
+ * immediately-preceding flag (e.g. `--model`) drop together — a command never
44
+ * carries a flag whose value is missing.
45
+ */
46
+ export declare function buildAgentCommand(template: readonly string[], tier: ModelTier, issue: number, tierModels: Readonly<Record<ModelTier, string | null>>): string[];
47
+ /** Build the child's stdin prompt for one dispatch. */
48
+ export declare function buildPrompt(template: string, issue: number): string;
49
+ /** One tier stronger on the ladder, or null at the top (RFC-0001 §C.1). */
50
+ export declare function escalateTier(tier: ModelTier): ModelTier | null;
51
+ export interface SpawnDeps {
52
+ /**
53
+ * Spawn a detached agent process and return its pid. `logFile` receives the
54
+ * child's combined stdout/stderr (agents outlive sched, so their output
55
+ * cannot stay in this process's pipes). Throws synchronously when the
56
+ * process cannot be spawned (missing binary, unwritable log dir).
57
+ */
58
+ spawn(cmd: string[], prompt: string, logFile: string): number;
59
+ /**
60
+ * Signal a pid; returns false when it was already dead (or not ours).
61
+ * `expectedStart` (the persisted `/proc` start-time) enables the pid-reuse
62
+ * guard: a pid whose start-time no longer matches was reused by an
63
+ * unrelated process and is never signalled.
64
+ */
65
+ kill(pid: number, expectedStart?: number): boolean;
66
+ /**
67
+ * Whether a pid is alive (best-effort). `expectedStart` applies the same
68
+ * pid-reuse guard as `kill`.
69
+ */
70
+ isAlive(pid: number, expectedStart?: number): boolean;
71
+ /**
72
+ * `/proc/<pid>/stat` start-time (field 22) of a pid, when the platform
73
+ * exposes it (Linux); null elsewhere. The engine persists this at spawn so
74
+ * pid identity survives engine restarts (decision 1, option C).
75
+ */
76
+ processStart(pid: number): number | null;
77
+ }
78
+ /** `issue:464` → `issue-464` (filesystem-safe unit ids for log file names). */
79
+ export declare function unitLogName(unit: string): string;
80
+ /** Poll cadence bounds for `sleep`'s stop-check interval (engine loop). */
81
+ export declare const STOP_POLL_MIN_MS = 100;
82
+ export declare const STOP_POLL_MAX_MS = 1000;
83
+ /**
84
+ * `/proc/<pid>/stat` start-time (field 22, clock ticks since boot) — stable
85
+ * process identity for the lifetime of the pid, so a recycled pid is
86
+ * detectable. Null when /proc is unavailable (macOS/Windows) or the process
87
+ * is gone.
88
+ */
89
+ export declare function procStartTime(pid: number): number | null;
90
+ /**
91
+ * Real process I/O: detached spawn with output to a log file, unref'd.
92
+ *
93
+ * Pid-reuse guard (decision 1, option C — hybrid): every pid this instance
94
+ * spawns is recorded with its `/proc` start-time; `kill`/`isAlive` accept the
95
+ * start-time persisted in state.json and refuse a pid whose current
96
+ * start-time no longer matches (it was reused by an unrelated process — the
97
+ * agent we spawned is already dead, so skipping the signal loses nothing).
98
+ * On platforms without /proc the guard degrades to best-effort, and pids
99
+ * without a recorded start-time (e.g. from pre-decision state files) stay
100
+ * best-effort everywhere.
101
+ */
102
+ export declare function createSpawnDeps(cwd?: string): SpawnDeps;
103
+ //# sourceMappingURL=dispatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,EAIL,KAAK,SAAS,EACd,KAAK,WAAW,EAEjB,MAAM,SAAS,CAAC;AAEjB,8EAA8E;AAC9E,eAAO,MAAM,mBAAmB,EAAE,QAAQ,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAInE,CAAC;AAEF,0FAA0F;AAC1F,eAAO,MAAM,wBAAwB,EAAE,SAAS,MAAM,EAOrD,CAAC;AAEF,oEAAoE;AACpE,eAAO,MAAM,yBAAyB,EAAE,SAAS,MAAM,EAOtD,CAAC;AAEF,gDAAgD;AAChD,eAAO,MAAM,uBAAuB,QAKwB,CAAC;AAE7D,6DAA6D;AAC7D,MAAM,WAAW,gBAAgB;IAC/B,8DAA8D;IAC9D,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,kDAAkD;IAClD,MAAM,EAAE,MAAM,CAAC;IACf,+FAA+F;IAC/F,UAAU,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,cAAc,EAAE,MAAM,CAAC;IACvB,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAED,0EAA0E;AAC1E,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,gBAAgB,CAcrE;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,SAAS,MAAM,EAAE,EAC3B,IAAI,EAAE,SAAS,EACf,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,GACrD,MAAM,EAAE,CAgBV;AAED,uDAAuD;AACvD,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnE;AAED,2EAA2E;AAC3E,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,IAAI,CAE9D;AAID,MAAM,WAAW,SAAS;IACxB;;;;;OAKG;IACH,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9D;;;;;OAKG;IACH,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACnD;;;OAGG;IACH,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACtD;;;;OAIG;IACH,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;CAC1C;AAED,+EAA+E;AAC/E,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,2EAA2E;AAC3E,eAAO,MAAM,gBAAgB,MAAM,CAAC;AACpC,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAcxD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAyEvD"}
@@ -0,0 +1,262 @@
1
+ "use strict";
2
+ /**
3
+ * Agent dispatch machinery (#464, AC1 — "a runnable queue unit is dispatched
4
+ * as a spawned agent process … with `--model` per its tier; pid + phase +
5
+ * last-progress timestamp tracked in state.json"; RFC-0001 §C.1 "spawns agent
6
+ * processes via the existing `run` machinery").
7
+ *
8
+ * The command is a template (default `claude -p --output-format json --model
9
+ * {model}` — the same headless invocation `ai-dossier run` builds in
10
+ * cli/src/helpers.ts) with `{model}`/`{issue}` placeholders; the prompt
11
+ * travels on stdin exactly like the run machinery's headless path. Children
12
+ * spawn DETACHED and unref'd: an agent must survive a sched crash (RFC F.10 —
13
+ * restart reconciles by pid, it never owns the agent's lifetime).
14
+ *
15
+ * All process I/O is injectable (`SpawnDeps`) so tests spawn fake agents, and
16
+ * the package itself never invokes an LLM — it spawns a process the operator
17
+ * configured.
18
+ */
19
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ var desc = Object.getOwnPropertyDescriptor(m, k);
22
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
23
+ desc = { enumerable: true, get: function() { return m[k]; } };
24
+ }
25
+ Object.defineProperty(o, k2, desc);
26
+ }) : (function(o, m, k, k2) {
27
+ if (k2 === undefined) k2 = k;
28
+ o[k2] = m[k];
29
+ }));
30
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
31
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
32
+ }) : function(o, v) {
33
+ o["default"] = v;
34
+ });
35
+ var __importStar = (this && this.__importStar) || (function () {
36
+ var ownKeys = function(o) {
37
+ ownKeys = Object.getOwnPropertyNames || function (o) {
38
+ var ar = [];
39
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
40
+ return ar;
41
+ };
42
+ return ownKeys(o);
43
+ };
44
+ return function (mod) {
45
+ if (mod && mod.__esModule) return mod;
46
+ var result = {};
47
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
48
+ __setModuleDefault(result, mod);
49
+ return result;
50
+ };
51
+ })();
52
+ Object.defineProperty(exports, "__esModule", { value: true });
53
+ exports.STOP_POLL_MAX_MS = exports.STOP_POLL_MIN_MS = exports.DEFAULT_PROMPT_TEMPLATE = exports.OPENCODE_DISPATCH_COMMAND = exports.DEFAULT_DISPATCH_COMMAND = exports.DEFAULT_TIER_MODELS = void 0;
54
+ exports.resolveDispatch = resolveDispatch;
55
+ exports.buildAgentCommand = buildAgentCommand;
56
+ exports.buildPrompt = buildPrompt;
57
+ exports.escalateTier = escalateTier;
58
+ exports.unitLogName = unitLogName;
59
+ exports.procStartTime = procStartTime;
60
+ exports.createSpawnDeps = createSpawnDeps;
61
+ const node_child_process_1 = require("node:child_process");
62
+ const fs = __importStar(require("node:fs"));
63
+ const path = __importStar(require("node:path"));
64
+ const project_1 = require("./project");
65
+ const types_1 = require("./types");
66
+ /** Default tier → model mapping (claude aliases; override in config.json). */
67
+ exports.DEFAULT_TIER_MODELS = {
68
+ mechanical: 'haiku',
69
+ mid: 'sonnet',
70
+ strong: 'opus',
71
+ };
72
+ /** Default headless agent command template (claude, the run machinery's first choice). */
73
+ exports.DEFAULT_DISPATCH_COMMAND = [
74
+ 'claude',
75
+ '-p',
76
+ '--output-format',
77
+ 'json',
78
+ '--model',
79
+ '{model}',
80
+ ];
81
+ /** opencode equivalent (the run machinery's second agent, #459). */
82
+ exports.OPENCODE_DISPATCH_COMMAND = [
83
+ 'opencode',
84
+ 'run',
85
+ '--format',
86
+ 'json',
87
+ '--model',
88
+ '{model}',
89
+ ];
90
+ /** Default prompt sent on the child's stdin. */
91
+ exports.DEFAULT_PROMPT_TEMPLATE = 'Run the full-cycle-issue workflow for GitHub issue #{issue} in this repository.\n\n' +
92
+ 'Begin by fetching the workflow: ai-dossier run imboard-ai/git/full-cycle-issue --pull\n\n' +
93
+ 'Then execute it end-to-end for issue #{issue}, following every phase ' +
94
+ '(gate, setup, plan, implement, review, ship, report) without asking questions, ' +
95
+ 'until the final report milestone is posted on the issue.';
96
+ /** Resolve engine dispatch settings from the (possibly sparse) config. */
97
+ function resolveDispatch(config) {
98
+ const dispatch = config.dispatch ?? {};
99
+ const tierModels = {
100
+ mechanical: dispatch.tier_models?.mechanical ?? exports.DEFAULT_TIER_MODELS.mechanical,
101
+ mid: dispatch.tier_models?.mid ?? exports.DEFAULT_TIER_MODELS.mid,
102
+ strong: dispatch.tier_models?.strong ?? exports.DEFAULT_TIER_MODELS.strong,
103
+ };
104
+ return {
105
+ command: dispatch.command ?? [...exports.DEFAULT_DISPATCH_COMMAND],
106
+ prompt: dispatch.prompt ?? exports.DEFAULT_PROMPT_TEMPLATE,
107
+ tierModels,
108
+ stallTimeoutMs: config.stall_timeout_ms ?? types_1.DEFAULT_STALL_TIMEOUT_MS,
109
+ reconcileIntervalMs: config.reconcile_interval_ms ?? types_1.DEFAULT_RECONCILE_INTERVAL_MS,
110
+ };
111
+ }
112
+ /**
113
+ * Build the concrete agent argv for one dispatch: substitute `{issue}` and
114
+ * `{model}`. When the tier's model is null, the `{model}` item AND its
115
+ * immediately-preceding flag (e.g. `--model`) drop together — a command never
116
+ * carries a flag whose value is missing.
117
+ */
118
+ function buildAgentCommand(template, tier, issue, tierModels) {
119
+ const model = tierModels[tier] ?? null;
120
+ const argv = [];
121
+ for (const item of template) {
122
+ if (item === '{model}') {
123
+ if (model === null) {
124
+ // Drop the preceding flag along with the placeholder.
125
+ if (argv.length > 0 && argv[argv.length - 1].startsWith('--'))
126
+ argv.pop();
127
+ continue;
128
+ }
129
+ argv.push(model);
130
+ continue;
131
+ }
132
+ argv.push(item.replaceAll('{issue}', String(issue)));
133
+ }
134
+ return argv;
135
+ }
136
+ /** Build the child's stdin prompt for one dispatch. */
137
+ function buildPrompt(template, issue) {
138
+ return template.replaceAll('{issue}', String(issue));
139
+ }
140
+ /** One tier stronger on the ladder, or null at the top (RFC-0001 §C.1). */
141
+ function escalateTier(tier) {
142
+ return types_1.TIER_LADDER[tier];
143
+ }
144
+ /** `issue:464` → `issue-464` (filesystem-safe unit ids for log file names). */
145
+ function unitLogName(unit) {
146
+ return (0, project_1.sanitizeSlug)(unit);
147
+ }
148
+ /** Poll cadence bounds for `sleep`'s stop-check interval (engine loop). */
149
+ exports.STOP_POLL_MIN_MS = 100;
150
+ exports.STOP_POLL_MAX_MS = 1000;
151
+ /**
152
+ * `/proc/<pid>/stat` start-time (field 22, clock ticks since boot) — stable
153
+ * process identity for the lifetime of the pid, so a recycled pid is
154
+ * detectable. Null when /proc is unavailable (macOS/Windows) or the process
155
+ * is gone.
156
+ */
157
+ function procStartTime(pid) {
158
+ try {
159
+ const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf-8');
160
+ // comm (field 2) is parenthesized and may contain spaces — everything
161
+ // after the LAST ')' is fields 3..N, space-separated.
162
+ const close = stat.lastIndexOf(')');
163
+ if (close === -1)
164
+ return null;
165
+ const fields = stat.slice(close + 2).split(' ');
166
+ // field 22 (starttime) -> index 19 in the post-comm array (field 3 = index 0)
167
+ const start = Number.parseInt(fields[19] ?? '', 10);
168
+ return Number.isInteger(start) && start >= 0 ? start : null;
169
+ }
170
+ catch {
171
+ return null;
172
+ }
173
+ }
174
+ /**
175
+ * Real process I/O: detached spawn with output to a log file, unref'd.
176
+ *
177
+ * Pid-reuse guard (decision 1, option C — hybrid): every pid this instance
178
+ * spawns is recorded with its `/proc` start-time; `kill`/`isAlive` accept the
179
+ * start-time persisted in state.json and refuse a pid whose current
180
+ * start-time no longer matches (it was reused by an unrelated process — the
181
+ * agent we spawned is already dead, so skipping the signal loses nothing).
182
+ * On platforms without /proc the guard degrades to best-effort, and pids
183
+ * without a recorded start-time (e.g. from pre-decision state files) stay
184
+ * best-effort everywhere.
185
+ */
186
+ function createSpawnDeps(cwd) {
187
+ /** Pids spawned by THIS instance -> their /proc start-time (Linux). */
188
+ const spawnedStarts = new Map();
189
+ /** True when `pid` plausibly still names the process we recorded. */
190
+ function matchesRecordedStart(pid, expectedStart) {
191
+ const expected = expectedStart ?? spawnedStarts.get(pid);
192
+ if (expected === undefined)
193
+ return true; // no recorded identity — best-effort
194
+ const current = procStartTime(pid);
195
+ if (current === null)
196
+ return true; // /proc unavailable (non-Linux) or a race — best-effort
197
+ return current === expected;
198
+ }
199
+ return {
200
+ spawn(cmd, prompt, logFile) {
201
+ fs.mkdirSync(path.dirname(logFile), { recursive: true, mode: 0o700 });
202
+ const out = fs.openSync(logFile, 'a', 0o600);
203
+ try {
204
+ const child = (0, node_child_process_1.spawn)(cmd[0], cmd.slice(1), {
205
+ ...(cwd ? { cwd } : {}),
206
+ detached: true,
207
+ stdio: ['pipe', out, out],
208
+ });
209
+ // Spawn failures surface synchronously via the pid check below; the
210
+ // async 'error' event must never crash the engine (ENOENT), and an
211
+ // agent exiting before reading stdin must never crash it either (EPIPE).
212
+ child.on('error', (err) => {
213
+ process.stderr.write(`⚠ sched: spawn '${cmd[0]}' failed: ${err.message}\n`);
214
+ });
215
+ if (child.stdin !== null) {
216
+ child.stdin.on('error', () => { });
217
+ child.stdin.write(prompt);
218
+ child.stdin.end();
219
+ }
220
+ child.unref();
221
+ if (child.pid === undefined) {
222
+ throw new Error(`failed to spawn '${cmd[0]}' — is it on PATH? (command: ${cmd.join(' ')})`);
223
+ }
224
+ const start = procStartTime(child.pid);
225
+ if (start !== null)
226
+ spawnedStarts.set(child.pid, start);
227
+ return child.pid;
228
+ }
229
+ finally {
230
+ // The child holds its own dups of the fd; the parent's copy must close.
231
+ fs.closeSync(out);
232
+ }
233
+ },
234
+ kill(pid, expectedStart) {
235
+ if (!matchesRecordedStart(pid, expectedStart)) {
236
+ spawnedStarts.delete(pid);
237
+ return false; // reused pid — the agent we spawned is already gone
238
+ }
239
+ try {
240
+ process.kill(pid, 'SIGTERM');
241
+ return true;
242
+ }
243
+ catch {
244
+ spawnedStarts.delete(pid);
245
+ return false;
246
+ }
247
+ },
248
+ isAlive(pid, expectedStart) {
249
+ try {
250
+ process.kill(pid, 0);
251
+ return matchesRecordedStart(pid, expectedStart);
252
+ }
253
+ catch (err) {
254
+ return err.code === 'EPERM';
255
+ }
256
+ },
257
+ processStart(pid) {
258
+ return procStartTime(pid);
259
+ },
260
+ };
261
+ }
262
+ //# sourceMappingURL=dispatch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatch.js","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The scheduler engine (#464): dispatch, completion verification, and the
3
+ * stall/escalation ladder (RFC-0001 §C.1) — the deterministic organs that
4
+ * replace fleet-cycle's LLM supervision.
5
+ *
6
+ * One `tick()` is a full reconcile+refill cycle:
7
+ *
8
+ * 1. **Poll** ground truth for every live unit OUTSIDE the state lock (gh/git
9
+ * subprocesses are slow; the lock must never wait on a network call).
10
+ * 2. **Apply** under `SchedStore.withLock`:
11
+ * - `assigned` slots (crash between assign and spawn) are spawned/re-attached
12
+ * - `running` slots: dead pid → exit rail; ground truth says complete →
13
+ * external advance (kill the leftover agent, complete); new milestone or
14
+ * pushed commit → progress; no progress for `stall_timeout_ms` →
15
+ * redispatch one tier stronger (cap 2, then failed)
16
+ * - `exited`/`verifying` slots: the agent exited — completion is verified
17
+ * against ground truth, never assumed (AC2); unverified exits ride the
18
+ * same recovery ladder as stalls
19
+ * - `recovering` slots: respawned with the escalated tier (resume rails —
20
+ * the agent re-enters via gate and resumes from the milestone trail)
21
+ * - failures block their TRANSITIVE dependents (AC4)
22
+ * 3. **Refill**: `computeAssignments` fills every freed slot in the SAME tick —
23
+ * a runnable unit never waits while a slot is idle (AC5).
24
+ *
25
+ * Everything that touches the world (processes, GitHub, git) is injected;
26
+ * the state machine is pure. Only `issue:<n>` units are dispatched — batch
27
+ * member sequencing is a follow-up (#464 non-goal).
28
+ */
29
+ import { type SpawnDeps } from './dispatch';
30
+ import { type GroundTruth } from './groundtruth';
31
+ import { type Journal } from './journal';
32
+ import type { SchedStore } from './persist';
33
+ import { type SchedConfig } from './types';
34
+ export interface EngineDeps {
35
+ store: SchedStore;
36
+ journal: Journal;
37
+ groundTruth: GroundTruth;
38
+ spawnDeps: SpawnDeps;
39
+ now: () => Date;
40
+ }
41
+ /** What one tick did — surfaced by `sched start`. */
42
+ export interface TickResult {
43
+ /** Units spawned this tick (first dispatch or redispatch). */
44
+ spawned: string[];
45
+ /** Units completed by external ground truth while their agent was still alive. */
46
+ externalAdvances: string[];
47
+ /** Units verified complete after an observed exit. */
48
+ completed: string[];
49
+ /** Units redispatched one tier stronger (stall or unverified exit). */
50
+ redispatched: string[];
51
+ /** Units failed (escalation cap / strongest tier / spawn error). */
52
+ failed: string[];
53
+ /** Issues blocked transitively by a failure. */
54
+ blocked: number[];
55
+ }
56
+ /**
57
+ * One full reconcile+refill cycle. Ground truth is polled WITHOUT the lock;
58
+ * every state mutation happens under `store.withLock`; refills are computed in
59
+ * the same pass, so a freed slot is reused within this tick (AC5).
60
+ */
61
+ export declare function tick(deps: EngineDeps, config: SchedConfig): TickResult;
62
+ /**
63
+ * The long-running loop behind `sched start`: tick, sleep, repeat. Returns
64
+ * when `shouldStop()` says so (the CLI wires SIGINT). A failed tick is
65
+ * journaled (`tick-failed`, with the error name — `LockTimeoutError` and
66
+ * `CorruptStateError` demand different operator actions) and reported on
67
+ * stderr, and the loop continues — one bad tick (e.g. a transient gh
68
+ * failure) never stops the scheduler.
69
+ */
70
+ export declare function runLoop(deps: EngineDeps, config: SchedConfig, shouldStop: () => boolean, onTick?: (result: TickResult) => void): Promise<void>;
71
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAGH,OAAO,EAML,KAAK,SAAS,EAIf,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,KAAK,WAAW,EAAiD,MAAM,eAAe,CAAC;AAChG,OAAO,EAAe,KAAK,OAAO,EAAa,MAAM,WAAW,CAAC;AACjE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAG5C,OAAO,EAGL,KAAK,WAAW,EAKjB,MAAM,SAAS,CAAC;AAEjB,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,UAAU,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,WAAW,CAAC;IACzB,SAAS,EAAE,SAAS,CAAC;IACrB,GAAG,EAAE,MAAM,IAAI,CAAC;CACjB;AAED,qDAAqD;AACrD,MAAM,WAAW,UAAU;IACzB,8DAA8D;IAC9D,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,kFAAkF;IAClF,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,sDAAsD;IACtD,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,uEAAuE;IACvE,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,oEAAoE;IACpE,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,gDAAgD;IAChD,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAqkBD;;;;GAIG;AACH,wBAAgB,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,GAAG,UAAU,CAWtE;AAED;;;;;;;GAOG;AACH,wBAAsB,OAAO,CAC3B,IAAI,EAAE,UAAU,EAChB,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,MAAM,OAAO,EACzB,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,GACpC,OAAO,CAAC,IAAI,CAAC,CAcf"}