@h-rig/supervisor-plugin 0.0.6-alpha.133
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/README.md +1 -0
- package/dist/src/awaiter.d.ts +15 -0
- package/dist/src/awaiter.js +39 -0
- package/dist/src/closureStage.d.ts +8 -0
- package/dist/src/closureStage.js +30 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +310 -0
- package/dist/src/journal.d.ts +14 -0
- package/dist/src/journal.js +64 -0
- package/dist/src/plugin.d.ts +3 -0
- package/dist/src/plugin.js +36 -0
- package/dist/src/supervisor.d.ts +57 -0
- package/dist/src/supervisor.js +172 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# @h-rig/supervisor-plugin
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RunId, RunStatus } from "@rig/contracts";
|
|
2
|
+
import type { SupervisorRunOutcome } from "./supervisor";
|
|
3
|
+
export interface RunStatusSnapshot {
|
|
4
|
+
readonly runId: RunId;
|
|
5
|
+
readonly status: RunStatus;
|
|
6
|
+
readonly diagnostic?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface SupervisorAwaitTerminalOptions {
|
|
9
|
+
readonly runId: RunId;
|
|
10
|
+
readonly readStatus: (runId: RunId) => Promise<RunStatusSnapshot | null> | RunStatusSnapshot | null;
|
|
11
|
+
readonly waitForChange: (timeoutMs: number) => Promise<void>;
|
|
12
|
+
readonly pollMs?: number;
|
|
13
|
+
readonly timeoutMs?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function awaitTerminalRun(options: SupervisorAwaitTerminalOptions): Promise<SupervisorRunOutcome>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/supervisor-plugin/src/awaiter.ts
|
|
3
|
+
var TERMINAL_RUN_STATUSES = {
|
|
4
|
+
created: false,
|
|
5
|
+
queued: false,
|
|
6
|
+
preparing: false,
|
|
7
|
+
running: false,
|
|
8
|
+
"waiting-approval": false,
|
|
9
|
+
"waiting-user-input": false,
|
|
10
|
+
paused: false,
|
|
11
|
+
validating: false,
|
|
12
|
+
reviewing: false,
|
|
13
|
+
"closing-out": false,
|
|
14
|
+
"needs-attention": true,
|
|
15
|
+
completed: true,
|
|
16
|
+
failed: true,
|
|
17
|
+
stopped: true
|
|
18
|
+
};
|
|
19
|
+
async function awaitTerminalRun(options) {
|
|
20
|
+
const startedAt = Date.now();
|
|
21
|
+
const pollMs = Math.max(0, Math.floor(options.pollMs ?? 5000));
|
|
22
|
+
while (true) {
|
|
23
|
+
const snapshot = await options.readStatus(options.runId);
|
|
24
|
+
if (snapshot && TERMINAL_RUN_STATUSES[snapshot.status]) {
|
|
25
|
+
return {
|
|
26
|
+
runId: options.runId,
|
|
27
|
+
status: snapshot.status,
|
|
28
|
+
failed: snapshot.status !== "completed"
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (options.timeoutMs !== undefined && Date.now() - startedAt >= options.timeoutMs) {
|
|
32
|
+
return { runId: options.runId, status: "needs-attention", failed: true };
|
|
33
|
+
}
|
|
34
|
+
await options.waitForChange(pollMs);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
awaitTerminalRun
|
|
39
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { StageContext, StageMutation, StageResult, TaskClosureSummary } from "@rig/contracts";
|
|
2
|
+
export declare const SUPERVISOR_CLOSURE_STAGE_ID = "supervisor-closure-observer";
|
|
3
|
+
export interface ClosureSummaryPort {
|
|
4
|
+
summarize(ctx: StageContext): Promise<TaskClosureSummary | null> | TaskClosureSummary | null;
|
|
5
|
+
record?(summary: TaskClosureSummary): Promise<void> | void;
|
|
6
|
+
}
|
|
7
|
+
export declare function createClosureStage(port: ClosureSummaryPort): (ctx: StageContext) => Promise<StageResult>;
|
|
8
|
+
export declare const supervisorClosureStageMutation: StageMutation;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/supervisor-plugin/src/closureStage.ts
|
|
3
|
+
var SUPERVISOR_CLOSURE_STAGE_ID = "supervisor-closure-observer";
|
|
4
|
+
function createClosureStage(port) {
|
|
5
|
+
return async (ctx) => {
|
|
6
|
+
const summary = await port.summarize(ctx);
|
|
7
|
+
if (!summary)
|
|
8
|
+
return { kind: "continue", ctx };
|
|
9
|
+
await port.record?.(summary);
|
|
10
|
+
const metadata = { ...ctx.metadata ?? {}, supervisorClosure: summary };
|
|
11
|
+
return { kind: "continue", ctx: { ...ctx, metadata } };
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
var supervisorClosureStageMutation = {
|
|
15
|
+
op: "insert",
|
|
16
|
+
contributedBy: "@rig/supervisor-plugin",
|
|
17
|
+
stage: {
|
|
18
|
+
id: SUPERVISOR_CLOSURE_STAGE_ID,
|
|
19
|
+
kind: "observe",
|
|
20
|
+
after: ["source-closeout"],
|
|
21
|
+
before: ["journal-append"],
|
|
22
|
+
priority: 0,
|
|
23
|
+
protected: false
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
export {
|
|
27
|
+
supervisorClosureStageMutation,
|
|
28
|
+
createClosureStage,
|
|
29
|
+
SUPERVISOR_CLOSURE_STAGE_ID
|
|
30
|
+
};
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/supervisor-plugin/src/awaiter.ts
|
|
3
|
+
var TERMINAL_RUN_STATUSES = {
|
|
4
|
+
created: false,
|
|
5
|
+
queued: false,
|
|
6
|
+
preparing: false,
|
|
7
|
+
running: false,
|
|
8
|
+
"waiting-approval": false,
|
|
9
|
+
"waiting-user-input": false,
|
|
10
|
+
paused: false,
|
|
11
|
+
validating: false,
|
|
12
|
+
reviewing: false,
|
|
13
|
+
"closing-out": false,
|
|
14
|
+
"needs-attention": true,
|
|
15
|
+
completed: true,
|
|
16
|
+
failed: true,
|
|
17
|
+
stopped: true
|
|
18
|
+
};
|
|
19
|
+
async function awaitTerminalRun(options) {
|
|
20
|
+
const startedAt = Date.now();
|
|
21
|
+
const pollMs = Math.max(0, Math.floor(options.pollMs ?? 5000));
|
|
22
|
+
while (true) {
|
|
23
|
+
const snapshot = await options.readStatus(options.runId);
|
|
24
|
+
if (snapshot && TERMINAL_RUN_STATUSES[snapshot.status]) {
|
|
25
|
+
return {
|
|
26
|
+
runId: options.runId,
|
|
27
|
+
status: snapshot.status,
|
|
28
|
+
failed: snapshot.status !== "completed"
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (options.timeoutMs !== undefined && Date.now() - startedAt >= options.timeoutMs) {
|
|
32
|
+
return { runId: options.runId, status: "needs-attention", failed: true };
|
|
33
|
+
}
|
|
34
|
+
await options.waitForChange(pollMs);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// packages/supervisor-plugin/src/closureStage.ts
|
|
38
|
+
var SUPERVISOR_CLOSURE_STAGE_ID = "supervisor-closure-observer";
|
|
39
|
+
function createClosureStage(port) {
|
|
40
|
+
return async (ctx) => {
|
|
41
|
+
const summary = await port.summarize(ctx);
|
|
42
|
+
if (!summary)
|
|
43
|
+
return { kind: "continue", ctx };
|
|
44
|
+
await port.record?.(summary);
|
|
45
|
+
const metadata = { ...ctx.metadata ?? {}, supervisorClosure: summary };
|
|
46
|
+
return { kind: "continue", ctx: { ...ctx, metadata } };
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
var supervisorClosureStageMutation = {
|
|
50
|
+
op: "insert",
|
|
51
|
+
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
|
+
};
|
|
61
|
+
// packages/supervisor-plugin/src/journal.ts
|
|
62
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
63
|
+
import { dirname } from "path";
|
|
64
|
+
import { Schema } from "effect";
|
|
65
|
+
import {
|
|
66
|
+
SupervisorEvent,
|
|
67
|
+
reduceSupervisorJournal
|
|
68
|
+
} from "@rig/contracts";
|
|
69
|
+
var NEWLINE = `
|
|
70
|
+
`;
|
|
71
|
+
function createFileSupervisorJournal(path) {
|
|
72
|
+
return {
|
|
73
|
+
async append(line) {
|
|
74
|
+
await mkdir(dirname(path), { recursive: true });
|
|
75
|
+
await writeFile(path, `${line}${NEWLINE}`, { flag: "a" });
|
|
76
|
+
},
|
|
77
|
+
async read() {
|
|
78
|
+
try {
|
|
79
|
+
return await readFile(path, "utf8");
|
|
80
|
+
} catch (error) {
|
|
81
|
+
const code = typeof error === "object" && error !== null && "code" in error ? error.code : undefined;
|
|
82
|
+
if (code === "ENOENT")
|
|
83
|
+
return "";
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function createInMemorySupervisorJournalStore(seed = []) {
|
|
90
|
+
const lines = seed.map((event) => JSON.stringify(event));
|
|
91
|
+
return {
|
|
92
|
+
async append(line) {
|
|
93
|
+
lines.push(line);
|
|
94
|
+
},
|
|
95
|
+
async read() {
|
|
96
|
+
return lines.join(NEWLINE);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function parseSupervisorJournal(text) {
|
|
101
|
+
return text.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0).map((line, index) => {
|
|
102
|
+
try {
|
|
103
|
+
return Schema.decodeUnknownSync(SupervisorEvent)(JSON.parse(line));
|
|
104
|
+
} catch (error) {
|
|
105
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
106
|
+
throw new Error(`Invalid supervisor journal line ${index + 1}: ${message}`);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function createSupervisorJournal(store) {
|
|
111
|
+
const append = async (event) => {
|
|
112
|
+
await store.append(JSON.stringify(Schema.decodeUnknownSync(SupervisorEvent)(event)));
|
|
113
|
+
};
|
|
114
|
+
const readEvents = async () => parseSupervisorJournal(await store.read());
|
|
115
|
+
const readProjection = async () => reduceSupervisorJournal(await readEvents());
|
|
116
|
+
return { append, readEvents, readProjection };
|
|
117
|
+
}
|
|
118
|
+
// packages/supervisor-plugin/src/plugin.ts
|
|
119
|
+
import { definePlugin } from "@rig/core";
|
|
120
|
+
var SUPERVISOR_PLUGIN_NAME = "@rig/supervisor-plugin";
|
|
121
|
+
var supervisorPlugin = definePlugin({
|
|
122
|
+
name: SUPERVISOR_PLUGIN_NAME,
|
|
123
|
+
version: "0.0.0-alpha.1",
|
|
124
|
+
provides: [],
|
|
125
|
+
requires: ["journal", "transport"],
|
|
126
|
+
contributes: {
|
|
127
|
+
stageMutations: [supervisorClosureStageMutation]
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
// packages/supervisor-plugin/src/supervisor.ts
|
|
131
|
+
import {
|
|
132
|
+
computeTaskDependencyBadges,
|
|
133
|
+
disjointScope,
|
|
134
|
+
isTaskTerminalStatus,
|
|
135
|
+
rankReadyTasks,
|
|
136
|
+
readTaskScope
|
|
137
|
+
} from "@rig/core/task-graph";
|
|
138
|
+
var DEFAULT_STOP_WHEN = new Set(["all-done", "all-human-blocked", "source-error"]);
|
|
139
|
+
var HUMAN_BLOCKER_CLASS = {
|
|
140
|
+
"not-blocked": false,
|
|
141
|
+
"task-blocked": false,
|
|
142
|
+
"human-decision": true,
|
|
143
|
+
"human-approval": true,
|
|
144
|
+
"external-input": true,
|
|
145
|
+
unknown: true
|
|
146
|
+
};
|
|
147
|
+
function normalizeOptions(options = {}) {
|
|
148
|
+
return {
|
|
149
|
+
concurrency: Math.max(1, Math.floor(options.concurrency ?? 1)),
|
|
150
|
+
selectionPolicy: options.selectionPolicy ?? "rank",
|
|
151
|
+
maxTasks: options.maxTasks === undefined ? null : Math.max(0, Math.floor(options.maxTasks)),
|
|
152
|
+
...options.filter ? { filter: options.filter } : {},
|
|
153
|
+
stopWhen: options.stopWhen ?? DEFAULT_STOP_WHEN,
|
|
154
|
+
pauseOnAttention: options.pauseOnAttention ?? true,
|
|
155
|
+
failFast: options.failFast ?? false,
|
|
156
|
+
pollMs: Math.max(0, Math.floor(options.pollMs ?? 5000))
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function selectionMode(policy) {
|
|
160
|
+
if (policy === "blocking-only")
|
|
161
|
+
return "blocking-only";
|
|
162
|
+
if (policy === "max-unblock")
|
|
163
|
+
return "max-unblock";
|
|
164
|
+
return "all-ready";
|
|
165
|
+
}
|
|
166
|
+
function dispatchHandle(result) {
|
|
167
|
+
return typeof result === "string" ? { runId: result } : result;
|
|
168
|
+
}
|
|
169
|
+
function filterTask(task, filter) {
|
|
170
|
+
if (!filter)
|
|
171
|
+
return true;
|
|
172
|
+
if (filter.statuses && !filter.statuses.includes(task.status))
|
|
173
|
+
return false;
|
|
174
|
+
if (filter.workspaceIds && !filter.workspaceIds.includes(task.workspaceId))
|
|
175
|
+
return false;
|
|
176
|
+
if (filter.assignees) {
|
|
177
|
+
const raw = task.metadata;
|
|
178
|
+
const assignee = typeof raw === "object" && raw !== null && "assignee" in raw && typeof raw.assignee === "string" ? raw.assignee : null;
|
|
179
|
+
if (!assignee || !filter.assignees.includes(assignee))
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
function classifyEmptyReadySet(snapshot, badges, classifyBlocker) {
|
|
185
|
+
if ((snapshot.diagnostics ?? []).length > 0)
|
|
186
|
+
return "source-error";
|
|
187
|
+
const openTasks = snapshot.tasks.filter((task) => !isTaskTerminalStatus(task.status));
|
|
188
|
+
if (openTasks.length === 0)
|
|
189
|
+
return "all-done";
|
|
190
|
+
if (classifyBlocker) {
|
|
191
|
+
const allHumanBlocked = openTasks.every((task) => HUMAN_BLOCKER_CLASS[classifyBlocker(task, badges).blockerClass]);
|
|
192
|
+
if (allHumanBlocked)
|
|
193
|
+
return "all-human-blocked";
|
|
194
|
+
}
|
|
195
|
+
return "all-done";
|
|
196
|
+
}
|
|
197
|
+
function terminalFailed(outcome) {
|
|
198
|
+
return outcome.failed ?? ["failed", "stopped", "needs-attention"].includes(outcome.status);
|
|
199
|
+
}
|
|
200
|
+
async function runSupervisor(ctx, options = {}) {
|
|
201
|
+
const normalized = normalizeOptions(options);
|
|
202
|
+
const now = () => (ctx.now?.() ?? new Date).toISOString();
|
|
203
|
+
const emit = async (event) => {
|
|
204
|
+
await ctx.emitEvent?.(event);
|
|
205
|
+
};
|
|
206
|
+
let processed = 0;
|
|
207
|
+
let succeeded = 0;
|
|
208
|
+
let failed = 0;
|
|
209
|
+
let skipped = 0;
|
|
210
|
+
let idleReason = null;
|
|
211
|
+
const completedTaskIds = new Set;
|
|
212
|
+
await emit({ kind: "supervisor.started", at: now(), options: normalized });
|
|
213
|
+
while (!ctx.shouldStop?.()) {
|
|
214
|
+
if (normalized.maxTasks !== null && processed >= normalized.maxTasks) {
|
|
215
|
+
idleReason = "max-tasks";
|
|
216
|
+
await emit({ kind: "supervisor.idle", at: now(), reason: idleReason });
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
const snapshot = await ctx.readTasks();
|
|
220
|
+
const badges = computeTaskDependencyBadges(snapshot.tasks);
|
|
221
|
+
const activeRuns = snapshot.activeRuns ?? [];
|
|
222
|
+
const activeTaskIds = activeRuns.map((run) => run.taskId);
|
|
223
|
+
const remainingSlots = normalized.maxTasks === null ? normalized.concurrency : Math.min(normalized.concurrency, normalized.maxTasks - processed);
|
|
224
|
+
const ranked = rankReadyTasks(snapshot.tasks, {
|
|
225
|
+
excludeTaskIds: completedTaskIds,
|
|
226
|
+
activeTaskIds,
|
|
227
|
+
filter: (task) => filterTask(task, normalized.filter),
|
|
228
|
+
selection: selectionMode(normalized.selectionPolicy)
|
|
229
|
+
});
|
|
230
|
+
const selected = [];
|
|
231
|
+
const occupiedScopes = activeRuns.flatMap((run) => {
|
|
232
|
+
const task = snapshot.tasks.find((candidate) => candidate.id === run.taskId);
|
|
233
|
+
return task ? readTaskScope(task) : [];
|
|
234
|
+
});
|
|
235
|
+
for (const entry of ranked) {
|
|
236
|
+
if (selected.length >= remainingSlots)
|
|
237
|
+
break;
|
|
238
|
+
if (!disjointScope(entry.scope, occupiedScopes))
|
|
239
|
+
continue;
|
|
240
|
+
selected.push(entry);
|
|
241
|
+
occupiedScopes.push(...entry.scope);
|
|
242
|
+
}
|
|
243
|
+
await emit({ kind: "supervisor.selection-planned", at: now(), taskIds: selected.map((entry) => entry.task.id), policy: normalized.selectionPolicy });
|
|
244
|
+
if (selected.length === 0) {
|
|
245
|
+
idleReason = classifyEmptyReadySet(snapshot, badges, ctx.classifyBlocker);
|
|
246
|
+
await emit({ kind: "supervisor.idle", at: now(), reason: idleReason });
|
|
247
|
+
if (normalized.stopWhen.has(idleReason))
|
|
248
|
+
break;
|
|
249
|
+
await ctx.waitForChange(normalized.pollMs);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const outcomes = await Promise.all(selected.map(async (entry) => {
|
|
253
|
+
await emit({ kind: "supervisor.dispatch-started", at: now(), taskId: entry.task.id, score: Math.max(0, Math.floor(entry.score)) });
|
|
254
|
+
const handle = dispatchHandle(await ctx.dispatch(entry.task));
|
|
255
|
+
await emit({
|
|
256
|
+
kind: "supervisor.dispatch-confirmed",
|
|
257
|
+
at: now(),
|
|
258
|
+
taskId: entry.task.id,
|
|
259
|
+
runId: handle.runId,
|
|
260
|
+
...handle.dispatchHandle === undefined ? {} : { dispatchHandle: handle.dispatchHandle }
|
|
261
|
+
});
|
|
262
|
+
const outcome = await ctx.awaitTerminal(handle.runId, entry.task);
|
|
263
|
+
const failedOutcome = terminalFailed(outcome);
|
|
264
|
+
await emit({
|
|
265
|
+
kind: "supervisor.outcome",
|
|
266
|
+
at: now(),
|
|
267
|
+
taskId: entry.task.id,
|
|
268
|
+
runId: outcome.runId,
|
|
269
|
+
status: outcome.status,
|
|
270
|
+
failed: failedOutcome,
|
|
271
|
+
unblockedTaskIds: outcome.closure?.unblockedTaskIds ?? [],
|
|
272
|
+
...outcome.closure ? { closure: outcome.closure } : {}
|
|
273
|
+
});
|
|
274
|
+
return { taskId: entry.task.id, status: outcome.status, failed: failedOutcome };
|
|
275
|
+
}));
|
|
276
|
+
for (const outcome of outcomes) {
|
|
277
|
+
completedTaskIds.add(outcome.taskId);
|
|
278
|
+
processed += 1;
|
|
279
|
+
if (outcome.failed)
|
|
280
|
+
failed += 1;
|
|
281
|
+
else
|
|
282
|
+
succeeded += 1;
|
|
283
|
+
}
|
|
284
|
+
if (normalized.pauseOnAttention && outcomes.some((outcome) => outcome.status === "needs-attention")) {
|
|
285
|
+
idleReason = "operator-stop";
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
if (normalized.failFast && outcomes.some((outcome) => outcome.failed))
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
if (ctx.shouldStop?.()) {
|
|
292
|
+
idleReason = "operator-stop";
|
|
293
|
+
await emit({ kind: "supervisor.stopped", at: now(), reason: idleReason });
|
|
294
|
+
}
|
|
295
|
+
await emit({ kind: "supervisor.finished", at: now(), processed, succeeded, failed, skipped, idleReason });
|
|
296
|
+
return { processed, succeeded, failed, skipped, idleReason };
|
|
297
|
+
}
|
|
298
|
+
export {
|
|
299
|
+
supervisorPlugin,
|
|
300
|
+
supervisorClosureStageMutation,
|
|
301
|
+
runSupervisor,
|
|
302
|
+
parseSupervisorJournal,
|
|
303
|
+
createSupervisorJournal,
|
|
304
|
+
createInMemorySupervisorJournalStore,
|
|
305
|
+
createFileSupervisorJournal,
|
|
306
|
+
createClosureStage,
|
|
307
|
+
awaitTerminalRun,
|
|
308
|
+
SUPERVISOR_PLUGIN_NAME,
|
|
309
|
+
SUPERVISOR_CLOSURE_STAGE_ID
|
|
310
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { SupervisorEvent, type SupervisorProjection } from "@rig/contracts";
|
|
2
|
+
export interface SupervisorJournalStore {
|
|
3
|
+
append(line: string): Promise<void>;
|
|
4
|
+
read(): Promise<string>;
|
|
5
|
+
}
|
|
6
|
+
export interface SupervisorJournal {
|
|
7
|
+
append(event: SupervisorEvent): Promise<void>;
|
|
8
|
+
readEvents(): Promise<readonly SupervisorEvent[]>;
|
|
9
|
+
readProjection(): Promise<SupervisorProjection>;
|
|
10
|
+
}
|
|
11
|
+
export declare function createFileSupervisorJournal(path: string): SupervisorJournalStore;
|
|
12
|
+
export declare function createInMemorySupervisorJournalStore(seed?: readonly SupervisorEvent[]): SupervisorJournalStore;
|
|
13
|
+
export declare function parseSupervisorJournal(text: string): readonly SupervisorEvent[];
|
|
14
|
+
export declare function createSupervisorJournal(store: SupervisorJournalStore): SupervisorJournal;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/supervisor-plugin/src/journal.ts
|
|
3
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
4
|
+
import { dirname } from "path";
|
|
5
|
+
import { Schema } from "effect";
|
|
6
|
+
import {
|
|
7
|
+
SupervisorEvent,
|
|
8
|
+
reduceSupervisorJournal
|
|
9
|
+
} from "@rig/contracts";
|
|
10
|
+
var NEWLINE = `
|
|
11
|
+
`;
|
|
12
|
+
function createFileSupervisorJournal(path) {
|
|
13
|
+
return {
|
|
14
|
+
async append(line) {
|
|
15
|
+
await mkdir(dirname(path), { recursive: true });
|
|
16
|
+
await writeFile(path, `${line}${NEWLINE}`, { flag: "a" });
|
|
17
|
+
},
|
|
18
|
+
async read() {
|
|
19
|
+
try {
|
|
20
|
+
return await readFile(path, "utf8");
|
|
21
|
+
} catch (error) {
|
|
22
|
+
const code = typeof error === "object" && error !== null && "code" in error ? error.code : undefined;
|
|
23
|
+
if (code === "ENOENT")
|
|
24
|
+
return "";
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function createInMemorySupervisorJournalStore(seed = []) {
|
|
31
|
+
const lines = seed.map((event) => JSON.stringify(event));
|
|
32
|
+
return {
|
|
33
|
+
async append(line) {
|
|
34
|
+
lines.push(line);
|
|
35
|
+
},
|
|
36
|
+
async read() {
|
|
37
|
+
return lines.join(NEWLINE);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function parseSupervisorJournal(text) {
|
|
42
|
+
return text.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0).map((line, index) => {
|
|
43
|
+
try {
|
|
44
|
+
return Schema.decodeUnknownSync(SupervisorEvent)(JSON.parse(line));
|
|
45
|
+
} catch (error) {
|
|
46
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
47
|
+
throw new Error(`Invalid supervisor journal line ${index + 1}: ${message}`);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function createSupervisorJournal(store) {
|
|
52
|
+
const append = async (event) => {
|
|
53
|
+
await store.append(JSON.stringify(Schema.decodeUnknownSync(SupervisorEvent)(event)));
|
|
54
|
+
};
|
|
55
|
+
const readEvents = async () => parseSupervisorJournal(await store.read());
|
|
56
|
+
const readProjection = async () => reduceSupervisorJournal(await readEvents());
|
|
57
|
+
return { append, readEvents, readProjection };
|
|
58
|
+
}
|
|
59
|
+
export {
|
|
60
|
+
parseSupervisorJournal,
|
|
61
|
+
createSupervisorJournal,
|
|
62
|
+
createInMemorySupervisorJournalStore,
|
|
63
|
+
createFileSupervisorJournal
|
|
64
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/supervisor-plugin/src/plugin.ts
|
|
3
|
+
import { definePlugin } from "@rig/core";
|
|
4
|
+
|
|
5
|
+
// packages/supervisor-plugin/src/closureStage.ts
|
|
6
|
+
var SUPERVISOR_CLOSURE_STAGE_ID = "supervisor-closure-observer";
|
|
7
|
+
var supervisorClosureStageMutation = {
|
|
8
|
+
op: "insert",
|
|
9
|
+
contributedBy: "@rig/supervisor-plugin",
|
|
10
|
+
stage: {
|
|
11
|
+
id: SUPERVISOR_CLOSURE_STAGE_ID,
|
|
12
|
+
kind: "observe",
|
|
13
|
+
after: ["source-closeout"],
|
|
14
|
+
before: ["journal-append"],
|
|
15
|
+
priority: 0,
|
|
16
|
+
protected: false
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// packages/supervisor-plugin/src/plugin.ts
|
|
21
|
+
var SUPERVISOR_PLUGIN_NAME = "@rig/supervisor-plugin";
|
|
22
|
+
var supervisorPlugin = definePlugin({
|
|
23
|
+
name: SUPERVISOR_PLUGIN_NAME,
|
|
24
|
+
version: "0.0.0-alpha.1",
|
|
25
|
+
provides: [],
|
|
26
|
+
requires: ["journal", "transport"],
|
|
27
|
+
contributes: {
|
|
28
|
+
stageMutations: [supervisorClosureStageMutation]
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
var plugin_default = supervisorPlugin;
|
|
32
|
+
export {
|
|
33
|
+
supervisorPlugin,
|
|
34
|
+
plugin_default as default,
|
|
35
|
+
SUPERVISOR_PLUGIN_NAME
|
|
36
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { BlockerClassification, RunId, RunStatus, SupervisorEvent, SupervisorSelectionPolicy, SupervisorStopReason, TaskClosureSummary, TaskSummary } from "@rig/contracts";
|
|
2
|
+
import { type TaskDependencyBadgeSummary } from "@rig/core/task-graph";
|
|
3
|
+
export interface SupervisorActiveRun {
|
|
4
|
+
readonly taskId: TaskSummary["id"];
|
|
5
|
+
readonly runId: RunId;
|
|
6
|
+
readonly status?: RunStatus;
|
|
7
|
+
}
|
|
8
|
+
export interface TaskSnapshot {
|
|
9
|
+
readonly tasks: readonly TaskSummary[];
|
|
10
|
+
readonly activeRuns?: readonly SupervisorActiveRun[];
|
|
11
|
+
readonly diagnostics?: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
export interface SupervisorDispatchHandle {
|
|
14
|
+
readonly runId: RunId;
|
|
15
|
+
readonly dispatchHandle?: string | null;
|
|
16
|
+
}
|
|
17
|
+
export type SupervisorDispatchResult = RunId | SupervisorDispatchHandle;
|
|
18
|
+
export interface SupervisorRunOutcome {
|
|
19
|
+
readonly runId: RunId;
|
|
20
|
+
readonly status: RunStatus;
|
|
21
|
+
readonly failed?: boolean;
|
|
22
|
+
readonly closure?: TaskClosureSummary;
|
|
23
|
+
}
|
|
24
|
+
export interface SupervisorContext {
|
|
25
|
+
readonly projectRoot: string;
|
|
26
|
+
readTasks(): Promise<TaskSnapshot>;
|
|
27
|
+
dispatch(task: TaskSummary): Promise<SupervisorDispatchResult>;
|
|
28
|
+
awaitTerminal(runId: RunId, task: TaskSummary): Promise<SupervisorRunOutcome>;
|
|
29
|
+
waitForChange(timeoutMs: number): Promise<void>;
|
|
30
|
+
emitEvent?(event: SupervisorEvent): Promise<void>;
|
|
31
|
+
classifyBlocker?(task: TaskSummary, badges: ReadonlyMap<string, TaskDependencyBadgeSummary>): BlockerClassification;
|
|
32
|
+
now?(): Date;
|
|
33
|
+
shouldStop?(): boolean;
|
|
34
|
+
}
|
|
35
|
+
export interface TaskFilter {
|
|
36
|
+
readonly statuses?: readonly TaskSummary["status"][];
|
|
37
|
+
readonly workspaceIds?: readonly TaskSummary["workspaceId"][];
|
|
38
|
+
readonly assignees?: readonly string[];
|
|
39
|
+
}
|
|
40
|
+
export interface SupervisorOptions {
|
|
41
|
+
readonly concurrency?: number;
|
|
42
|
+
readonly selectionPolicy?: SupervisorSelectionPolicy;
|
|
43
|
+
readonly maxTasks?: number;
|
|
44
|
+
readonly filter?: TaskFilter;
|
|
45
|
+
readonly stopWhen?: ReadonlySet<SupervisorStopReason>;
|
|
46
|
+
readonly pauseOnAttention?: boolean;
|
|
47
|
+
readonly failFast?: boolean;
|
|
48
|
+
readonly pollMs?: number;
|
|
49
|
+
}
|
|
50
|
+
export interface SupervisorSummary {
|
|
51
|
+
readonly processed: number;
|
|
52
|
+
readonly succeeded: number;
|
|
53
|
+
readonly failed: number;
|
|
54
|
+
readonly skipped: number;
|
|
55
|
+
readonly idleReason: SupervisorStopReason | null;
|
|
56
|
+
}
|
|
57
|
+
export declare function runSupervisor(ctx: SupervisorContext, options?: SupervisorOptions): Promise<SupervisorSummary>;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/supervisor-plugin/src/supervisor.ts
|
|
3
|
+
import {
|
|
4
|
+
computeTaskDependencyBadges,
|
|
5
|
+
disjointScope,
|
|
6
|
+
isTaskTerminalStatus,
|
|
7
|
+
rankReadyTasks,
|
|
8
|
+
readTaskScope
|
|
9
|
+
} from "@rig/core/task-graph";
|
|
10
|
+
var DEFAULT_STOP_WHEN = new Set(["all-done", "all-human-blocked", "source-error"]);
|
|
11
|
+
var HUMAN_BLOCKER_CLASS = {
|
|
12
|
+
"not-blocked": false,
|
|
13
|
+
"task-blocked": false,
|
|
14
|
+
"human-decision": true,
|
|
15
|
+
"human-approval": true,
|
|
16
|
+
"external-input": true,
|
|
17
|
+
unknown: true
|
|
18
|
+
};
|
|
19
|
+
function normalizeOptions(options = {}) {
|
|
20
|
+
return {
|
|
21
|
+
concurrency: Math.max(1, Math.floor(options.concurrency ?? 1)),
|
|
22
|
+
selectionPolicy: options.selectionPolicy ?? "rank",
|
|
23
|
+
maxTasks: options.maxTasks === undefined ? null : Math.max(0, Math.floor(options.maxTasks)),
|
|
24
|
+
...options.filter ? { filter: options.filter } : {},
|
|
25
|
+
stopWhen: options.stopWhen ?? DEFAULT_STOP_WHEN,
|
|
26
|
+
pauseOnAttention: options.pauseOnAttention ?? true,
|
|
27
|
+
failFast: options.failFast ?? false,
|
|
28
|
+
pollMs: Math.max(0, Math.floor(options.pollMs ?? 5000))
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function selectionMode(policy) {
|
|
32
|
+
if (policy === "blocking-only")
|
|
33
|
+
return "blocking-only";
|
|
34
|
+
if (policy === "max-unblock")
|
|
35
|
+
return "max-unblock";
|
|
36
|
+
return "all-ready";
|
|
37
|
+
}
|
|
38
|
+
function dispatchHandle(result) {
|
|
39
|
+
return typeof result === "string" ? { runId: result } : result;
|
|
40
|
+
}
|
|
41
|
+
function filterTask(task, filter) {
|
|
42
|
+
if (!filter)
|
|
43
|
+
return true;
|
|
44
|
+
if (filter.statuses && !filter.statuses.includes(task.status))
|
|
45
|
+
return false;
|
|
46
|
+
if (filter.workspaceIds && !filter.workspaceIds.includes(task.workspaceId))
|
|
47
|
+
return false;
|
|
48
|
+
if (filter.assignees) {
|
|
49
|
+
const raw = task.metadata;
|
|
50
|
+
const assignee = typeof raw === "object" && raw !== null && "assignee" in raw && typeof raw.assignee === "string" ? raw.assignee : null;
|
|
51
|
+
if (!assignee || !filter.assignees.includes(assignee))
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
function classifyEmptyReadySet(snapshot, badges, classifyBlocker) {
|
|
57
|
+
if ((snapshot.diagnostics ?? []).length > 0)
|
|
58
|
+
return "source-error";
|
|
59
|
+
const openTasks = snapshot.tasks.filter((task) => !isTaskTerminalStatus(task.status));
|
|
60
|
+
if (openTasks.length === 0)
|
|
61
|
+
return "all-done";
|
|
62
|
+
if (classifyBlocker) {
|
|
63
|
+
const allHumanBlocked = openTasks.every((task) => HUMAN_BLOCKER_CLASS[classifyBlocker(task, badges).blockerClass]);
|
|
64
|
+
if (allHumanBlocked)
|
|
65
|
+
return "all-human-blocked";
|
|
66
|
+
}
|
|
67
|
+
return "all-done";
|
|
68
|
+
}
|
|
69
|
+
function terminalFailed(outcome) {
|
|
70
|
+
return outcome.failed ?? ["failed", "stopped", "needs-attention"].includes(outcome.status);
|
|
71
|
+
}
|
|
72
|
+
async function runSupervisor(ctx, options = {}) {
|
|
73
|
+
const normalized = normalizeOptions(options);
|
|
74
|
+
const now = () => (ctx.now?.() ?? new Date).toISOString();
|
|
75
|
+
const emit = async (event) => {
|
|
76
|
+
await ctx.emitEvent?.(event);
|
|
77
|
+
};
|
|
78
|
+
let processed = 0;
|
|
79
|
+
let succeeded = 0;
|
|
80
|
+
let failed = 0;
|
|
81
|
+
let skipped = 0;
|
|
82
|
+
let idleReason = null;
|
|
83
|
+
const completedTaskIds = new Set;
|
|
84
|
+
await emit({ kind: "supervisor.started", at: now(), options: normalized });
|
|
85
|
+
while (!ctx.shouldStop?.()) {
|
|
86
|
+
if (normalized.maxTasks !== null && processed >= normalized.maxTasks) {
|
|
87
|
+
idleReason = "max-tasks";
|
|
88
|
+
await emit({ kind: "supervisor.idle", at: now(), reason: idleReason });
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
const snapshot = await ctx.readTasks();
|
|
92
|
+
const badges = computeTaskDependencyBadges(snapshot.tasks);
|
|
93
|
+
const activeRuns = snapshot.activeRuns ?? [];
|
|
94
|
+
const activeTaskIds = activeRuns.map((run) => run.taskId);
|
|
95
|
+
const remainingSlots = normalized.maxTasks === null ? normalized.concurrency : Math.min(normalized.concurrency, normalized.maxTasks - processed);
|
|
96
|
+
const ranked = rankReadyTasks(snapshot.tasks, {
|
|
97
|
+
excludeTaskIds: completedTaskIds,
|
|
98
|
+
activeTaskIds,
|
|
99
|
+
filter: (task) => filterTask(task, normalized.filter),
|
|
100
|
+
selection: selectionMode(normalized.selectionPolicy)
|
|
101
|
+
});
|
|
102
|
+
const selected = [];
|
|
103
|
+
const occupiedScopes = activeRuns.flatMap((run) => {
|
|
104
|
+
const task = snapshot.tasks.find((candidate) => candidate.id === run.taskId);
|
|
105
|
+
return task ? readTaskScope(task) : [];
|
|
106
|
+
});
|
|
107
|
+
for (const entry of ranked) {
|
|
108
|
+
if (selected.length >= remainingSlots)
|
|
109
|
+
break;
|
|
110
|
+
if (!disjointScope(entry.scope, occupiedScopes))
|
|
111
|
+
continue;
|
|
112
|
+
selected.push(entry);
|
|
113
|
+
occupiedScopes.push(...entry.scope);
|
|
114
|
+
}
|
|
115
|
+
await emit({ kind: "supervisor.selection-planned", at: now(), taskIds: selected.map((entry) => entry.task.id), policy: normalized.selectionPolicy });
|
|
116
|
+
if (selected.length === 0) {
|
|
117
|
+
idleReason = classifyEmptyReadySet(snapshot, badges, ctx.classifyBlocker);
|
|
118
|
+
await emit({ kind: "supervisor.idle", at: now(), reason: idleReason });
|
|
119
|
+
if (normalized.stopWhen.has(idleReason))
|
|
120
|
+
break;
|
|
121
|
+
await ctx.waitForChange(normalized.pollMs);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const outcomes = await Promise.all(selected.map(async (entry) => {
|
|
125
|
+
await emit({ kind: "supervisor.dispatch-started", at: now(), taskId: entry.task.id, score: Math.max(0, Math.floor(entry.score)) });
|
|
126
|
+
const handle = dispatchHandle(await ctx.dispatch(entry.task));
|
|
127
|
+
await emit({
|
|
128
|
+
kind: "supervisor.dispatch-confirmed",
|
|
129
|
+
at: now(),
|
|
130
|
+
taskId: entry.task.id,
|
|
131
|
+
runId: handle.runId,
|
|
132
|
+
...handle.dispatchHandle === undefined ? {} : { dispatchHandle: handle.dispatchHandle }
|
|
133
|
+
});
|
|
134
|
+
const outcome = await ctx.awaitTerminal(handle.runId, entry.task);
|
|
135
|
+
const failedOutcome = terminalFailed(outcome);
|
|
136
|
+
await emit({
|
|
137
|
+
kind: "supervisor.outcome",
|
|
138
|
+
at: now(),
|
|
139
|
+
taskId: entry.task.id,
|
|
140
|
+
runId: outcome.runId,
|
|
141
|
+
status: outcome.status,
|
|
142
|
+
failed: failedOutcome,
|
|
143
|
+
unblockedTaskIds: outcome.closure?.unblockedTaskIds ?? [],
|
|
144
|
+
...outcome.closure ? { closure: outcome.closure } : {}
|
|
145
|
+
});
|
|
146
|
+
return { taskId: entry.task.id, status: outcome.status, failed: failedOutcome };
|
|
147
|
+
}));
|
|
148
|
+
for (const outcome of outcomes) {
|
|
149
|
+
completedTaskIds.add(outcome.taskId);
|
|
150
|
+
processed += 1;
|
|
151
|
+
if (outcome.failed)
|
|
152
|
+
failed += 1;
|
|
153
|
+
else
|
|
154
|
+
succeeded += 1;
|
|
155
|
+
}
|
|
156
|
+
if (normalized.pauseOnAttention && outcomes.some((outcome) => outcome.status === "needs-attention")) {
|
|
157
|
+
idleReason = "operator-stop";
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
if (normalized.failFast && outcomes.some((outcome) => outcome.failed))
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
if (ctx.shouldStop?.()) {
|
|
164
|
+
idleReason = "operator-stop";
|
|
165
|
+
await emit({ kind: "supervisor.stopped", at: now(), reason: idleReason });
|
|
166
|
+
}
|
|
167
|
+
await emit({ kind: "supervisor.finished", at: now(), processed, succeeded, failed, skipped, idleReason });
|
|
168
|
+
return { processed, succeeded, failed, skipped, idleReason };
|
|
169
|
+
}
|
|
170
|
+
export {
|
|
171
|
+
runSupervisor
|
|
172
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@h-rig/supervisor-plugin",
|
|
3
|
+
"version": "0.0.6-alpha.133",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "First-party autonomous supervisor loop plugin for Rig.",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/src/index.d.ts",
|
|
14
|
+
"import": "./dist/src/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./supervisor": {
|
|
17
|
+
"types": "./dist/src/supervisor.d.ts",
|
|
18
|
+
"import": "./dist/src/supervisor.js"
|
|
19
|
+
},
|
|
20
|
+
"./journal": {
|
|
21
|
+
"types": "./dist/src/journal.d.ts",
|
|
22
|
+
"import": "./dist/src/journal.js"
|
|
23
|
+
},
|
|
24
|
+
"./closure-stage": {
|
|
25
|
+
"types": "./dist/src/closureStage.d.ts",
|
|
26
|
+
"import": "./dist/src/closureStage.js"
|
|
27
|
+
},
|
|
28
|
+
"./awaiter": {
|
|
29
|
+
"types": "./dist/src/awaiter.d.ts",
|
|
30
|
+
"import": "./dist/src/awaiter.js"
|
|
31
|
+
},
|
|
32
|
+
"./plugin": {
|
|
33
|
+
"types": "./dist/src/plugin.d.ts",
|
|
34
|
+
"import": "./dist/src/plugin.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"bun": ">=1.3.11"
|
|
39
|
+
},
|
|
40
|
+
"main": "./dist/src/index.js",
|
|
41
|
+
"module": "./dist/src/index.js",
|
|
42
|
+
"types": "./dist/src/index.d.ts",
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.133",
|
|
45
|
+
"@rig/core": "npm:@h-rig/core@0.0.6-alpha.133",
|
|
46
|
+
"effect": "4.0.0-beta.78"
|
|
47
|
+
}
|
|
48
|
+
}
|