@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
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ClientTaskProjection } from "@rig/contracts";
|
|
2
|
+
import type { TaskLike } from "@rig/core/task-io";
|
|
3
|
+
import type { RankedReadyTask } from "@rig/dependency-graph-plugin";
|
|
4
|
+
import type { RunControl } from "@rig/run-worker/runs";
|
|
5
|
+
import type { RunRecord, SupervisorEvent, SupervisorProjection, SupervisorSelectionPolicy, SupervisorStopReason } from "@rig/contracts";
|
|
6
|
+
export interface SupervisorLoopOptions {
|
|
7
|
+
readonly maxTasks?: number;
|
|
8
|
+
readonly concurrency?: number;
|
|
9
|
+
readonly selectionPolicy?: SupervisorSelectionPolicy;
|
|
10
|
+
readonly failFast?: boolean;
|
|
11
|
+
readonly pauseOnAttention?: boolean;
|
|
12
|
+
readonly dryRun?: boolean;
|
|
13
|
+
readonly model?: string | null;
|
|
14
|
+
readonly force?: boolean;
|
|
15
|
+
readonly excludeTaskIds?: Iterable<string>;
|
|
16
|
+
readonly candidateTaskIds?: Iterable<string>;
|
|
17
|
+
}
|
|
18
|
+
type SupervisorDispatch = (input: {
|
|
19
|
+
readonly projectRoot: string;
|
|
20
|
+
readonly taskId: string;
|
|
21
|
+
readonly title?: string | null;
|
|
22
|
+
readonly model?: string | null;
|
|
23
|
+
readonly force?: boolean;
|
|
24
|
+
}) => Promise<{
|
|
25
|
+
readonly runId: string;
|
|
26
|
+
}>;
|
|
27
|
+
export interface SupervisorLoopDeps {
|
|
28
|
+
readonly listTasks: (projectRoot: string) => Promise<readonly TaskLike[]>;
|
|
29
|
+
readonly listRuns?: (projectRoot: string) => Promise<readonly RunRecord[]>;
|
|
30
|
+
readonly dispatch?: SupervisorDispatch;
|
|
31
|
+
readonly dispatchRun?: SupervisorDispatch;
|
|
32
|
+
readonly awaitRunTerminal?: (projectRoot: string, runId: string, taskId: string) => Promise<RunTerminalOutcome>;
|
|
33
|
+
readonly now?: () => string;
|
|
34
|
+
}
|
|
35
|
+
export interface RunTerminalOutcome {
|
|
36
|
+
readonly status: string;
|
|
37
|
+
readonly failed?: boolean;
|
|
38
|
+
readonly unblockedTaskIds?: readonly string[];
|
|
39
|
+
}
|
|
40
|
+
export interface SupervisorLoopResult {
|
|
41
|
+
readonly ok: boolean;
|
|
42
|
+
readonly dryRun: boolean;
|
|
43
|
+
readonly plannedOrder: readonly string[];
|
|
44
|
+
readonly events: readonly SupervisorEvent[];
|
|
45
|
+
readonly projection: SupervisorProjection;
|
|
46
|
+
}
|
|
47
|
+
export declare function planSupervisorLoop(projectRoot: string, deps: Pick<SupervisorLoopDeps, "listTasks" | "listRuns">, options?: SupervisorLoopOptions): Promise<{
|
|
48
|
+
readonly plannedOrder: readonly string[];
|
|
49
|
+
readonly ranked: readonly RankedReadyTask<ClientTaskProjection>[];
|
|
50
|
+
readonly idleReason: SupervisorStopReason | null;
|
|
51
|
+
}>;
|
|
52
|
+
export declare function runSupervisorLoop(projectRoot: string, deps: SupervisorLoopDeps, options?: SupervisorLoopOptions): Promise<SupervisorLoopResult>;
|
|
53
|
+
export declare function controlSupervisorRun(projectRoot: string, runId: string, control: RunControl, deps: {
|
|
54
|
+
readonly deliverRunControl: (projectRoot: string, runId: string, control: RunControl) => Promise<void>;
|
|
55
|
+
}): Promise<{
|
|
56
|
+
readonly ok: true;
|
|
57
|
+
readonly runId: string;
|
|
58
|
+
readonly control: RunControl["kind"];
|
|
59
|
+
}>;
|
|
60
|
+
export declare function unblockTask(projectRoot: string, taskId: string | null, deps: SupervisorLoopDeps, options?: Omit<SupervisorLoopOptions, "selectionPolicy" | "maxTasks">): Promise<SupervisorLoopResult>;
|
|
61
|
+
export {};
|
package/dist/src/loop.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/supervisor-plugin/src/loop.ts
|
|
3
|
+
import { computeTaskDependencyBadges, toTaskDependencyProjection } from "@rig/contracts";
|
|
4
|
+
import { rankReadyTasks, selectRankedReadyTasks } from "@rig/dependency-graph-plugin";
|
|
5
|
+
|
|
6
|
+
// packages/supervisor-plugin/src/journal.ts
|
|
7
|
+
import { Schema } from "effect";
|
|
8
|
+
import {
|
|
9
|
+
SupervisorEvent
|
|
10
|
+
} from "@rig/contracts";
|
|
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
|
+
}
|
|
84
|
+
|
|
85
|
+
// packages/supervisor-plugin/src/loop.ts
|
|
86
|
+
import { classifyTasks, isHumanBlockerClass } from "@rig/blocker-classifier-plugin";
|
|
87
|
+
function selectionMode(policy) {
|
|
88
|
+
return policy === "blocking-only" || policy === "max-unblock" ? policy : "all-ready";
|
|
89
|
+
}
|
|
90
|
+
function at(deps) {
|
|
91
|
+
return deps.now ? deps.now() : new Date().toISOString();
|
|
92
|
+
}
|
|
93
|
+
function activeTaskIds(runs) {
|
|
94
|
+
const terminal = new Set(["completed", "failed", "stopped", "stale"]);
|
|
95
|
+
return new Set(runs.filter((run) => run.taskId && run.live && !run.stale && !terminal.has(run.status)).map((run) => run.taskId));
|
|
96
|
+
}
|
|
97
|
+
function stopReasonForNoCandidates(tasks, runs) {
|
|
98
|
+
if (tasks.length === 0)
|
|
99
|
+
return "all-done";
|
|
100
|
+
const classifications = classifyTasks(tasks, runs).classifications;
|
|
101
|
+
const nonTerminal = tasks.map(toTaskDependencyProjection).filter((task) => task.status !== "closed" && task.status !== "completed" && task.status !== "cancelled");
|
|
102
|
+
if (nonTerminal.length > 0 && nonTerminal.every((task) => {
|
|
103
|
+
const classification = classifications.find((entry) => entry.taskId === task.id);
|
|
104
|
+
return classification ? isHumanBlockerClass(classification.blockerClass) : false;
|
|
105
|
+
}))
|
|
106
|
+
return "all-human-blocked";
|
|
107
|
+
return "all-done";
|
|
108
|
+
}
|
|
109
|
+
async function planSupervisorLoop(projectRoot, deps, options = {}) {
|
|
110
|
+
const [tasks, runs] = await Promise.all([
|
|
111
|
+
deps.listTasks(projectRoot),
|
|
112
|
+
deps.listRuns ? deps.listRuns(projectRoot) : Promise.resolve([])
|
|
113
|
+
]);
|
|
114
|
+
const projected = tasks.map(toTaskDependencyProjection);
|
|
115
|
+
const excluded = new Set(options.excludeTaskIds ?? []);
|
|
116
|
+
const candidates = options.candidateTaskIds ? new Set(options.candidateTaskIds) : null;
|
|
117
|
+
const ranked = rankReadyTasks(projected, {
|
|
118
|
+
activeTaskIds: activeTaskIds(runs),
|
|
119
|
+
excludeTaskIds: excluded,
|
|
120
|
+
...candidates ? { filter: (task) => candidates.has(task.id) } : {},
|
|
121
|
+
selection: selectionMode(options.selectionPolicy)
|
|
122
|
+
});
|
|
123
|
+
const limit = Math.max(0, options.maxTasks ?? ranked.length);
|
|
124
|
+
const selected = selectRankedReadyTasks(projected, {
|
|
125
|
+
activeTaskIds: activeTaskIds(runs),
|
|
126
|
+
excludeTaskIds: excluded,
|
|
127
|
+
...candidates ? { filter: (task) => candidates.has(task.id) } : {},
|
|
128
|
+
selection: selectionMode(options.selectionPolicy),
|
|
129
|
+
requireDisjointScopes: (options.concurrency ?? 1) > 1,
|
|
130
|
+
limit
|
|
131
|
+
});
|
|
132
|
+
const plannedOrder = selected.map((task) => task.id);
|
|
133
|
+
return {
|
|
134
|
+
plannedOrder,
|
|
135
|
+
ranked: ranked.filter((entry) => plannedOrder.includes(entry.task.id)),
|
|
136
|
+
idleReason: plannedOrder.length === 0 ? stopReasonForNoCandidates(tasks, runs) : null
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function failedOutcome(outcome) {
|
|
140
|
+
if (typeof outcome.failed === "boolean")
|
|
141
|
+
return outcome.failed;
|
|
142
|
+
return outcome.status === "failed" || outcome.status === "stopped" || outcome.status === "needs-attention" || outcome.status === "waiting-approval" || outcome.status === "waiting-user-input";
|
|
143
|
+
}
|
|
144
|
+
function runStatus(value) {
|
|
145
|
+
switch (value) {
|
|
146
|
+
case "created":
|
|
147
|
+
case "queued":
|
|
148
|
+
case "preparing":
|
|
149
|
+
case "running":
|
|
150
|
+
case "waiting-approval":
|
|
151
|
+
case "waiting-user-input":
|
|
152
|
+
case "paused":
|
|
153
|
+
case "validating":
|
|
154
|
+
case "reviewing":
|
|
155
|
+
case "closing-out":
|
|
156
|
+
case "needs-attention":
|
|
157
|
+
case "completed":
|
|
158
|
+
case "failed":
|
|
159
|
+
case "stopped":
|
|
160
|
+
return value;
|
|
161
|
+
default:
|
|
162
|
+
return "failed";
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async function runSupervisorLoop(projectRoot, deps, options = {}) {
|
|
166
|
+
const events = [{ kind: "supervisor.started", at: at(deps), options }];
|
|
167
|
+
const maxTasks = Math.max(0, options.maxTasks ?? Number.POSITIVE_INFINITY);
|
|
168
|
+
const concurrency = Math.max(1, options.concurrency ?? 1);
|
|
169
|
+
let processed = 0;
|
|
170
|
+
let succeeded = 0;
|
|
171
|
+
let failed = 0;
|
|
172
|
+
let skipped = 0;
|
|
173
|
+
const plannedAll = [];
|
|
174
|
+
const dispatchedTaskIds = new Set(options.excludeTaskIds ?? []);
|
|
175
|
+
while (processed < maxTasks) {
|
|
176
|
+
const plan = await planSupervisorLoop(projectRoot, deps, { ...options, excludeTaskIds: dispatchedTaskIds, maxTasks: Math.min(concurrency, maxTasks - processed) });
|
|
177
|
+
if (plan.plannedOrder.length === 0) {
|
|
178
|
+
events.push({ kind: "supervisor.idle", at: at(deps), reason: plan.idleReason ?? "all-done" });
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
events.push({ kind: "supervisor.selection-planned", at: at(deps), taskIds: plan.plannedOrder, policy: options.selectionPolicy ?? "rank" });
|
|
182
|
+
plannedAll.push(...plan.plannedOrder);
|
|
183
|
+
if (options.dryRun)
|
|
184
|
+
break;
|
|
185
|
+
const dispatchTask = deps.dispatch ?? deps.dispatchRun;
|
|
186
|
+
if (!dispatchTask)
|
|
187
|
+
throw new Error("runSupervisorLoop requires dispatch when dryRun is false.");
|
|
188
|
+
if (!deps.awaitRunTerminal)
|
|
189
|
+
throw new Error("runSupervisorLoop requires awaitRunTerminal when dryRun is false.");
|
|
190
|
+
for (const entry of plan.ranked) {
|
|
191
|
+
const taskId = entry.task.id;
|
|
192
|
+
events.push({ kind: "supervisor.dispatch-started", at: at(deps), taskId, score: entry.score });
|
|
193
|
+
const dispatch = await dispatchTask({
|
|
194
|
+
projectRoot,
|
|
195
|
+
taskId: entry.task.id,
|
|
196
|
+
...entry.task.title !== undefined ? { title: entry.task.title } : {},
|
|
197
|
+
...options.model !== undefined ? { model: options.model } : {},
|
|
198
|
+
...options.force !== undefined ? { force: options.force } : {}
|
|
199
|
+
});
|
|
200
|
+
dispatchedTaskIds.add(entry.task.id);
|
|
201
|
+
const runId = dispatch.runId;
|
|
202
|
+
events.push({ kind: "supervisor.dispatch-confirmed", at: at(deps), taskId, runId });
|
|
203
|
+
const outcome = await deps.awaitRunTerminal(projectRoot, dispatch.runId, entry.task.id);
|
|
204
|
+
const outcomeFailed = failedOutcome(outcome);
|
|
205
|
+
if (outcomeFailed)
|
|
206
|
+
failed += 1;
|
|
207
|
+
else
|
|
208
|
+
succeeded += 1;
|
|
209
|
+
processed += 1;
|
|
210
|
+
events.push({
|
|
211
|
+
kind: "supervisor.outcome",
|
|
212
|
+
at: at(deps),
|
|
213
|
+
taskId,
|
|
214
|
+
runId,
|
|
215
|
+
status: runStatus(outcome.status),
|
|
216
|
+
failed: outcomeFailed,
|
|
217
|
+
unblockedTaskIds: [...outcome.unblockedTaskIds ?? []]
|
|
218
|
+
});
|
|
219
|
+
if (options.failFast && outcomeFailed || options.pauseOnAttention && outcomeFailed) {
|
|
220
|
+
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" });
|
|
221
|
+
processed = maxTasks;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
if (processed >= maxTasks)
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const projectionBeforeFinish = reduceSupervisorJournal(events);
|
|
229
|
+
const idleReason = projectionBeforeFinish.idleReason ?? (processed >= maxTasks && Number.isFinite(maxTasks) ? "max-tasks" : null);
|
|
230
|
+
events.push({ kind: "supervisor.finished", at: at(deps), processed, succeeded, failed, skipped, idleReason });
|
|
231
|
+
const projection = reduceSupervisorJournal(events);
|
|
232
|
+
return { ok: failed === 0, dryRun: options.dryRun === true, plannedOrder: plannedAll, events, projection };
|
|
233
|
+
}
|
|
234
|
+
async function controlSupervisorRun(projectRoot, runId, control, deps) {
|
|
235
|
+
await deps.deliverRunControl(projectRoot, runId, control);
|
|
236
|
+
return { ok: true, runId, control: control.kind };
|
|
237
|
+
}
|
|
238
|
+
function collectBlockingClosure(taskId, badges) {
|
|
239
|
+
const closure = new Set;
|
|
240
|
+
const visit = (currentTaskId) => {
|
|
241
|
+
for (const blockerId of badges.get(currentTaskId)?.blockedBy ?? []) {
|
|
242
|
+
if (closure.has(blockerId))
|
|
243
|
+
continue;
|
|
244
|
+
closure.add(blockerId);
|
|
245
|
+
visit(blockerId);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
visit(taskId);
|
|
249
|
+
return closure;
|
|
250
|
+
}
|
|
251
|
+
async function unblockTask(projectRoot, taskId, deps, options = {}) {
|
|
252
|
+
if (taskId === null)
|
|
253
|
+
return runSupervisorLoop(projectRoot, deps, { ...options, selectionPolicy: "max-unblock", maxTasks: 1 });
|
|
254
|
+
const tasks = await deps.listTasks(projectRoot);
|
|
255
|
+
const projected = tasks.map(toTaskDependencyProjection);
|
|
256
|
+
const badges = computeTaskDependencyBadges(projected);
|
|
257
|
+
const blockers = collectBlockingClosure(taskId, badges);
|
|
258
|
+
return runSupervisorLoop(projectRoot, deps, { ...options, candidateTaskIds: blockers, selectionPolicy: "rank", maxTasks: 1 });
|
|
259
|
+
}
|
|
260
|
+
export {
|
|
261
|
+
unblockTask,
|
|
262
|
+
runSupervisorLoop,
|
|
263
|
+
planSupervisorLoop,
|
|
264
|
+
controlSupervisorRun
|
|
265
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type WorkerPanelProducerContext } from "@rig/contracts";
|
|
2
|
+
/**
|
|
3
|
+
* Builds the live supervisor-panel payload from a folded run-journal
|
|
4
|
+
* projection. Moved here from the run-worker facade so the supervisor panel is
|
|
5
|
+
* a real first-party plugin contribution (architecture two-category cutover).
|
|
6
|
+
*/
|
|
7
|
+
export declare function buildSupervisorPanelPayload(context: WorkerPanelProducerContext): unknown;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/supervisor-plugin/src/panel.ts
|
|
3
|
+
import { RIG_RUN_STOP_PANEL_ACTION } from "@rig/contracts";
|
|
4
|
+
function buildSupervisorPanelPayload(context) {
|
|
5
|
+
const status = context.folded.status ?? "unknown";
|
|
6
|
+
const taskId = context.folded.record.taskId ?? context.taskIdAtStart;
|
|
7
|
+
const operatorActive = status === "running" || status === "validating" || status === "closing-out" || status === "needs-attention";
|
|
8
|
+
return {
|
|
9
|
+
status,
|
|
10
|
+
currentTask: taskId ? { id: taskId, title: context.runDisplayTitle } : null,
|
|
11
|
+
processed: context.folded.closeoutPhases.length,
|
|
12
|
+
succeeded: context.folded.closeoutPhases.filter((phase) => phase.outcome === "completed").length,
|
|
13
|
+
failed: context.folded.closeoutPhases.filter((phase) => phase.outcome === "failed").length,
|
|
14
|
+
skipped: 0,
|
|
15
|
+
plannedOrder: taskId ? [{ id: taskId, title: context.runDisplayTitle, status }] : [],
|
|
16
|
+
idleReason: operatorActive ? null : status,
|
|
17
|
+
stopActionId: operatorActive ? RIG_RUN_STOP_PANEL_ACTION : null,
|
|
18
|
+
closures: []
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export {
|
|
22
|
+
buildSupervisorPanelPayload
|
|
23
|
+
};
|
package/dist/src/plugin.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export declare const SUPERVISOR_PLUGIN_NAME = "@rig/supervisor-plugin";
|
|
2
|
-
export declare const supervisorPlugin: import("@rig/core").
|
|
3
|
-
export declare function createSupervisorPlugin(): import("@rig/core").
|
|
2
|
+
export declare const supervisorPlugin: import("@rig/core").RigPlugin;
|
|
3
|
+
export declare function createSupervisorPlugin(): import("@rig/core").RigPlugin;
|
|
4
4
|
export default supervisorPlugin;
|