@h-rig/supervisor-plugin 0.0.6-alpha.155 → 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/cli.js
CHANGED
|
@@ -37,6 +37,261 @@ async function awaitTerminalRun(options) {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
// packages/supervisor-plugin/src/loop.ts
|
|
41
|
+
import { computeTaskDependencyBadges, toTaskDependencyProjection } from "@rig/contracts";
|
|
42
|
+
import { rankReadyTasks, selectRankedReadyTasks } from "@rig/dependency-graph-plugin";
|
|
43
|
+
|
|
44
|
+
// packages/supervisor-plugin/src/journal.ts
|
|
45
|
+
import { Schema } from "effect";
|
|
46
|
+
import {
|
|
47
|
+
SupervisorEvent
|
|
48
|
+
} from "@rig/contracts";
|
|
49
|
+
function reduceSupervisorJournal(events) {
|
|
50
|
+
let processed = 0;
|
|
51
|
+
let succeeded = 0;
|
|
52
|
+
let failed = 0;
|
|
53
|
+
let skipped = 0;
|
|
54
|
+
let current = null;
|
|
55
|
+
let idleReason = null;
|
|
56
|
+
let stopReason = null;
|
|
57
|
+
let status = "running";
|
|
58
|
+
let plannedOrder = [];
|
|
59
|
+
let selectionPolicy = null;
|
|
60
|
+
const concurrency = null;
|
|
61
|
+
const closures = [];
|
|
62
|
+
const anomalies = [];
|
|
63
|
+
for (const event of events) {
|
|
64
|
+
switch (event.kind) {
|
|
65
|
+
case "supervisor.started":
|
|
66
|
+
status = "running";
|
|
67
|
+
break;
|
|
68
|
+
case "supervisor.selection-planned":
|
|
69
|
+
plannedOrder = [...event.taskIds];
|
|
70
|
+
selectionPolicy = event.policy;
|
|
71
|
+
break;
|
|
72
|
+
case "supervisor.dispatch-started":
|
|
73
|
+
break;
|
|
74
|
+
case "supervisor.dispatch-confirmed":
|
|
75
|
+
current = { taskId: event.taskId, runId: event.runId };
|
|
76
|
+
break;
|
|
77
|
+
case "supervisor.dispatch":
|
|
78
|
+
current = { taskId: event.taskId, runId: event.runId };
|
|
79
|
+
break;
|
|
80
|
+
case "supervisor.outcome":
|
|
81
|
+
processed += 1;
|
|
82
|
+
if (event.failed) {
|
|
83
|
+
failed += 1;
|
|
84
|
+
} else {
|
|
85
|
+
succeeded += 1;
|
|
86
|
+
}
|
|
87
|
+
if (event.closure) {
|
|
88
|
+
closures.push(event.closure);
|
|
89
|
+
}
|
|
90
|
+
if (current?.runId === event.runId) {
|
|
91
|
+
current = null;
|
|
92
|
+
} else if (current !== null) {
|
|
93
|
+
anomalies.push(`outcome for ${event.runId} did not match current ${current.runId}`);
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
case "supervisor.skipped":
|
|
97
|
+
processed += 1;
|
|
98
|
+
skipped += 1;
|
|
99
|
+
break;
|
|
100
|
+
case "supervisor.idle":
|
|
101
|
+
status = "idle";
|
|
102
|
+
idleReason = event.reason;
|
|
103
|
+
break;
|
|
104
|
+
case "supervisor.stopped":
|
|
105
|
+
status = "stopped";
|
|
106
|
+
stopReason = event.reason;
|
|
107
|
+
current = null;
|
|
108
|
+
break;
|
|
109
|
+
case "supervisor.finished":
|
|
110
|
+
status = "finished";
|
|
111
|
+
processed = event.processed;
|
|
112
|
+
succeeded = event.succeeded;
|
|
113
|
+
failed = event.failed;
|
|
114
|
+
skipped = event.skipped ?? skipped;
|
|
115
|
+
idleReason = event.idleReason;
|
|
116
|
+
current = null;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { status, processed, succeeded, failed, skipped, current, plannedOrder, selectionPolicy, concurrency, idleReason, stopReason, closures, anomalies };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// packages/supervisor-plugin/src/loop.ts
|
|
124
|
+
import { classifyTasks, isHumanBlockerClass } from "@rig/blocker-classifier-plugin";
|
|
125
|
+
function selectionMode(policy) {
|
|
126
|
+
return policy === "blocking-only" || policy === "max-unblock" ? policy : "all-ready";
|
|
127
|
+
}
|
|
128
|
+
function at(deps) {
|
|
129
|
+
return deps.now ? deps.now() : new Date().toISOString();
|
|
130
|
+
}
|
|
131
|
+
function activeTaskIds(runs) {
|
|
132
|
+
const terminal = new Set(["completed", "failed", "stopped", "stale"]);
|
|
133
|
+
return new Set(runs.filter((run) => run.taskId && run.live && !run.stale && !terminal.has(run.status)).map((run) => run.taskId));
|
|
134
|
+
}
|
|
135
|
+
function stopReasonForNoCandidates(tasks, runs) {
|
|
136
|
+
if (tasks.length === 0)
|
|
137
|
+
return "all-done";
|
|
138
|
+
const classifications = classifyTasks(tasks, runs).classifications;
|
|
139
|
+
const nonTerminal = tasks.map(toTaskDependencyProjection).filter((task) => task.status !== "closed" && task.status !== "completed" && task.status !== "cancelled");
|
|
140
|
+
if (nonTerminal.length > 0 && nonTerminal.every((task) => {
|
|
141
|
+
const classification = classifications.find((entry) => entry.taskId === task.id);
|
|
142
|
+
return classification ? isHumanBlockerClass(classification.blockerClass) : false;
|
|
143
|
+
}))
|
|
144
|
+
return "all-human-blocked";
|
|
145
|
+
return "all-done";
|
|
146
|
+
}
|
|
147
|
+
async function planSupervisorLoop(projectRoot, deps, options = {}) {
|
|
148
|
+
const [tasks, runs] = await Promise.all([
|
|
149
|
+
deps.listTasks(projectRoot),
|
|
150
|
+
deps.listRuns ? deps.listRuns(projectRoot) : Promise.resolve([])
|
|
151
|
+
]);
|
|
152
|
+
const projected = tasks.map(toTaskDependencyProjection);
|
|
153
|
+
const excluded = new Set(options.excludeTaskIds ?? []);
|
|
154
|
+
const candidates = options.candidateTaskIds ? new Set(options.candidateTaskIds) : null;
|
|
155
|
+
const ranked = rankReadyTasks(projected, {
|
|
156
|
+
activeTaskIds: activeTaskIds(runs),
|
|
157
|
+
excludeTaskIds: excluded,
|
|
158
|
+
...candidates ? { filter: (task) => candidates.has(task.id) } : {},
|
|
159
|
+
selection: selectionMode(options.selectionPolicy)
|
|
160
|
+
});
|
|
161
|
+
const limit = Math.max(0, options.maxTasks ?? ranked.length);
|
|
162
|
+
const selected = selectRankedReadyTasks(projected, {
|
|
163
|
+
activeTaskIds: activeTaskIds(runs),
|
|
164
|
+
excludeTaskIds: excluded,
|
|
165
|
+
...candidates ? { filter: (task) => candidates.has(task.id) } : {},
|
|
166
|
+
selection: selectionMode(options.selectionPolicy),
|
|
167
|
+
requireDisjointScopes: (options.concurrency ?? 1) > 1,
|
|
168
|
+
limit
|
|
169
|
+
});
|
|
170
|
+
const plannedOrder = selected.map((task) => task.id);
|
|
171
|
+
return {
|
|
172
|
+
plannedOrder,
|
|
173
|
+
ranked: ranked.filter((entry) => plannedOrder.includes(entry.task.id)),
|
|
174
|
+
idleReason: plannedOrder.length === 0 ? stopReasonForNoCandidates(tasks, runs) : null
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function failedOutcome(outcome) {
|
|
178
|
+
if (typeof outcome.failed === "boolean")
|
|
179
|
+
return outcome.failed;
|
|
180
|
+
return outcome.status === "failed" || outcome.status === "stopped" || outcome.status === "needs-attention" || outcome.status === "waiting-approval" || outcome.status === "waiting-user-input";
|
|
181
|
+
}
|
|
182
|
+
function runStatus(value) {
|
|
183
|
+
switch (value) {
|
|
184
|
+
case "created":
|
|
185
|
+
case "queued":
|
|
186
|
+
case "preparing":
|
|
187
|
+
case "running":
|
|
188
|
+
case "waiting-approval":
|
|
189
|
+
case "waiting-user-input":
|
|
190
|
+
case "paused":
|
|
191
|
+
case "validating":
|
|
192
|
+
case "reviewing":
|
|
193
|
+
case "closing-out":
|
|
194
|
+
case "needs-attention":
|
|
195
|
+
case "completed":
|
|
196
|
+
case "failed":
|
|
197
|
+
case "stopped":
|
|
198
|
+
return value;
|
|
199
|
+
default:
|
|
200
|
+
return "failed";
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async function runSupervisorLoop(projectRoot, deps, options = {}) {
|
|
204
|
+
const events = [{ kind: "supervisor.started", at: at(deps), options }];
|
|
205
|
+
const maxTasks = Math.max(0, options.maxTasks ?? Number.POSITIVE_INFINITY);
|
|
206
|
+
const concurrency = Math.max(1, options.concurrency ?? 1);
|
|
207
|
+
let processed = 0;
|
|
208
|
+
let succeeded = 0;
|
|
209
|
+
let failed = 0;
|
|
210
|
+
let skipped = 0;
|
|
211
|
+
const plannedAll = [];
|
|
212
|
+
const dispatchedTaskIds = new Set(options.excludeTaskIds ?? []);
|
|
213
|
+
while (processed < maxTasks) {
|
|
214
|
+
const plan = await planSupervisorLoop(projectRoot, deps, { ...options, excludeTaskIds: dispatchedTaskIds, maxTasks: Math.min(concurrency, maxTasks - processed) });
|
|
215
|
+
if (plan.plannedOrder.length === 0) {
|
|
216
|
+
events.push({ kind: "supervisor.idle", at: at(deps), reason: plan.idleReason ?? "all-done" });
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
events.push({ kind: "supervisor.selection-planned", at: at(deps), taskIds: plan.plannedOrder, policy: options.selectionPolicy ?? "rank" });
|
|
220
|
+
plannedAll.push(...plan.plannedOrder);
|
|
221
|
+
if (options.dryRun)
|
|
222
|
+
break;
|
|
223
|
+
const dispatchTask = deps.dispatch ?? deps.dispatchRun;
|
|
224
|
+
if (!dispatchTask)
|
|
225
|
+
throw new Error("runSupervisorLoop requires dispatch when dryRun is false.");
|
|
226
|
+
if (!deps.awaitRunTerminal)
|
|
227
|
+
throw new Error("runSupervisorLoop requires awaitRunTerminal when dryRun is false.");
|
|
228
|
+
for (const entry of plan.ranked) {
|
|
229
|
+
const taskId = entry.task.id;
|
|
230
|
+
events.push({ kind: "supervisor.dispatch-started", at: at(deps), taskId, score: entry.score });
|
|
231
|
+
const dispatch = await dispatchTask({
|
|
232
|
+
projectRoot,
|
|
233
|
+
taskId: entry.task.id,
|
|
234
|
+
...entry.task.title !== undefined ? { title: entry.task.title } : {},
|
|
235
|
+
...options.model !== undefined ? { model: options.model } : {},
|
|
236
|
+
...options.force !== undefined ? { force: options.force } : {}
|
|
237
|
+
});
|
|
238
|
+
dispatchedTaskIds.add(entry.task.id);
|
|
239
|
+
const runId = dispatch.runId;
|
|
240
|
+
events.push({ kind: "supervisor.dispatch-confirmed", at: at(deps), taskId, runId });
|
|
241
|
+
const outcome = await deps.awaitRunTerminal(projectRoot, dispatch.runId, entry.task.id);
|
|
242
|
+
const outcomeFailed = failedOutcome(outcome);
|
|
243
|
+
if (outcomeFailed)
|
|
244
|
+
failed += 1;
|
|
245
|
+
else
|
|
246
|
+
succeeded += 1;
|
|
247
|
+
processed += 1;
|
|
248
|
+
events.push({
|
|
249
|
+
kind: "supervisor.outcome",
|
|
250
|
+
at: at(deps),
|
|
251
|
+
taskId,
|
|
252
|
+
runId,
|
|
253
|
+
status: runStatus(outcome.status),
|
|
254
|
+
failed: outcomeFailed,
|
|
255
|
+
unblockedTaskIds: [...outcome.unblockedTaskIds ?? []]
|
|
256
|
+
});
|
|
257
|
+
if (options.failFast && outcomeFailed || options.pauseOnAttention && outcomeFailed) {
|
|
258
|
+
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" });
|
|
259
|
+
processed = maxTasks;
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
if (processed >= maxTasks)
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const projectionBeforeFinish = reduceSupervisorJournal(events);
|
|
267
|
+
const idleReason = projectionBeforeFinish.idleReason ?? (processed >= maxTasks && Number.isFinite(maxTasks) ? "max-tasks" : null);
|
|
268
|
+
events.push({ kind: "supervisor.finished", at: at(deps), processed, succeeded, failed, skipped, idleReason });
|
|
269
|
+
const projection = reduceSupervisorJournal(events);
|
|
270
|
+
return { ok: failed === 0, dryRun: options.dryRun === true, plannedOrder: plannedAll, events, projection };
|
|
271
|
+
}
|
|
272
|
+
function collectBlockingClosure(taskId, badges) {
|
|
273
|
+
const closure = new Set;
|
|
274
|
+
const visit = (currentTaskId) => {
|
|
275
|
+
for (const blockerId of badges.get(currentTaskId)?.blockedBy ?? []) {
|
|
276
|
+
if (closure.has(blockerId))
|
|
277
|
+
continue;
|
|
278
|
+
closure.add(blockerId);
|
|
279
|
+
visit(blockerId);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
visit(taskId);
|
|
283
|
+
return closure;
|
|
284
|
+
}
|
|
285
|
+
async function unblockTask(projectRoot, taskId, deps, options = {}) {
|
|
286
|
+
if (taskId === null)
|
|
287
|
+
return runSupervisorLoop(projectRoot, deps, { ...options, selectionPolicy: "max-unblock", maxTasks: 1 });
|
|
288
|
+
const tasks = await deps.listTasks(projectRoot);
|
|
289
|
+
const projected = tasks.map(toTaskDependencyProjection);
|
|
290
|
+
const badges = computeTaskDependencyBadges(projected);
|
|
291
|
+
const blockers = collectBlockingClosure(taskId, badges);
|
|
292
|
+
return runSupervisorLoop(projectRoot, deps, { ...options, candidateTaskIds: blockers, selectionPolicy: "rank", maxTasks: 1 });
|
|
293
|
+
}
|
|
294
|
+
|
|
40
295
|
// packages/supervisor-plugin/src/cli.ts
|
|
41
296
|
var SUPERVISOR_LOOP_CLI_ID = "supervisor.loop";
|
|
42
297
|
var SUPERVISOR_UNBLOCK_CLI_ID = "supervisor.unblock";
|
|
@@ -106,7 +361,12 @@ function delay(ms) {
|
|
|
106
361
|
return promise;
|
|
107
362
|
}
|
|
108
363
|
async function loadSupervisorClient() {
|
|
109
|
-
|
|
364
|
+
const [taskIo, runIo, dispatchIo] = await Promise.all([
|
|
365
|
+
import("@rig/core/task-io"),
|
|
366
|
+
import("@rig/run-worker/runs"),
|
|
367
|
+
import("@rig/runtime/control-plane/dispatch")
|
|
368
|
+
]);
|
|
369
|
+
return { listTasks: taskIo.listTasks, listRuns: runIo.listRuns, dispatchRun: dispatchIo.dispatchRun };
|
|
110
370
|
}
|
|
111
371
|
function supervisorDeps(timeoutMs) {
|
|
112
372
|
return {
|
|
@@ -149,8 +409,7 @@ async function executeLoop(context, args) {
|
|
|
149
409
|
const stopWhen = takeOption(task.rest, "--stop-when");
|
|
150
410
|
const timeout = takeOption(stopWhen.rest, "--timeout-ms");
|
|
151
411
|
requireNoExtraArgs(timeout.rest, "rig loop [--task <id>] [--max-tasks <n>] [--concurrency <n>] [--stop-when <csv>] [--dry-run] [--json]");
|
|
152
|
-
const
|
|
153
|
-
const result = await runSupervisorLoop(context.projectRoot, supervisorDeps(parsePositiveInt(timeout.value, "--timeout-ms", 1800000)), {
|
|
412
|
+
const result = await runSupervisorLoop(context.projectRoot, supervisorDeps(parsePositiveInt(timeout.value, "--timeout-ms", 30 * 60 * 1000)), {
|
|
154
413
|
maxTasks: parsePositiveInt(maxTasks.value, "--max-tasks", 1),
|
|
155
414
|
concurrency: parsePositiveInt(concurrency.value, "--concurrency", 1),
|
|
156
415
|
...task.value ? { candidateTaskIds: [task.value] } : {},
|
|
@@ -171,8 +430,7 @@ async function executeUnblock(context, args) {
|
|
|
171
430
|
const timeout = takeOption(json.rest, "--timeout-ms");
|
|
172
431
|
const taskId = timeout.rest[0]?.startsWith("-") ? undefined : timeout.rest[0];
|
|
173
432
|
requireNoExtraArgs(taskId ? timeout.rest.slice(1) : timeout.rest, "rig unblock [task-id] [--dry-run] [--json]");
|
|
174
|
-
const
|
|
175
|
-
const result = await unblockTask(context.projectRoot, taskId ?? null, supervisorDeps(parsePositiveInt(timeout.value, "--timeout-ms", 1800000)), { dryRun: dry.value || context.dryRun });
|
|
433
|
+
const result = await unblockTask(context.projectRoot, taskId ?? null, supervisorDeps(parsePositiveInt(timeout.value, "--timeout-ms", 30 * 60 * 1000)), { dryRun: dry.value || context.dryRun });
|
|
176
434
|
if (context.outputMode === "text") {
|
|
177
435
|
if (json.value)
|
|
178
436
|
printJson(result);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { StageContext, StageMutation, StageResult, StageRun, TaskClosureSummary } from "@rig/contracts";
|
|
1
|
+
import type { Stage, StageContext, StageMutation, StageResult, StageRun, TaskClosureSummary } from "@rig/contracts";
|
|
2
2
|
export declare const SUPERVISOR_CLOSURE_STAGE_ID = "supervisor-closure-observer";
|
|
3
3
|
export interface ClosureSummaryPort {
|
|
4
4
|
summarize(ctx: StageContext): Promise<TaskClosureSummary | null> | TaskClosureSummary | null;
|
|
@@ -19,4 +19,5 @@ export declare function createSupervisorClosureStage(): StageRun;
|
|
|
19
19
|
* supervisor projection's `closures` are populated by real closeouts.
|
|
20
20
|
*/
|
|
21
21
|
export declare function createDefaultSupervisorClosureStage(): StageRun;
|
|
22
|
+
export declare const supervisorClosureStage: Stage;
|
|
22
23
|
export declare const supervisorClosureStageMutation: StageMutation;
|
package/dist/src/closureStage.js
CHANGED
|
@@ -46,20 +46,22 @@ function createDefaultSupervisorClosureStage() {
|
|
|
46
46
|
return { kind: "continue", ctx };
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
|
+
var supervisorClosureStage = {
|
|
50
|
+
id: SUPERVISOR_CLOSURE_STAGE_ID,
|
|
51
|
+
kind: "observe",
|
|
52
|
+
after: ["source-closeout"],
|
|
53
|
+
before: ["journal-append"],
|
|
54
|
+
priority: 0,
|
|
55
|
+
protected: false
|
|
56
|
+
};
|
|
49
57
|
var supervisorClosureStageMutation = {
|
|
50
58
|
op: "insert",
|
|
51
59
|
contributedBy: "@rig/supervisor-plugin",
|
|
52
|
-
stage:
|
|
53
|
-
id: SUPERVISOR_CLOSURE_STAGE_ID,
|
|
54
|
-
kind: "observe",
|
|
55
|
-
after: ["source-closeout"],
|
|
56
|
-
before: ["journal-append"],
|
|
57
|
-
priority: 0,
|
|
58
|
-
protected: false
|
|
59
|
-
}
|
|
60
|
+
stage: supervisorClosureStage
|
|
60
61
|
};
|
|
61
62
|
export {
|
|
62
63
|
supervisorClosureStageMutation,
|
|
64
|
+
supervisorClosureStage,
|
|
63
65
|
createSupervisorClosureStage,
|
|
64
66
|
createDefaultSupervisorClosureStage,
|
|
65
67
|
createClosureStage,
|