@h-rig/supervisor-plugin 0.0.6-alpha.154 → 0.0.6-alpha.156
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/dist/src/cli.js +263 -5
- package/dist/src/closureStage.d.ts +2 -1
- package/dist/src/closureStage.js +10 -8
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +379 -89
- package/dist/src/journal.d.ts +8 -0
- package/dist/src/journal.js +75 -2
- package/dist/src/loop.d.ts +61 -0
- package/dist/src/loop.js +265 -0
- package/dist/src/panel.d.ts +7 -0
- package/dist/src/panel.js +23 -0
- package/dist/src/plugin.d.ts +2 -2
- package/dist/src/plugin.js +306 -22
- package/dist/src/supervisor.d.ts +1 -1
- package/dist/src/supervisor.js +2 -2
- package/package.json +7 -4
package/dist/src/index.js
CHANGED
|
@@ -81,18 +81,329 @@ function createDefaultSupervisorClosureStage() {
|
|
|
81
81
|
return { kind: "continue", ctx };
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
|
+
var supervisorClosureStage = {
|
|
85
|
+
id: SUPERVISOR_CLOSURE_STAGE_ID,
|
|
86
|
+
kind: "observe",
|
|
87
|
+
after: ["source-closeout"],
|
|
88
|
+
before: ["journal-append"],
|
|
89
|
+
priority: 0,
|
|
90
|
+
protected: false
|
|
91
|
+
};
|
|
84
92
|
var supervisorClosureStageMutation = {
|
|
85
93
|
op: "insert",
|
|
86
94
|
contributedBy: "@rig/supervisor-plugin",
|
|
87
|
-
stage:
|
|
88
|
-
id: SUPERVISOR_CLOSURE_STAGE_ID,
|
|
89
|
-
kind: "observe",
|
|
90
|
-
after: ["source-closeout"],
|
|
91
|
-
before: ["journal-append"],
|
|
92
|
-
priority: 0,
|
|
93
|
-
protected: false
|
|
94
|
-
}
|
|
95
|
+
stage: supervisorClosureStage
|
|
95
96
|
};
|
|
97
|
+
// packages/supervisor-plugin/src/loop.ts
|
|
98
|
+
import { computeTaskDependencyBadges, toTaskDependencyProjection } from "@rig/contracts";
|
|
99
|
+
import { rankReadyTasks, selectRankedReadyTasks } from "@rig/dependency-graph-plugin";
|
|
100
|
+
|
|
101
|
+
// packages/supervisor-plugin/src/journal.ts
|
|
102
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
103
|
+
import { dirname } from "path";
|
|
104
|
+
import { Schema } from "effect";
|
|
105
|
+
import {
|
|
106
|
+
SupervisorEvent
|
|
107
|
+
} from "@rig/contracts";
|
|
108
|
+
var NEWLINE = `
|
|
109
|
+
`;
|
|
110
|
+
function reduceSupervisorJournal(events) {
|
|
111
|
+
let processed = 0;
|
|
112
|
+
let succeeded = 0;
|
|
113
|
+
let failed = 0;
|
|
114
|
+
let skipped = 0;
|
|
115
|
+
let current = null;
|
|
116
|
+
let idleReason = null;
|
|
117
|
+
let stopReason = null;
|
|
118
|
+
let status = "running";
|
|
119
|
+
let plannedOrder = [];
|
|
120
|
+
let selectionPolicy = null;
|
|
121
|
+
const concurrency = null;
|
|
122
|
+
const closures = [];
|
|
123
|
+
const anomalies = [];
|
|
124
|
+
for (const event of events) {
|
|
125
|
+
switch (event.kind) {
|
|
126
|
+
case "supervisor.started":
|
|
127
|
+
status = "running";
|
|
128
|
+
break;
|
|
129
|
+
case "supervisor.selection-planned":
|
|
130
|
+
plannedOrder = [...event.taskIds];
|
|
131
|
+
selectionPolicy = event.policy;
|
|
132
|
+
break;
|
|
133
|
+
case "supervisor.dispatch-started":
|
|
134
|
+
break;
|
|
135
|
+
case "supervisor.dispatch-confirmed":
|
|
136
|
+
current = { taskId: event.taskId, runId: event.runId };
|
|
137
|
+
break;
|
|
138
|
+
case "supervisor.dispatch":
|
|
139
|
+
current = { taskId: event.taskId, runId: event.runId };
|
|
140
|
+
break;
|
|
141
|
+
case "supervisor.outcome":
|
|
142
|
+
processed += 1;
|
|
143
|
+
if (event.failed) {
|
|
144
|
+
failed += 1;
|
|
145
|
+
} else {
|
|
146
|
+
succeeded += 1;
|
|
147
|
+
}
|
|
148
|
+
if (event.closure) {
|
|
149
|
+
closures.push(event.closure);
|
|
150
|
+
}
|
|
151
|
+
if (current?.runId === event.runId) {
|
|
152
|
+
current = null;
|
|
153
|
+
} else if (current !== null) {
|
|
154
|
+
anomalies.push(`outcome for ${event.runId} did not match current ${current.runId}`);
|
|
155
|
+
}
|
|
156
|
+
break;
|
|
157
|
+
case "supervisor.skipped":
|
|
158
|
+
processed += 1;
|
|
159
|
+
skipped += 1;
|
|
160
|
+
break;
|
|
161
|
+
case "supervisor.idle":
|
|
162
|
+
status = "idle";
|
|
163
|
+
idleReason = event.reason;
|
|
164
|
+
break;
|
|
165
|
+
case "supervisor.stopped":
|
|
166
|
+
status = "stopped";
|
|
167
|
+
stopReason = event.reason;
|
|
168
|
+
current = null;
|
|
169
|
+
break;
|
|
170
|
+
case "supervisor.finished":
|
|
171
|
+
status = "finished";
|
|
172
|
+
processed = event.processed;
|
|
173
|
+
succeeded = event.succeeded;
|
|
174
|
+
failed = event.failed;
|
|
175
|
+
skipped = event.skipped ?? skipped;
|
|
176
|
+
idleReason = event.idleReason;
|
|
177
|
+
current = null;
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return { status, processed, succeeded, failed, skipped, current, plannedOrder, selectionPolicy, concurrency, idleReason, stopReason, closures, anomalies };
|
|
182
|
+
}
|
|
183
|
+
function createFileSupervisorJournal(path) {
|
|
184
|
+
return {
|
|
185
|
+
async append(line) {
|
|
186
|
+
await mkdir(dirname(path), { recursive: true });
|
|
187
|
+
await writeFile(path, `${line}${NEWLINE}`, { flag: "a" });
|
|
188
|
+
},
|
|
189
|
+
async read() {
|
|
190
|
+
try {
|
|
191
|
+
return await readFile(path, "utf8");
|
|
192
|
+
} catch (error) {
|
|
193
|
+
const code = typeof error === "object" && error !== null && "code" in error ? error.code : undefined;
|
|
194
|
+
if (code === "ENOENT")
|
|
195
|
+
return "";
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function createInMemorySupervisorJournalStore(seed = []) {
|
|
202
|
+
const lines = seed.map((event) => JSON.stringify(event));
|
|
203
|
+
return {
|
|
204
|
+
async append(line) {
|
|
205
|
+
lines.push(line);
|
|
206
|
+
},
|
|
207
|
+
async read() {
|
|
208
|
+
return lines.join(NEWLINE);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function parseSupervisorJournal(text) {
|
|
213
|
+
return text.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0).map((line, index) => {
|
|
214
|
+
try {
|
|
215
|
+
return Schema.decodeUnknownSync(SupervisorEvent)(JSON.parse(line));
|
|
216
|
+
} catch (error) {
|
|
217
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
218
|
+
throw new Error(`Invalid supervisor journal line ${index + 1}: ${message}`);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
function createSupervisorJournal(store) {
|
|
223
|
+
const append = async (event) => {
|
|
224
|
+
await store.append(JSON.stringify(Schema.decodeUnknownSync(SupervisorEvent)(event)));
|
|
225
|
+
};
|
|
226
|
+
const readEvents = async () => parseSupervisorJournal(await store.read());
|
|
227
|
+
const readProjection = async () => reduceSupervisorJournal(await readEvents());
|
|
228
|
+
return { append, readEvents, readProjection };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// packages/supervisor-plugin/src/loop.ts
|
|
232
|
+
import { classifyTasks, isHumanBlockerClass } from "@rig/blocker-classifier-plugin";
|
|
233
|
+
function selectionMode(policy) {
|
|
234
|
+
return policy === "blocking-only" || policy === "max-unblock" ? policy : "all-ready";
|
|
235
|
+
}
|
|
236
|
+
function at(deps) {
|
|
237
|
+
return deps.now ? deps.now() : new Date().toISOString();
|
|
238
|
+
}
|
|
239
|
+
function activeTaskIds(runs) {
|
|
240
|
+
const terminal = new Set(["completed", "failed", "stopped", "stale"]);
|
|
241
|
+
return new Set(runs.filter((run) => run.taskId && run.live && !run.stale && !terminal.has(run.status)).map((run) => run.taskId));
|
|
242
|
+
}
|
|
243
|
+
function stopReasonForNoCandidates(tasks, runs) {
|
|
244
|
+
if (tasks.length === 0)
|
|
245
|
+
return "all-done";
|
|
246
|
+
const classifications = classifyTasks(tasks, runs).classifications;
|
|
247
|
+
const nonTerminal = tasks.map(toTaskDependencyProjection).filter((task) => task.status !== "closed" && task.status !== "completed" && task.status !== "cancelled");
|
|
248
|
+
if (nonTerminal.length > 0 && nonTerminal.every((task) => {
|
|
249
|
+
const classification = classifications.find((entry) => entry.taskId === task.id);
|
|
250
|
+
return classification ? isHumanBlockerClass(classification.blockerClass) : false;
|
|
251
|
+
}))
|
|
252
|
+
return "all-human-blocked";
|
|
253
|
+
return "all-done";
|
|
254
|
+
}
|
|
255
|
+
async function planSupervisorLoop(projectRoot, deps, options = {}) {
|
|
256
|
+
const [tasks, runs] = await Promise.all([
|
|
257
|
+
deps.listTasks(projectRoot),
|
|
258
|
+
deps.listRuns ? deps.listRuns(projectRoot) : Promise.resolve([])
|
|
259
|
+
]);
|
|
260
|
+
const projected = tasks.map(toTaskDependencyProjection);
|
|
261
|
+
const excluded = new Set(options.excludeTaskIds ?? []);
|
|
262
|
+
const candidates = options.candidateTaskIds ? new Set(options.candidateTaskIds) : null;
|
|
263
|
+
const ranked = rankReadyTasks(projected, {
|
|
264
|
+
activeTaskIds: activeTaskIds(runs),
|
|
265
|
+
excludeTaskIds: excluded,
|
|
266
|
+
...candidates ? { filter: (task) => candidates.has(task.id) } : {},
|
|
267
|
+
selection: selectionMode(options.selectionPolicy)
|
|
268
|
+
});
|
|
269
|
+
const limit = Math.max(0, options.maxTasks ?? ranked.length);
|
|
270
|
+
const selected = selectRankedReadyTasks(projected, {
|
|
271
|
+
activeTaskIds: activeTaskIds(runs),
|
|
272
|
+
excludeTaskIds: excluded,
|
|
273
|
+
...candidates ? { filter: (task) => candidates.has(task.id) } : {},
|
|
274
|
+
selection: selectionMode(options.selectionPolicy),
|
|
275
|
+
requireDisjointScopes: (options.concurrency ?? 1) > 1,
|
|
276
|
+
limit
|
|
277
|
+
});
|
|
278
|
+
const plannedOrder = selected.map((task) => task.id);
|
|
279
|
+
return {
|
|
280
|
+
plannedOrder,
|
|
281
|
+
ranked: ranked.filter((entry) => plannedOrder.includes(entry.task.id)),
|
|
282
|
+
idleReason: plannedOrder.length === 0 ? stopReasonForNoCandidates(tasks, runs) : null
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function failedOutcome(outcome) {
|
|
286
|
+
if (typeof outcome.failed === "boolean")
|
|
287
|
+
return outcome.failed;
|
|
288
|
+
return outcome.status === "failed" || outcome.status === "stopped" || outcome.status === "needs-attention" || outcome.status === "waiting-approval" || outcome.status === "waiting-user-input";
|
|
289
|
+
}
|
|
290
|
+
function runStatus(value) {
|
|
291
|
+
switch (value) {
|
|
292
|
+
case "created":
|
|
293
|
+
case "queued":
|
|
294
|
+
case "preparing":
|
|
295
|
+
case "running":
|
|
296
|
+
case "waiting-approval":
|
|
297
|
+
case "waiting-user-input":
|
|
298
|
+
case "paused":
|
|
299
|
+
case "validating":
|
|
300
|
+
case "reviewing":
|
|
301
|
+
case "closing-out":
|
|
302
|
+
case "needs-attention":
|
|
303
|
+
case "completed":
|
|
304
|
+
case "failed":
|
|
305
|
+
case "stopped":
|
|
306
|
+
return value;
|
|
307
|
+
default:
|
|
308
|
+
return "failed";
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async function runSupervisorLoop(projectRoot, deps, options = {}) {
|
|
312
|
+
const events = [{ kind: "supervisor.started", at: at(deps), options }];
|
|
313
|
+
const maxTasks = Math.max(0, options.maxTasks ?? Number.POSITIVE_INFINITY);
|
|
314
|
+
const concurrency = Math.max(1, options.concurrency ?? 1);
|
|
315
|
+
let processed = 0;
|
|
316
|
+
let succeeded = 0;
|
|
317
|
+
let failed = 0;
|
|
318
|
+
let skipped = 0;
|
|
319
|
+
const plannedAll = [];
|
|
320
|
+
const dispatchedTaskIds = new Set(options.excludeTaskIds ?? []);
|
|
321
|
+
while (processed < maxTasks) {
|
|
322
|
+
const plan = await planSupervisorLoop(projectRoot, deps, { ...options, excludeTaskIds: dispatchedTaskIds, maxTasks: Math.min(concurrency, maxTasks - processed) });
|
|
323
|
+
if (plan.plannedOrder.length === 0) {
|
|
324
|
+
events.push({ kind: "supervisor.idle", at: at(deps), reason: plan.idleReason ?? "all-done" });
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
events.push({ kind: "supervisor.selection-planned", at: at(deps), taskIds: plan.plannedOrder, policy: options.selectionPolicy ?? "rank" });
|
|
328
|
+
plannedAll.push(...plan.plannedOrder);
|
|
329
|
+
if (options.dryRun)
|
|
330
|
+
break;
|
|
331
|
+
const dispatchTask = deps.dispatch ?? deps.dispatchRun;
|
|
332
|
+
if (!dispatchTask)
|
|
333
|
+
throw new Error("runSupervisorLoop requires dispatch when dryRun is false.");
|
|
334
|
+
if (!deps.awaitRunTerminal)
|
|
335
|
+
throw new Error("runSupervisorLoop requires awaitRunTerminal when dryRun is false.");
|
|
336
|
+
for (const entry of plan.ranked) {
|
|
337
|
+
const taskId = entry.task.id;
|
|
338
|
+
events.push({ kind: "supervisor.dispatch-started", at: at(deps), taskId, score: entry.score });
|
|
339
|
+
const dispatch = await dispatchTask({
|
|
340
|
+
projectRoot,
|
|
341
|
+
taskId: entry.task.id,
|
|
342
|
+
...entry.task.title !== undefined ? { title: entry.task.title } : {},
|
|
343
|
+
...options.model !== undefined ? { model: options.model } : {},
|
|
344
|
+
...options.force !== undefined ? { force: options.force } : {}
|
|
345
|
+
});
|
|
346
|
+
dispatchedTaskIds.add(entry.task.id);
|
|
347
|
+
const runId = dispatch.runId;
|
|
348
|
+
events.push({ kind: "supervisor.dispatch-confirmed", at: at(deps), taskId, runId });
|
|
349
|
+
const outcome = await deps.awaitRunTerminal(projectRoot, dispatch.runId, entry.task.id);
|
|
350
|
+
const outcomeFailed = failedOutcome(outcome);
|
|
351
|
+
if (outcomeFailed)
|
|
352
|
+
failed += 1;
|
|
353
|
+
else
|
|
354
|
+
succeeded += 1;
|
|
355
|
+
processed += 1;
|
|
356
|
+
events.push({
|
|
357
|
+
kind: "supervisor.outcome",
|
|
358
|
+
at: at(deps),
|
|
359
|
+
taskId,
|
|
360
|
+
runId,
|
|
361
|
+
status: runStatus(outcome.status),
|
|
362
|
+
failed: outcomeFailed,
|
|
363
|
+
unblockedTaskIds: [...outcome.unblockedTaskIds ?? []]
|
|
364
|
+
});
|
|
365
|
+
if (options.failFast && outcomeFailed || options.pauseOnAttention && outcomeFailed) {
|
|
366
|
+
events.push({ kind: "supervisor.idle", at: at(deps), reason: outcome.status === "needs-attention" || outcome.status === "waiting-approval" || outcome.status === "waiting-user-input" ? "judge-stop" : "source-error" });
|
|
367
|
+
processed = maxTasks;
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
if (processed >= maxTasks)
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const projectionBeforeFinish = reduceSupervisorJournal(events);
|
|
375
|
+
const idleReason = projectionBeforeFinish.idleReason ?? (processed >= maxTasks && Number.isFinite(maxTasks) ? "max-tasks" : null);
|
|
376
|
+
events.push({ kind: "supervisor.finished", at: at(deps), processed, succeeded, failed, skipped, idleReason });
|
|
377
|
+
const projection = reduceSupervisorJournal(events);
|
|
378
|
+
return { ok: failed === 0, dryRun: options.dryRun === true, plannedOrder: plannedAll, events, projection };
|
|
379
|
+
}
|
|
380
|
+
async function controlSupervisorRun(projectRoot, runId, control, deps) {
|
|
381
|
+
await deps.deliverRunControl(projectRoot, runId, control);
|
|
382
|
+
return { ok: true, runId, control: control.kind };
|
|
383
|
+
}
|
|
384
|
+
function collectBlockingClosure(taskId, badges) {
|
|
385
|
+
const closure = new Set;
|
|
386
|
+
const visit = (currentTaskId) => {
|
|
387
|
+
for (const blockerId of badges.get(currentTaskId)?.blockedBy ?? []) {
|
|
388
|
+
if (closure.has(blockerId))
|
|
389
|
+
continue;
|
|
390
|
+
closure.add(blockerId);
|
|
391
|
+
visit(blockerId);
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
visit(taskId);
|
|
395
|
+
return closure;
|
|
396
|
+
}
|
|
397
|
+
async function unblockTask(projectRoot, taskId, deps, options = {}) {
|
|
398
|
+
if (taskId === null)
|
|
399
|
+
return runSupervisorLoop(projectRoot, deps, { ...options, selectionPolicy: "max-unblock", maxTasks: 1 });
|
|
400
|
+
const tasks = await deps.listTasks(projectRoot);
|
|
401
|
+
const projected = tasks.map(toTaskDependencyProjection);
|
|
402
|
+
const badges = computeTaskDependencyBadges(projected);
|
|
403
|
+
const blockers = collectBlockingClosure(taskId, badges);
|
|
404
|
+
return runSupervisorLoop(projectRoot, deps, { ...options, candidateTaskIds: blockers, selectionPolicy: "rank", maxTasks: 1 });
|
|
405
|
+
}
|
|
406
|
+
|
|
96
407
|
// packages/supervisor-plugin/src/cli.ts
|
|
97
408
|
var SUPERVISOR_LOOP_CLI_ID = "supervisor.loop";
|
|
98
409
|
var SUPERVISOR_UNBLOCK_CLI_ID = "supervisor.unblock";
|
|
@@ -162,7 +473,12 @@ function delay(ms) {
|
|
|
162
473
|
return promise;
|
|
163
474
|
}
|
|
164
475
|
async function loadSupervisorClient() {
|
|
165
|
-
|
|
476
|
+
const [taskIo, runIo, dispatchIo] = await Promise.all([
|
|
477
|
+
import("@rig/core/task-io"),
|
|
478
|
+
import("@rig/run-worker/runs"),
|
|
479
|
+
import("@rig/runtime/control-plane/dispatch")
|
|
480
|
+
]);
|
|
481
|
+
return { listTasks: taskIo.listTasks, listRuns: runIo.listRuns, dispatchRun: dispatchIo.dispatchRun };
|
|
166
482
|
}
|
|
167
483
|
function supervisorDeps(timeoutMs) {
|
|
168
484
|
return {
|
|
@@ -205,8 +521,7 @@ async function executeLoop(context, args) {
|
|
|
205
521
|
const stopWhen = takeOption(task.rest, "--stop-when");
|
|
206
522
|
const timeout = takeOption(stopWhen.rest, "--timeout-ms");
|
|
207
523
|
requireNoExtraArgs(timeout.rest, "rig loop [--task <id>] [--max-tasks <n>] [--concurrency <n>] [--stop-when <csv>] [--dry-run] [--json]");
|
|
208
|
-
const
|
|
209
|
-
const result = await runSupervisorLoop(context.projectRoot, supervisorDeps(parsePositiveInt(timeout.value, "--timeout-ms", 1800000)), {
|
|
524
|
+
const result = await runSupervisorLoop(context.projectRoot, supervisorDeps(parsePositiveInt(timeout.value, "--timeout-ms", 30 * 60 * 1000)), {
|
|
210
525
|
maxTasks: parsePositiveInt(maxTasks.value, "--max-tasks", 1),
|
|
211
526
|
concurrency: parsePositiveInt(concurrency.value, "--concurrency", 1),
|
|
212
527
|
...task.value ? { candidateTaskIds: [task.value] } : {},
|
|
@@ -227,8 +542,7 @@ async function executeUnblock(context, args) {
|
|
|
227
542
|
const timeout = takeOption(json.rest, "--timeout-ms");
|
|
228
543
|
const taskId = timeout.rest[0]?.startsWith("-") ? undefined : timeout.rest[0];
|
|
229
544
|
requireNoExtraArgs(taskId ? timeout.rest.slice(1) : timeout.rest, "rig unblock [task-id] [--dry-run] [--json]");
|
|
230
|
-
const
|
|
231
|
-
const result = await unblockTask(context.projectRoot, taskId ?? null, supervisorDeps(parsePositiveInt(timeout.value, "--timeout-ms", 1800000)), { dryRun: dry.value || context.dryRun });
|
|
545
|
+
const result = await unblockTask(context.projectRoot, taskId ?? null, supervisorDeps(parsePositiveInt(timeout.value, "--timeout-ms", 30 * 60 * 1000)), { dryRun: dry.value || context.dryRun });
|
|
232
546
|
if (context.outputMode === "text") {
|
|
233
547
|
if (json.value)
|
|
234
548
|
printJson(result);
|
|
@@ -257,65 +571,31 @@ var supervisorCliCommands = [
|
|
|
257
571
|
run: executeUnblock
|
|
258
572
|
}
|
|
259
573
|
];
|
|
260
|
-
// packages/supervisor-plugin/src/
|
|
261
|
-
import {
|
|
262
|
-
import {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
function createFileSupervisorJournal(path) {
|
|
271
|
-
return {
|
|
272
|
-
async append(line) {
|
|
273
|
-
await mkdir(dirname(path), { recursive: true });
|
|
274
|
-
await writeFile(path, `${line}${NEWLINE}`, { flag: "a" });
|
|
275
|
-
},
|
|
276
|
-
async read() {
|
|
277
|
-
try {
|
|
278
|
-
return await readFile(path, "utf8");
|
|
279
|
-
} catch (error) {
|
|
280
|
-
const code = typeof error === "object" && error !== null && "code" in error ? error.code : undefined;
|
|
281
|
-
if (code === "ENOENT")
|
|
282
|
-
return "";
|
|
283
|
-
throw error;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
function createInMemorySupervisorJournalStore(seed = []) {
|
|
289
|
-
const lines = seed.map((event) => JSON.stringify(event));
|
|
574
|
+
// packages/supervisor-plugin/src/plugin.ts
|
|
575
|
+
import { RIG_CAPABILITY_PANEL_SLOT, RIG_SUPERVISOR_PANEL_ID } from "@rig/contracts";
|
|
576
|
+
import { definePlugin } from "@rig/core/config";
|
|
577
|
+
|
|
578
|
+
// packages/supervisor-plugin/src/panel.ts
|
|
579
|
+
import { RIG_RUN_STOP_PANEL_ACTION } from "@rig/contracts";
|
|
580
|
+
function buildSupervisorPanelPayload(context) {
|
|
581
|
+
const status = context.folded.status ?? "unknown";
|
|
582
|
+
const taskId = context.folded.record.taskId ?? context.taskIdAtStart;
|
|
583
|
+
const operatorActive = status === "running" || status === "validating" || status === "closing-out" || status === "needs-attention";
|
|
290
584
|
return {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
585
|
+
status,
|
|
586
|
+
currentTask: taskId ? { id: taskId, title: context.runDisplayTitle } : null,
|
|
587
|
+
processed: context.folded.closeoutPhases.length,
|
|
588
|
+
succeeded: context.folded.closeoutPhases.filter((phase) => phase.outcome === "completed").length,
|
|
589
|
+
failed: context.folded.closeoutPhases.filter((phase) => phase.outcome === "failed").length,
|
|
590
|
+
skipped: 0,
|
|
591
|
+
plannedOrder: taskId ? [{ id: taskId, title: context.runDisplayTitle, status }] : [],
|
|
592
|
+
idleReason: operatorActive ? null : status,
|
|
593
|
+
stopActionId: operatorActive ? RIG_RUN_STOP_PANEL_ACTION : null,
|
|
594
|
+
closures: []
|
|
297
595
|
};
|
|
298
596
|
}
|
|
299
|
-
|
|
300
|
-
return text.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0).map((line, index) => {
|
|
301
|
-
try {
|
|
302
|
-
return Schema.decodeUnknownSync(SupervisorEvent)(JSON.parse(line));
|
|
303
|
-
} catch (error) {
|
|
304
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
305
|
-
throw new Error(`Invalid supervisor journal line ${index + 1}: ${message}`);
|
|
306
|
-
}
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
function createSupervisorJournal(store) {
|
|
310
|
-
const append = async (event) => {
|
|
311
|
-
await store.append(JSON.stringify(Schema.decodeUnknownSync(SupervisorEvent)(event)));
|
|
312
|
-
};
|
|
313
|
-
const readEvents = async () => parseSupervisorJournal(await store.read());
|
|
314
|
-
const readProjection = async () => reduceSupervisorJournal(await readEvents());
|
|
315
|
-
return { append, readEvents, readProjection };
|
|
316
|
-
}
|
|
597
|
+
|
|
317
598
|
// packages/supervisor-plugin/src/plugin.ts
|
|
318
|
-
import { definePlugin } from "@rig/core/config";
|
|
319
599
|
var SUPERVISOR_PLUGIN_NAME = "@rig/supervisor-plugin";
|
|
320
600
|
var supervisorPlugin = definePlugin({
|
|
321
601
|
name: SUPERVISOR_PLUGIN_NAME,
|
|
@@ -327,28 +607,32 @@ var supervisorPlugin = definePlugin({
|
|
|
327
607
|
{ id: "supervisor.loop", title: "Supervisor loop", commandId: SUPERVISOR_LOOP_CLI_ID },
|
|
328
608
|
{ id: "supervisor.unblock", title: "Supervisor unblock", commandId: SUPERVISOR_UNBLOCK_CLI_ID }
|
|
329
609
|
],
|
|
330
|
-
cliCommands: supervisorCliCommands
|
|
331
|
-
|
|
610
|
+
cliCommands: supervisorCliCommands,
|
|
611
|
+
panels: [
|
|
612
|
+
{
|
|
613
|
+
id: RIG_SUPERVISOR_PANEL_ID,
|
|
614
|
+
slot: RIG_CAPABILITY_PANEL_SLOT,
|
|
615
|
+
title: "Supervisor",
|
|
616
|
+
capabilityId: "run.supervisor",
|
|
617
|
+
description: "Live run status, closeout progress, and operator stop control.",
|
|
618
|
+
produce: (context) => buildSupervisorPanelPayload(context)
|
|
619
|
+
}
|
|
620
|
+
],
|
|
621
|
+
stageMutations: [supervisorClosureStageMutation],
|
|
622
|
+
stages: [{ ...supervisorClosureStage, run: createDefaultSupervisorClosureStage() }]
|
|
332
623
|
}
|
|
333
|
-
}, {
|
|
334
|
-
stages: { [SUPERVISOR_CLOSURE_STAGE_ID]: createDefaultSupervisorClosureStage() },
|
|
335
|
-
featureCapabilities: [
|
|
336
|
-
{ id: "supervisor.loop", title: "Supervisor loop", commandId: SUPERVISOR_LOOP_CLI_ID },
|
|
337
|
-
{ id: "supervisor.unblock", title: "Supervisor unblock", commandId: SUPERVISOR_UNBLOCK_CLI_ID }
|
|
338
|
-
],
|
|
339
|
-
cliCommands: supervisorCliCommands
|
|
340
624
|
});
|
|
341
625
|
function createSupervisorPlugin() {
|
|
342
626
|
return supervisorPlugin;
|
|
343
627
|
}
|
|
344
628
|
// packages/supervisor-plugin/src/supervisor.ts
|
|
345
629
|
import {
|
|
346
|
-
computeTaskDependencyBadges,
|
|
630
|
+
computeTaskDependencyBadges as computeTaskDependencyBadges2,
|
|
347
631
|
disjointScope,
|
|
348
632
|
isTaskTerminalStatus,
|
|
349
|
-
rankReadyTasks,
|
|
350
633
|
readTaskScope
|
|
351
|
-
} from "@rig/
|
|
634
|
+
} from "@rig/contracts";
|
|
635
|
+
import { rankReadyTasks as rankReadyTasks2 } from "@rig/dependency-graph-plugin";
|
|
352
636
|
var DEFAULT_STOP_WHEN = new Set(["all-done", "all-human-blocked", "source-error"]);
|
|
353
637
|
var HUMAN_BLOCKER_CLASS = {
|
|
354
638
|
"not-blocked": false,
|
|
@@ -370,7 +654,7 @@ function normalizeOptions(options = {}) {
|
|
|
370
654
|
pollMs: Math.max(0, Math.floor(options.pollMs ?? 5000))
|
|
371
655
|
};
|
|
372
656
|
}
|
|
373
|
-
function
|
|
657
|
+
function selectionMode2(policy) {
|
|
374
658
|
if (policy === "blocking-only")
|
|
375
659
|
return "blocking-only";
|
|
376
660
|
if (policy === "max-unblock")
|
|
@@ -431,15 +715,15 @@ async function runSupervisor(ctx, options = {}) {
|
|
|
431
715
|
break;
|
|
432
716
|
}
|
|
433
717
|
const snapshot = await ctx.readTasks();
|
|
434
|
-
const badges =
|
|
718
|
+
const badges = computeTaskDependencyBadges2(snapshot.tasks);
|
|
435
719
|
const activeRuns = snapshot.activeRuns ?? [];
|
|
436
|
-
const
|
|
720
|
+
const activeTaskIds2 = activeRuns.map((run) => run.taskId);
|
|
437
721
|
const remainingSlots = normalized.maxTasks === null ? normalized.concurrency : Math.min(normalized.concurrency, normalized.maxTasks - processed);
|
|
438
|
-
const ranked =
|
|
722
|
+
const ranked = rankReadyTasks2(snapshot.tasks, {
|
|
439
723
|
excludeTaskIds: completedTaskIds,
|
|
440
|
-
activeTaskIds,
|
|
724
|
+
activeTaskIds: activeTaskIds2,
|
|
441
725
|
filter: (task) => filterTask(task, normalized.filter),
|
|
442
|
-
selection:
|
|
726
|
+
selection: selectionMode2(normalized.selectionPolicy)
|
|
443
727
|
});
|
|
444
728
|
const selected = [];
|
|
445
729
|
const occupiedScopes = activeRuns.flatMap((run) => {
|
|
@@ -474,18 +758,18 @@ async function runSupervisor(ctx, options = {}) {
|
|
|
474
758
|
...handle.dispatchHandle === undefined ? {} : { dispatchHandle: handle.dispatchHandle }
|
|
475
759
|
});
|
|
476
760
|
const outcome = await ctx.awaitTerminal(handle.runId, entry.task);
|
|
477
|
-
const
|
|
761
|
+
const failedOutcome2 = terminalFailed(outcome);
|
|
478
762
|
await emit({
|
|
479
763
|
kind: "supervisor.outcome",
|
|
480
764
|
at: now(),
|
|
481
765
|
taskId: entry.task.id,
|
|
482
766
|
runId: outcome.runId,
|
|
483
767
|
status: outcome.status,
|
|
484
|
-
failed:
|
|
768
|
+
failed: failedOutcome2,
|
|
485
769
|
unblockedTaskIds: outcome.closure?.unblockedTaskIds ?? [],
|
|
486
770
|
...outcome.closure ? { closure: outcome.closure } : {}
|
|
487
771
|
});
|
|
488
|
-
return { taskId: entry.task.id, status: outcome.status, failed:
|
|
772
|
+
return { taskId: entry.task.id, status: outcome.status, failed: failedOutcome2 };
|
|
489
773
|
}));
|
|
490
774
|
for (const outcome of outcomes) {
|
|
491
775
|
completedTaskIds.add(outcome.taskId);
|
|
@@ -510,10 +794,15 @@ async function runSupervisor(ctx, options = {}) {
|
|
|
510
794
|
return { processed, succeeded, failed, skipped, idleReason };
|
|
511
795
|
}
|
|
512
796
|
export {
|
|
797
|
+
unblockTask,
|
|
513
798
|
supervisorPlugin,
|
|
514
799
|
supervisorClosureStageMutation,
|
|
800
|
+
supervisorClosureStage,
|
|
515
801
|
supervisorCliCommands,
|
|
802
|
+
runSupervisorLoop,
|
|
516
803
|
runSupervisor,
|
|
804
|
+
reduceSupervisorJournal,
|
|
805
|
+
planSupervisorLoop,
|
|
517
806
|
parseSupervisorJournal,
|
|
518
807
|
executeUnblock,
|
|
519
808
|
executeLoop,
|
|
@@ -524,6 +813,7 @@ export {
|
|
|
524
813
|
createFileSupervisorJournal,
|
|
525
814
|
createDefaultSupervisorClosureStage,
|
|
526
815
|
createClosureStage,
|
|
816
|
+
controlSupervisorRun,
|
|
527
817
|
awaitTerminalRun,
|
|
528
818
|
SUPERVISOR_UNBLOCK_CLI_ID,
|
|
529
819
|
SUPERVISOR_PLUGIN_NAME,
|
package/dist/src/journal.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import { SupervisorEvent, type SupervisorProjection } from "@rig/contracts";
|
|
2
|
+
/**
|
|
3
|
+
* Fold a supervisor event journal into the current supervisor projection.
|
|
4
|
+
*
|
|
5
|
+
* Behavioral reducer owned by the supervisor plugin (the only consumer of the
|
|
6
|
+
* supervisor loop's read-model). `@rig/contracts` keeps the `SupervisorEvent` /
|
|
7
|
+
* `SupervisorProjection` schemas; the fold over them lives here.
|
|
8
|
+
*/
|
|
9
|
+
export declare function reduceSupervisorJournal(events: readonly SupervisorEvent[]): SupervisorProjection;
|
|
2
10
|
export interface SupervisorJournalStore {
|
|
3
11
|
append(line: string): Promise<void>;
|
|
4
12
|
read(): Promise<string>;
|
package/dist/src/journal.js
CHANGED
|
@@ -4,11 +4,83 @@ import { mkdir, readFile, writeFile } from "fs/promises";
|
|
|
4
4
|
import { dirname } from "path";
|
|
5
5
|
import { Schema } from "effect";
|
|
6
6
|
import {
|
|
7
|
-
SupervisorEvent
|
|
8
|
-
reduceSupervisorJournal
|
|
7
|
+
SupervisorEvent
|
|
9
8
|
} from "@rig/contracts";
|
|
10
9
|
var NEWLINE = `
|
|
11
10
|
`;
|
|
11
|
+
function reduceSupervisorJournal(events) {
|
|
12
|
+
let processed = 0;
|
|
13
|
+
let succeeded = 0;
|
|
14
|
+
let failed = 0;
|
|
15
|
+
let skipped = 0;
|
|
16
|
+
let current = null;
|
|
17
|
+
let idleReason = null;
|
|
18
|
+
let stopReason = null;
|
|
19
|
+
let status = "running";
|
|
20
|
+
let plannedOrder = [];
|
|
21
|
+
let selectionPolicy = null;
|
|
22
|
+
const concurrency = null;
|
|
23
|
+
const closures = [];
|
|
24
|
+
const anomalies = [];
|
|
25
|
+
for (const event of events) {
|
|
26
|
+
switch (event.kind) {
|
|
27
|
+
case "supervisor.started":
|
|
28
|
+
status = "running";
|
|
29
|
+
break;
|
|
30
|
+
case "supervisor.selection-planned":
|
|
31
|
+
plannedOrder = [...event.taskIds];
|
|
32
|
+
selectionPolicy = event.policy;
|
|
33
|
+
break;
|
|
34
|
+
case "supervisor.dispatch-started":
|
|
35
|
+
break;
|
|
36
|
+
case "supervisor.dispatch-confirmed":
|
|
37
|
+
current = { taskId: event.taskId, runId: event.runId };
|
|
38
|
+
break;
|
|
39
|
+
case "supervisor.dispatch":
|
|
40
|
+
current = { taskId: event.taskId, runId: event.runId };
|
|
41
|
+
break;
|
|
42
|
+
case "supervisor.outcome":
|
|
43
|
+
processed += 1;
|
|
44
|
+
if (event.failed) {
|
|
45
|
+
failed += 1;
|
|
46
|
+
} else {
|
|
47
|
+
succeeded += 1;
|
|
48
|
+
}
|
|
49
|
+
if (event.closure) {
|
|
50
|
+
closures.push(event.closure);
|
|
51
|
+
}
|
|
52
|
+
if (current?.runId === event.runId) {
|
|
53
|
+
current = null;
|
|
54
|
+
} else if (current !== null) {
|
|
55
|
+
anomalies.push(`outcome for ${event.runId} did not match current ${current.runId}`);
|
|
56
|
+
}
|
|
57
|
+
break;
|
|
58
|
+
case "supervisor.skipped":
|
|
59
|
+
processed += 1;
|
|
60
|
+
skipped += 1;
|
|
61
|
+
break;
|
|
62
|
+
case "supervisor.idle":
|
|
63
|
+
status = "idle";
|
|
64
|
+
idleReason = event.reason;
|
|
65
|
+
break;
|
|
66
|
+
case "supervisor.stopped":
|
|
67
|
+
status = "stopped";
|
|
68
|
+
stopReason = event.reason;
|
|
69
|
+
current = null;
|
|
70
|
+
break;
|
|
71
|
+
case "supervisor.finished":
|
|
72
|
+
status = "finished";
|
|
73
|
+
processed = event.processed;
|
|
74
|
+
succeeded = event.succeeded;
|
|
75
|
+
failed = event.failed;
|
|
76
|
+
skipped = event.skipped ?? skipped;
|
|
77
|
+
idleReason = event.idleReason;
|
|
78
|
+
current = null;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { status, processed, succeeded, failed, skipped, current, plannedOrder, selectionPolicy, concurrency, idleReason, stopReason, closures, anomalies };
|
|
83
|
+
}
|
|
12
84
|
function createFileSupervisorJournal(path) {
|
|
13
85
|
return {
|
|
14
86
|
async append(line) {
|
|
@@ -57,6 +129,7 @@ function createSupervisorJournal(store) {
|
|
|
57
129
|
return { append, readEvents, readProjection };
|
|
58
130
|
}
|
|
59
131
|
export {
|
|
132
|
+
reduceSupervisorJournal,
|
|
60
133
|
parseSupervisorJournal,
|
|
61
134
|
createSupervisorJournal,
|
|
62
135
|
createInMemorySupervisorJournalStore,
|