@h-rig/supervisor-plugin 0.0.6-alpha.155 → 0.0.6-alpha.157

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 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
- return await import("@rig/client");
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 { runSupervisorLoop } = await loadSupervisorClient();
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 { unblockTask } = await loadSupervisorClient();
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;
@@ -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,
@@ -1,6 +1,7 @@
1
1
  export * from "./awaiter";
2
2
  export * from "./closureStage";
3
3
  export * from "./cli";
4
+ export * from "./loop";
4
5
  export * from "./journal";
5
6
  export * from "./plugin";
6
7
  export * from "./supervisor";