@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 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,5 @@
1
+ export * from "./awaiter";
2
+ export * from "./closureStage";
3
+ export * from "./journal";
4
+ export * from "./plugin";
5
+ export * from "./supervisor";
@@ -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,3 @@
1
+ export declare const SUPERVISOR_PLUGIN_NAME = "@rig/supervisor-plugin";
2
+ export declare const supervisorPlugin: import("@rig/core").RigPluginWithRuntime;
3
+ export default supervisorPlugin;
@@ -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
+ }