harnery 0.14.0 → 0.16.0
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/commander.d.ts.map +1 -1
- package/dist/commander.js +2 -0
- package/dist/commands/supervisor.d.ts +4 -0
- package/dist/commands/supervisor.d.ts.map +1 -0
- package/dist/commands/supervisor.js +446 -0
- package/dist/core/supervisor/index.d.ts +4 -0
- package/dist/core/supervisor/index.d.ts.map +1 -0
- package/dist/core/supervisor/index.js +3 -0
- package/dist/core/supervisor/read.d.ts +4 -0
- package/dist/core/supervisor/read.d.ts.map +1 -0
- package/dist/core/supervisor/read.js +2 -0
- package/dist/core/supervisor/runner.d.ts +34 -0
- package/dist/core/supervisor/runner.d.ts.map +1 -0
- package/dist/core/supervisor/runner.js +198 -0
- package/dist/core/supervisor/service-read.d.ts +6 -0
- package/dist/core/supervisor/service-read.d.ts.map +1 -0
- package/dist/core/supervisor/service-read.js +127 -0
- package/dist/core/supervisor/service.d.ts +114 -0
- package/dist/core/supervisor/service.d.ts.map +1 -0
- package/dist/core/supervisor/service.js +699 -0
- package/dist/core/supervisor/state.d.ts +69 -0
- package/dist/core/supervisor/state.d.ts.map +1 -0
- package/dist/core/supervisor/state.js +413 -0
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +21 -8
- package/dist/core/workflow/index.d.ts +1 -1
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +1 -0
- package/dist/core/workflow/run-state.d.ts +2 -0
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +12 -0
- package/dist/core/workflow/specialists.d.ts +7 -0
- package/dist/core/workflow/specialists.d.ts.map +1 -0
- package/dist/core/workflow/specialists.js +78 -0
- package/dist/core/workflow/types.d.ts +17 -0
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/lib/tunnel/gate.d.ts.map +1 -1
- package/dist/lib/tunnel/gate.js +6 -1
- package/package.json +11 -1
- package/src/commander.ts +2 -0
- package/src/commands/supervisor.ts +575 -0
- package/src/core/supervisor/index.ts +51 -0
- package/src/core/supervisor/read.ts +29 -0
- package/src/core/supervisor/runner.ts +278 -0
- package/src/core/supervisor/service-read.ts +144 -0
- package/src/core/supervisor/service.ts +920 -0
- package/src/core/supervisor/state.ts +552 -0
- package/src/core/workflow/engine.ts +21 -8
- package/src/core/workflow/index.ts +1 -0
- package/src/core/workflow/proof.ts +1 -0
- package/src/core/workflow/run-state.ts +17 -0
- package/src/core/workflow/specialists.ts +97 -0
- package/src/core/workflow/types.ts +18 -0
- package/src/lib/tunnel/gate.ts +7 -1
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import type { Command } from "commander";
|
|
4
|
+
import type { EmitContext } from "../commander.ts";
|
|
5
|
+
import { workflowSubscriptionOnly } from "../core/config.ts";
|
|
6
|
+
import { createBuiltinHarnessRegistry } from "../core/harnesses/index.ts";
|
|
7
|
+
import { findCoordRoot } from "../core/hooks/resolve/coord-root.ts";
|
|
8
|
+
import type { PolicyIsolation } from "../core/policy/index.ts";
|
|
9
|
+
import { loadPolicyFile } from "../core/policy/index.ts";
|
|
10
|
+
import {
|
|
11
|
+
configureSupervisorService,
|
|
12
|
+
createSupervisor,
|
|
13
|
+
listSupervisors,
|
|
14
|
+
readSupervisor,
|
|
15
|
+
readSupervisorServiceConfig,
|
|
16
|
+
readSupervisorServiceStatus,
|
|
17
|
+
requestSupervisorServiceStop,
|
|
18
|
+
runSupervisor,
|
|
19
|
+
runSupervisorServiceDaemon,
|
|
20
|
+
type SupervisorRecord,
|
|
21
|
+
type SupervisorRunReport,
|
|
22
|
+
type SupervisorServiceConfig,
|
|
23
|
+
type SupervisorServiceStatus,
|
|
24
|
+
spawnSupervisorService,
|
|
25
|
+
supervisorServiceLogPath,
|
|
26
|
+
} from "../core/supervisor/index.ts";
|
|
27
|
+
import type { WorkflowSpecialistProfile } from "../core/workflow/index.ts";
|
|
28
|
+
|
|
29
|
+
interface CreateOpts {
|
|
30
|
+
team: string;
|
|
31
|
+
id?: string;
|
|
32
|
+
title?: string;
|
|
33
|
+
maxCycles?: string;
|
|
34
|
+
maxRuntimeMs?: string;
|
|
35
|
+
maxParallelWork?: string;
|
|
36
|
+
maxTotalAttempts?: string;
|
|
37
|
+
maxAgentsPerWork?: string;
|
|
38
|
+
agentConcurrency?: string;
|
|
39
|
+
acceptPassingProof?: boolean;
|
|
40
|
+
resumeApproved?: boolean;
|
|
41
|
+
retryBlocked?: boolean;
|
|
42
|
+
json?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface RunOpts {
|
|
46
|
+
harness?: string;
|
|
47
|
+
cwd?: string;
|
|
48
|
+
subscriptionOnly?: boolean;
|
|
49
|
+
allowApiBilling?: boolean;
|
|
50
|
+
policy?: string;
|
|
51
|
+
isolation?: PolicyIsolation;
|
|
52
|
+
approvalTo?: string;
|
|
53
|
+
actor?: string;
|
|
54
|
+
json?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface ServiceOpts extends RunOpts {
|
|
58
|
+
wakeIntervalMs?: string;
|
|
59
|
+
heartbeatIntervalMs?: string;
|
|
60
|
+
errorBackoffBaseMs?: string;
|
|
61
|
+
errorBackoffMaxMs?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function registerSupervisorCommand(program: Command, emit: EmitContext): void {
|
|
65
|
+
const registry = createBuiltinHarnessRegistry();
|
|
66
|
+
const supervisor = program
|
|
67
|
+
.command("supervisor")
|
|
68
|
+
.description("Run a bounded specialist team over a durable-work dependency graph.");
|
|
69
|
+
|
|
70
|
+
supervisor
|
|
71
|
+
.command("create <root-work-id>")
|
|
72
|
+
.description("Freeze a goal, specialist team, automation policy, and execution limits.")
|
|
73
|
+
.requiredOption("--team <file>", "JSON object mapping specialist ids to profiles")
|
|
74
|
+
.option("--id <goal-id>", "Stable goal id (generated by default)")
|
|
75
|
+
.option("--title <text>", "Goal title (defaults to root work title)")
|
|
76
|
+
.option("--max-cycles <n>", "Foreground cycle ceiling (default 50)")
|
|
77
|
+
.option("--max-runtime-ms <n>", "Foreground wall-time ceiling (default 14400000)")
|
|
78
|
+
.option("--max-parallel-work <n>", "Concurrent work-item ceiling (default 1)")
|
|
79
|
+
.option("--max-total-attempts <n>", "Graph-wide attempt ceiling (default 100)")
|
|
80
|
+
.option("--max-agents-per-work <n>", "Child-agent ceiling per work item (default 20)")
|
|
81
|
+
.option("--agent-concurrency <n>", "Child concurrency per work item (default 4)")
|
|
82
|
+
.option("--accept-passing-proof", "Allow the supervisor to explicitly accept passing proof")
|
|
83
|
+
.option("--no-resume-approved", "Stop after an approval instead of resuming its parked run")
|
|
84
|
+
.option("--retry-blocked", "Allow bounded retry of blocked work")
|
|
85
|
+
.option("--json", "Emit the complete supervisor record as JSON")
|
|
86
|
+
.action((rootWorkId: string, opts: CreateOpts) => {
|
|
87
|
+
withSupervisorRoot(emit, (coordRoot) => {
|
|
88
|
+
const specialists = readTeamFile(opts.team);
|
|
89
|
+
for (const [id, profile] of Object.entries(specialists)) {
|
|
90
|
+
if (profile.harness && !registry.get(profile.harness)) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`specialist ${id} names unknown harness ${JSON.stringify(profile.harness)}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
emitSupervisor(
|
|
97
|
+
createSupervisor({
|
|
98
|
+
coordRoot,
|
|
99
|
+
rootWorkId,
|
|
100
|
+
specialists,
|
|
101
|
+
id: opts.id,
|
|
102
|
+
title: opts.title,
|
|
103
|
+
limits: {
|
|
104
|
+
max_cycles: integer(opts.maxCycles),
|
|
105
|
+
max_runtime_ms: integer(opts.maxRuntimeMs),
|
|
106
|
+
max_parallel_work: integer(opts.maxParallelWork),
|
|
107
|
+
max_total_attempts: integer(opts.maxTotalAttempts),
|
|
108
|
+
max_agents_per_work: integer(opts.maxAgentsPerWork),
|
|
109
|
+
agent_concurrency: integer(opts.agentConcurrency),
|
|
110
|
+
},
|
|
111
|
+
automation: {
|
|
112
|
+
accept_passing_proof: opts.acceptPassingProof,
|
|
113
|
+
resume_approved: opts.resumeApproved,
|
|
114
|
+
retry_blocked: opts.retryBlocked,
|
|
115
|
+
},
|
|
116
|
+
}),
|
|
117
|
+
opts.json,
|
|
118
|
+
emit,
|
|
119
|
+
true,
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
supervisor
|
|
125
|
+
.command("list")
|
|
126
|
+
.description("List durable goals and their derived next action.")
|
|
127
|
+
.option("--json", "Emit complete supervisor records as JSON")
|
|
128
|
+
.action((opts: { json?: boolean }) => {
|
|
129
|
+
withSupervisorRoot(emit, (coordRoot) => {
|
|
130
|
+
const records = listSupervisors(coordRoot);
|
|
131
|
+
if (opts.json) {
|
|
132
|
+
emit.config({ format: "json" });
|
|
133
|
+
emit.data(records);
|
|
134
|
+
} else if (records.length === 0) {
|
|
135
|
+
emit.text("no durable supervisors\n");
|
|
136
|
+
} else {
|
|
137
|
+
emit.text(`${records.map(renderSupervisorRow).join("\n")}\n`);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
supervisor
|
|
143
|
+
.command("show <goal-id>")
|
|
144
|
+
.description("Show one goal, frozen team, bounds, graph state, and next action.")
|
|
145
|
+
.option("--json", "Emit the complete supervisor record as JSON")
|
|
146
|
+
.action((goalId: string, opts: { json?: boolean }) => {
|
|
147
|
+
withSupervisorRoot(emit, (coordRoot) => {
|
|
148
|
+
emitSupervisor(readSupervisor(coordRoot, goalId), opts.json, emit, true);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
registerServiceCommand(supervisor, registry, emit);
|
|
153
|
+
registerRunCommand(supervisor, "tick", registry, emit);
|
|
154
|
+
registerRunCommand(supervisor, "run", registry, emit);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function registerServiceCommand(
|
|
158
|
+
supervisor: Command,
|
|
159
|
+
registry: ReturnType<typeof createBuiltinHarnessRegistry>,
|
|
160
|
+
emit: EmitContext,
|
|
161
|
+
): void {
|
|
162
|
+
const service = supervisor
|
|
163
|
+
.command("service")
|
|
164
|
+
.description("Run explicitly enrolled durable goals in a restartable background service.");
|
|
165
|
+
|
|
166
|
+
addServiceOptions(
|
|
167
|
+
service
|
|
168
|
+
.command("start [goal-ids...]")
|
|
169
|
+
.description("Configure and start the detached per-repository supervisor service."),
|
|
170
|
+
)
|
|
171
|
+
.option("--json", "Emit complete service status as JSON")
|
|
172
|
+
.action(async (goalIds: string[], opts: ServiceOpts) => {
|
|
173
|
+
await withSupervisorRootAsync(emit, async (coordRoot) => {
|
|
174
|
+
prepareServiceConfig(coordRoot, goalIds, opts, registry);
|
|
175
|
+
const status = await spawnSupervisorService(coordRoot);
|
|
176
|
+
emitServiceStatus(status, opts.json, emit);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
addServiceOptions(
|
|
181
|
+
service
|
|
182
|
+
.command("run [goal-ids...]")
|
|
183
|
+
.description("Run the persistent service loop in the foreground for a process manager."),
|
|
184
|
+
)
|
|
185
|
+
.option("--json", "Emit terminal service status as JSON")
|
|
186
|
+
.action(async (goalIds: string[], opts: ServiceOpts) => {
|
|
187
|
+
await withSupervisorRootAsync(emit, async (coordRoot) => {
|
|
188
|
+
const config = prepareServiceConfig(coordRoot, goalIds, opts, registry);
|
|
189
|
+
const status = await runSupervisorServiceDaemon({
|
|
190
|
+
coordRoot,
|
|
191
|
+
engine: serviceEngine(config, registry),
|
|
192
|
+
onLog: opts.json ? undefined : (line) => emit.text(`${line}\n`),
|
|
193
|
+
});
|
|
194
|
+
if (opts.json) {
|
|
195
|
+
emit.config({ format: "json" });
|
|
196
|
+
emit.data(status);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
service
|
|
202
|
+
.command("status")
|
|
203
|
+
.description("Show service liveness, heartbeat, enrolled goals, and durable wake state.")
|
|
204
|
+
.option("--json", "Emit complete service status as JSON")
|
|
205
|
+
.action((opts: { json?: boolean }) => {
|
|
206
|
+
withSupervisorRoot(emit, (coordRoot) => {
|
|
207
|
+
emitServiceStatus(readSupervisorServiceStatus(coordRoot), opts.json, emit);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
service
|
|
212
|
+
.command("stop")
|
|
213
|
+
.description("Request a graceful stop; an active goal tick is allowed to finish safely.")
|
|
214
|
+
.option("--json", "Emit complete service status as JSON")
|
|
215
|
+
.action(async (opts: { json?: boolean }) => {
|
|
216
|
+
await withSupervisorRootAsync(emit, async (coordRoot) => {
|
|
217
|
+
let status = requestSupervisorServiceStop(coordRoot);
|
|
218
|
+
const deadline = Date.now() + 5_000;
|
|
219
|
+
while (status.running && Date.now() < deadline) {
|
|
220
|
+
await delay(50);
|
|
221
|
+
status = readSupervisorServiceStatus(coordRoot);
|
|
222
|
+
}
|
|
223
|
+
emitServiceStatus(status, opts.json, emit);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
service
|
|
228
|
+
.command("logs")
|
|
229
|
+
.description("Show the tail of the private background-service log.")
|
|
230
|
+
.option("--lines <n>", "Number of lines to show (default 50)", "50")
|
|
231
|
+
.action((opts: { lines: string }) => {
|
|
232
|
+
withSupervisorRoot(emit, (coordRoot) => {
|
|
233
|
+
const lines = integer(opts.lines) ?? 50;
|
|
234
|
+
if (lines > 10_000) throw new Error("service log lines must not exceed 10000");
|
|
235
|
+
const path = supervisorServiceLogPath(coordRoot);
|
|
236
|
+
if (!existsSync(path)) {
|
|
237
|
+
emit.text("no supervisor service log\n");
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const selected = readFileSync(path, "utf8")
|
|
241
|
+
.split("\n")
|
|
242
|
+
.slice(-(lines + 1))
|
|
243
|
+
.join("\n");
|
|
244
|
+
emit.text(selected.endsWith("\n") ? selected : `${selected}\n`);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
service
|
|
249
|
+
.command("daemon", { hidden: true })
|
|
250
|
+
.description("Internal detached service entrypoint.")
|
|
251
|
+
.action(async () => {
|
|
252
|
+
await withSupervisorRootAsync(emit, async (coordRoot) => {
|
|
253
|
+
const config = readSupervisorServiceConfig(coordRoot);
|
|
254
|
+
await runSupervisorServiceDaemon({
|
|
255
|
+
coordRoot,
|
|
256
|
+
engine: serviceEngine(config, registry),
|
|
257
|
+
onLog: (line) => emit.text(`${line}\n`),
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function addServiceOptions(command: Command): Command {
|
|
264
|
+
return command
|
|
265
|
+
.option("--wake-interval-ms <n>", "State-change poll interval (default 5000)")
|
|
266
|
+
.option("--heartbeat-interval-ms <n>", "Heartbeat interval (default 2000)")
|
|
267
|
+
.option("--error-backoff-base-ms <n>", "Initial service-error backoff (default 2000)")
|
|
268
|
+
.option("--error-backoff-max-ms <n>", "Maximum service-error backoff (default 300000)")
|
|
269
|
+
.option("--harness <name>", "Fallback harness for agent calls without a specialist")
|
|
270
|
+
.option("--cwd <dir>", "Working directory for child agents")
|
|
271
|
+
.option("--subscription-only", "Require stored harness-login billing")
|
|
272
|
+
.option("--allow-api-billing", "Permit API-key override billing")
|
|
273
|
+
.option("--policy <file>", "Host policy JSON/JSONC")
|
|
274
|
+
.option("--isolation <mode>", "shared | worktree | sandbox | remote")
|
|
275
|
+
.option("--approval-to <address>", "Address durable ASK requests");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function prepareServiceConfig(
|
|
279
|
+
coordRoot: string,
|
|
280
|
+
goalIds: string[],
|
|
281
|
+
opts: ServiceOpts,
|
|
282
|
+
registry: ReturnType<typeof createBuiltinHarnessRegistry>,
|
|
283
|
+
): SupervisorServiceConfig {
|
|
284
|
+
if (goalIds.length === 0) {
|
|
285
|
+
if (hasServiceConfigOverrides(opts)) {
|
|
286
|
+
throw new Error("goal ids are required when changing supervisor service options");
|
|
287
|
+
}
|
|
288
|
+
return readSupervisorServiceConfig(coordRoot);
|
|
289
|
+
}
|
|
290
|
+
if (opts.harness && !registry.get(opts.harness)) {
|
|
291
|
+
throw new Error(`unknown harness ${JSON.stringify(opts.harness)}`);
|
|
292
|
+
}
|
|
293
|
+
const policy = opts.policy ? loadPolicyFile(opts.policy) : undefined;
|
|
294
|
+
return configureSupervisorService({
|
|
295
|
+
coordRoot,
|
|
296
|
+
goalIds,
|
|
297
|
+
wakeIntervalMs: integer(opts.wakeIntervalMs),
|
|
298
|
+
heartbeatIntervalMs: integer(opts.heartbeatIntervalMs),
|
|
299
|
+
errorBackoffBaseMs: integer(opts.errorBackoffBaseMs),
|
|
300
|
+
errorBackoffMaxMs: integer(opts.errorBackoffMaxMs),
|
|
301
|
+
engine: {
|
|
302
|
+
default_harness: opts.harness,
|
|
303
|
+
cwd: opts.cwd,
|
|
304
|
+
subscription_only:
|
|
305
|
+
opts.subscriptionOnly === true ? true : workflowSubscriptionOnly(coordRoot),
|
|
306
|
+
allow_api_billing: opts.allowApiBilling,
|
|
307
|
+
policy,
|
|
308
|
+
isolation: opts.isolation,
|
|
309
|
+
approval_addressee: opts.approvalTo,
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function hasServiceConfigOverrides(opts: ServiceOpts): boolean {
|
|
315
|
+
return Boolean(
|
|
316
|
+
opts.wakeIntervalMs ||
|
|
317
|
+
opts.heartbeatIntervalMs ||
|
|
318
|
+
opts.errorBackoffBaseMs ||
|
|
319
|
+
opts.errorBackoffMaxMs ||
|
|
320
|
+
opts.harness ||
|
|
321
|
+
opts.cwd ||
|
|
322
|
+
opts.subscriptionOnly ||
|
|
323
|
+
opts.allowApiBilling ||
|
|
324
|
+
opts.policy ||
|
|
325
|
+
opts.isolation ||
|
|
326
|
+
opts.approvalTo,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function serviceEngine(
|
|
331
|
+
config: SupervisorServiceConfig,
|
|
332
|
+
registry: ReturnType<typeof createBuiltinHarnessRegistry>,
|
|
333
|
+
) {
|
|
334
|
+
return {
|
|
335
|
+
spawners: registry.spawners(),
|
|
336
|
+
defaultHarness: config.engine.default_harness,
|
|
337
|
+
cwd: config.engine.cwd,
|
|
338
|
+
subscriptionOnly: config.engine.subscription_only,
|
|
339
|
+
allowApiBilling: config.engine.allow_api_billing,
|
|
340
|
+
harnessEvidence: Object.fromEntries(
|
|
341
|
+
registry
|
|
342
|
+
.list()
|
|
343
|
+
.map((adapter) => [
|
|
344
|
+
adapter.profile.id,
|
|
345
|
+
{ toolEvidence: adapter.profile.capabilities.toolEvidence },
|
|
346
|
+
]),
|
|
347
|
+
),
|
|
348
|
+
policy: config.engine.policy,
|
|
349
|
+
approvalMode: "park" as const,
|
|
350
|
+
approvalAddressee: config.engine.approval_addressee,
|
|
351
|
+
isolation: config.engine.isolation,
|
|
352
|
+
networkAccess: "enabled" as const,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function emitServiceStatus(
|
|
357
|
+
status: SupervisorServiceStatus,
|
|
358
|
+
json: boolean | undefined,
|
|
359
|
+
emit: EmitContext,
|
|
360
|
+
): void {
|
|
361
|
+
if (json) {
|
|
362
|
+
emit.config({ format: "json" });
|
|
363
|
+
emit.data(status);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (!status.config && !status.record) {
|
|
367
|
+
emit.text("supervisor service: unconfigured\n");
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const state = status.running
|
|
371
|
+
? (status.record?.state ?? "running")
|
|
372
|
+
: status.stale
|
|
373
|
+
? "stale"
|
|
374
|
+
: "stopped";
|
|
375
|
+
const lines = [
|
|
376
|
+
`supervisor service: ${state}`,
|
|
377
|
+
`goals: ${status.config?.goal_ids.join(", ") || "none"}`,
|
|
378
|
+
];
|
|
379
|
+
if (status.record) {
|
|
380
|
+
lines.push(`pid: ${status.record.pid} on ${status.record.host}`);
|
|
381
|
+
lines.push(`heartbeat: ${status.record.heartbeat_at}`);
|
|
382
|
+
lines.push(`sweeps: ${status.record.sweep_count}`);
|
|
383
|
+
if (status.record.active_goal_id) lines.push(`active: ${status.record.active_goal_id}`);
|
|
384
|
+
if (status.record.next_wake_at) lines.push(`next wake: ${status.record.next_wake_at}`);
|
|
385
|
+
if (status.record.last_error) lines.push(`error: ${status.record.last_error}`);
|
|
386
|
+
}
|
|
387
|
+
const backingOff = Object.entries(status.runtime?.goals ?? {}).filter(
|
|
388
|
+
([, runtime]) => runtime.state === "backoff",
|
|
389
|
+
);
|
|
390
|
+
if (backingOff.length) {
|
|
391
|
+
lines.push(`backoff: ${backingOff.map(([goalId]) => goalId).join(", ")}`);
|
|
392
|
+
}
|
|
393
|
+
emit.text(`${lines.join("\n")}\n`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function registerRunCommand(
|
|
397
|
+
supervisor: Command,
|
|
398
|
+
mode: "tick" | "run",
|
|
399
|
+
registry: ReturnType<typeof createBuiltinHarnessRegistry>,
|
|
400
|
+
emit: EmitContext,
|
|
401
|
+
): void {
|
|
402
|
+
supervisor
|
|
403
|
+
.command(`${mode} <goal-id>`)
|
|
404
|
+
.description(
|
|
405
|
+
mode === "tick"
|
|
406
|
+
? "Perform at most one bounded scheduling cycle."
|
|
407
|
+
: "Run bounded cycles until success, attention, no progress, or budget exhaustion.",
|
|
408
|
+
)
|
|
409
|
+
.option("--harness <name>", "Fallback harness for agent calls without a specialist")
|
|
410
|
+
.option("--cwd <dir>", "Working directory for child agents")
|
|
411
|
+
.option("--subscription-only", "Require stored harness-login billing")
|
|
412
|
+
.option("--allow-api-billing", "Permit API-key override billing")
|
|
413
|
+
.option("--policy <file>", "Host policy JSON/JSONC")
|
|
414
|
+
.option("--isolation <mode>", "shared | worktree | sandbox | remote")
|
|
415
|
+
.option("--approval-to <address>", "Address durable ASK requests")
|
|
416
|
+
.option("--actor <name>", "Actor recorded on supervisor work decisions")
|
|
417
|
+
.option("--json", "Emit the complete run report as JSON")
|
|
418
|
+
.action(async (goalId: string, opts: RunOpts) => {
|
|
419
|
+
await withSupervisorRootAsync(emit, async (coordRoot) => {
|
|
420
|
+
if (opts.harness && !registry.get(opts.harness)) {
|
|
421
|
+
throw new Error(`unknown harness ${JSON.stringify(opts.harness)}`);
|
|
422
|
+
}
|
|
423
|
+
const report = await runSupervisor({
|
|
424
|
+
coordRoot,
|
|
425
|
+
goalId,
|
|
426
|
+
mode,
|
|
427
|
+
actor: opts.actor,
|
|
428
|
+
onLog: opts.json ? undefined : (line) => emit.text(`${line}\n`),
|
|
429
|
+
engine: {
|
|
430
|
+
spawners: registry.spawners(),
|
|
431
|
+
defaultHarness: opts.harness,
|
|
432
|
+
cwd: opts.cwd,
|
|
433
|
+
subscriptionOnly:
|
|
434
|
+
opts.subscriptionOnly === true ? true : workflowSubscriptionOnly(coordRoot),
|
|
435
|
+
allowApiBilling: opts.allowApiBilling,
|
|
436
|
+
harnessEvidence: Object.fromEntries(
|
|
437
|
+
registry
|
|
438
|
+
.list()
|
|
439
|
+
.map((adapter) => [
|
|
440
|
+
adapter.profile.id,
|
|
441
|
+
{ toolEvidence: adapter.profile.capabilities.toolEvidence },
|
|
442
|
+
]),
|
|
443
|
+
),
|
|
444
|
+
policy: opts.policy ? loadPolicyFile(opts.policy) : undefined,
|
|
445
|
+
approvalMode: "park",
|
|
446
|
+
approvalAddressee: opts.approvalTo,
|
|
447
|
+
isolation: opts.isolation,
|
|
448
|
+
networkAccess: "enabled",
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
emitSupervisorReport(report, opts.json, emit);
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function readTeamFile(path: string): Record<string, WorkflowSpecialistProfile> {
|
|
457
|
+
const absolute = resolve(path);
|
|
458
|
+
let value: unknown;
|
|
459
|
+
try {
|
|
460
|
+
value = JSON.parse(readFileSync(absolute, "utf8"));
|
|
461
|
+
} catch (error) {
|
|
462
|
+
throw new Error(`cannot read supervisor team at ${absolute}: ${(error as Error).message}`);
|
|
463
|
+
}
|
|
464
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
465
|
+
throw new Error("supervisor team must be a JSON object keyed by specialist id");
|
|
466
|
+
}
|
|
467
|
+
return value as Record<string, WorkflowSpecialistProfile>;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function emitSupervisor(
|
|
471
|
+
record: SupervisorRecord,
|
|
472
|
+
json: boolean | undefined,
|
|
473
|
+
emit: EmitContext,
|
|
474
|
+
detail = false,
|
|
475
|
+
): void {
|
|
476
|
+
if (json) {
|
|
477
|
+
emit.config({ format: "json" });
|
|
478
|
+
emit.data(record);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (!detail) {
|
|
482
|
+
emit.text(`${renderSupervisorRow(record)}\n`);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const projection = record.projection;
|
|
486
|
+
const lines = [
|
|
487
|
+
`${record.intent.id}: ${record.intent.title}`,
|
|
488
|
+
`state: ${projection.state}`,
|
|
489
|
+
`root: ${record.intent.root_work_id}`,
|
|
490
|
+
`reason: ${projection.reason}`,
|
|
491
|
+
`next: ${projection.next_action}`,
|
|
492
|
+
`work: ${projection.work_ids.length}`,
|
|
493
|
+
`attempts: ${projection.attempts_used}/${record.intent.limits.max_total_attempts}`,
|
|
494
|
+
`specialists: ${projection.specialists.join(", ") || "none"}`,
|
|
495
|
+
`automation: accept=${record.intent.automation.accept_passing_proof}, ` +
|
|
496
|
+
`resume=${record.intent.automation.resume_approved}, retry=${record.intent.automation.retry_blocked}`,
|
|
497
|
+
];
|
|
498
|
+
if (projection.attention_work.length) {
|
|
499
|
+
lines.push(`attention: ${projection.attention_work.join(", ")}`);
|
|
500
|
+
}
|
|
501
|
+
emit.text(`${lines.join("\n")}\n`);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function emitSupervisorReport(
|
|
505
|
+
report: SupervisorRunReport,
|
|
506
|
+
json: boolean | undefined,
|
|
507
|
+
emit: EmitContext,
|
|
508
|
+
): void {
|
|
509
|
+
if (json) {
|
|
510
|
+
emit.config({ format: "json" });
|
|
511
|
+
emit.data(report);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
emit.text(
|
|
515
|
+
`supervisor ${report.goal_id}: ${report.stop_reason}\n` +
|
|
516
|
+
`reason: ${report.reason}\n` +
|
|
517
|
+
`cycles: ${report.cycles}; dispatches: ${report.dispatches}; acceptances: ${report.acceptances}\n` +
|
|
518
|
+
`state: ${report.projection.state}; next: ${report.projection.next_action}\n`,
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function renderSupervisorRow(record: SupervisorRecord): string {
|
|
523
|
+
const projection = record.projection;
|
|
524
|
+
return `${record.intent.id} ${projection.state.padEnd(20)} ${projection.attempts_used}/${record.intent.limits.max_total_attempts} ${projection.next_action.padEnd(18)} ${record.intent.title}`;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function integer(value: string | undefined): number | undefined {
|
|
528
|
+
if (value === undefined) return undefined;
|
|
529
|
+
if (!/^\d+$/.test(value))
|
|
530
|
+
throw new Error(`expected a positive integer, got ${JSON.stringify(value)}`);
|
|
531
|
+
return Number.parseInt(value, 10);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function delay(ms: number): Promise<void> {
|
|
535
|
+
return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function withSupervisorRoot(emit: EmitContext, fn: (coordRoot: string) => void): void {
|
|
539
|
+
const coordRoot = findCoordRoot();
|
|
540
|
+
if (!coordRoot) {
|
|
541
|
+
emit.error({
|
|
542
|
+
code: "no_coord_root",
|
|
543
|
+
message: "no .harnery/ coordination root found; run `init` first",
|
|
544
|
+
});
|
|
545
|
+
emit.setExitCode(1);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
try {
|
|
549
|
+
fn(coordRoot);
|
|
550
|
+
} catch (error) {
|
|
551
|
+
emit.error({ code: "supervisor_failed", message: (error as Error).message });
|
|
552
|
+
emit.setExitCode(1);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
async function withSupervisorRootAsync(
|
|
557
|
+
emit: EmitContext,
|
|
558
|
+
fn: (coordRoot: string) => Promise<void>,
|
|
559
|
+
): Promise<void> {
|
|
560
|
+
const coordRoot = findCoordRoot();
|
|
561
|
+
if (!coordRoot) {
|
|
562
|
+
emit.error({
|
|
563
|
+
code: "no_coord_root",
|
|
564
|
+
message: "no .harnery/ coordination root found; run `init` first",
|
|
565
|
+
});
|
|
566
|
+
emit.setExitCode(1);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
try {
|
|
570
|
+
await fn(coordRoot);
|
|
571
|
+
} catch (error) {
|
|
572
|
+
emit.error({ code: "supervisor_failed", message: (error as Error).message });
|
|
573
|
+
emit.setExitCode(1);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type RunSupervisorInput,
|
|
3
|
+
runSupervisor,
|
|
4
|
+
type SupervisorDispatchOutcome,
|
|
5
|
+
type SupervisorRunReport,
|
|
6
|
+
type SupervisorStopReason,
|
|
7
|
+
} from "./runner.ts";
|
|
8
|
+
export {
|
|
9
|
+
type ConfigureSupervisorServiceInput,
|
|
10
|
+
configureSupervisorService,
|
|
11
|
+
type RunSupervisorServiceDaemonInput,
|
|
12
|
+
type RunSupervisorServiceSweepInput,
|
|
13
|
+
readSupervisorServiceConfig,
|
|
14
|
+
readSupervisorServiceRuntime,
|
|
15
|
+
readSupervisorServiceStatus,
|
|
16
|
+
requestSupervisorServiceStop,
|
|
17
|
+
runSupervisorServiceDaemon,
|
|
18
|
+
runSupervisorServiceSweep,
|
|
19
|
+
SUPERVISOR_SERVICE_CONFIG_SCHEMA_VERSION,
|
|
20
|
+
SUPERVISOR_SERVICE_RUNTIME_SCHEMA_VERSION,
|
|
21
|
+
SUPERVISOR_SERVICE_STATUS_SCHEMA_VERSION,
|
|
22
|
+
type SupervisorServiceConfig,
|
|
23
|
+
type SupervisorServiceEngineConfig,
|
|
24
|
+
type SupervisorServiceGoalRuntime,
|
|
25
|
+
type SupervisorServiceGoalState,
|
|
26
|
+
type SupervisorServiceProcessState,
|
|
27
|
+
type SupervisorServiceRuntime,
|
|
28
|
+
type SupervisorServiceStatus,
|
|
29
|
+
type SupervisorServiceStatusRecord,
|
|
30
|
+
type SupervisorServiceSweepOutcome,
|
|
31
|
+
type SupervisorServiceSweepReport,
|
|
32
|
+
spawnSupervisorService,
|
|
33
|
+
supervisorServiceLogPath,
|
|
34
|
+
} from "./service.ts";
|
|
35
|
+
export {
|
|
36
|
+
assertSupervisorId,
|
|
37
|
+
type CreateSupervisorInput,
|
|
38
|
+
collectSupervisorWork,
|
|
39
|
+
createSupervisor,
|
|
40
|
+
listSupervisors,
|
|
41
|
+
newSupervisorId,
|
|
42
|
+
readSupervisor,
|
|
43
|
+
SUPERVISOR_INTENT_SCHEMA_VERSION,
|
|
44
|
+
type SupervisorAutomationPolicy,
|
|
45
|
+
type SupervisorIntent,
|
|
46
|
+
type SupervisorLimits,
|
|
47
|
+
type SupervisorNextAction,
|
|
48
|
+
type SupervisorProjection,
|
|
49
|
+
type SupervisorRecord,
|
|
50
|
+
type SupervisorState,
|
|
51
|
+
} from "./state.ts";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
SupervisorServiceConfig,
|
|
3
|
+
SupervisorServiceGoalRuntime,
|
|
4
|
+
SupervisorServiceGoalState,
|
|
5
|
+
SupervisorServiceProcessState,
|
|
6
|
+
SupervisorServiceRuntime,
|
|
7
|
+
SupervisorServiceStatus,
|
|
8
|
+
SupervisorServiceStatusRecord,
|
|
9
|
+
} from "./service.ts";
|
|
10
|
+
export {
|
|
11
|
+
readSupervisorServiceConfig,
|
|
12
|
+
readSupervisorServiceRuntime,
|
|
13
|
+
readSupervisorServiceStatus,
|
|
14
|
+
supervisorServiceLogPath,
|
|
15
|
+
} from "./service-read.ts";
|
|
16
|
+
export {
|
|
17
|
+
assertSupervisorId,
|
|
18
|
+
collectSupervisorWork,
|
|
19
|
+
listSupervisors,
|
|
20
|
+
readSupervisor,
|
|
21
|
+
SUPERVISOR_INTENT_SCHEMA_VERSION,
|
|
22
|
+
type SupervisorAutomationPolicy,
|
|
23
|
+
type SupervisorIntent,
|
|
24
|
+
type SupervisorLimits,
|
|
25
|
+
type SupervisorNextAction,
|
|
26
|
+
type SupervisorProjection,
|
|
27
|
+
type SupervisorRecord,
|
|
28
|
+
type SupervisorState,
|
|
29
|
+
} from "./state.ts";
|