harnery 0.15.0 → 0.17.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.
Files changed (48) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/supervisor.d.ts.map +1 -1
  3. package/dist/commands/supervisor.js +393 -4
  4. package/dist/core/supervisor/index.d.ts +4 -0
  5. package/dist/core/supervisor/index.d.ts.map +1 -1
  6. package/dist/core/supervisor/index.js +4 -0
  7. package/dist/core/supervisor/plan-api.d.ts +16 -0
  8. package/dist/core/supervisor/plan-api.d.ts.map +1 -0
  9. package/dist/core/supervisor/plan-api.js +34 -0
  10. package/dist/core/supervisor/plan-read.d.ts +4 -0
  11. package/dist/core/supervisor/plan-read.d.ts.map +1 -0
  12. package/dist/core/supervisor/plan-read.js +269 -0
  13. package/dist/core/supervisor/plan-types.d.ts +113 -0
  14. package/dist/core/supervisor/plan-types.d.ts.map +1 -0
  15. package/dist/core/supervisor/plan-types.js +14 -0
  16. package/dist/core/supervisor/planning.d.ts +26 -0
  17. package/dist/core/supervisor/planning.d.ts.map +1 -0
  18. package/dist/core/supervisor/planning.js +486 -0
  19. package/dist/core/supervisor/read.d.ts +4 -0
  20. package/dist/core/supervisor/read.d.ts.map +1 -1
  21. package/dist/core/supervisor/read.js +3 -0
  22. package/dist/core/supervisor/runner.d.ts +3 -0
  23. package/dist/core/supervisor/runner.d.ts.map +1 -1
  24. package/dist/core/supervisor/runner.js +37 -7
  25. package/dist/core/supervisor/service-read.d.ts +6 -0
  26. package/dist/core/supervisor/service-read.d.ts.map +1 -0
  27. package/dist/core/supervisor/service-read.js +127 -0
  28. package/dist/core/supervisor/service.d.ts +114 -0
  29. package/dist/core/supervisor/service.d.ts.map +1 -0
  30. package/dist/core/supervisor/service.js +706 -0
  31. package/dist/core/supervisor/state.d.ts +11 -1
  32. package/dist/core/supervisor/state.d.ts.map +1 -1
  33. package/dist/core/supervisor/state.js +186 -7
  34. package/dist/lib/tunnel/gate.d.ts.map +1 -1
  35. package/dist/lib/tunnel/gate.js +6 -1
  36. package/package.json +1 -1
  37. package/src/commands/supervisor.ts +490 -3
  38. package/src/core/supervisor/index.ts +45 -0
  39. package/src/core/supervisor/plan-api.ts +47 -0
  40. package/src/core/supervisor/plan-read.ts +299 -0
  41. package/src/core/supervisor/plan-types.ts +144 -0
  42. package/src/core/supervisor/planning.ts +576 -0
  43. package/src/core/supervisor/read.ts +30 -0
  44. package/src/core/supervisor/runner.ts +39 -2
  45. package/src/core/supervisor/service-read.ts +144 -0
  46. package/src/core/supervisor/service.ts +927 -0
  47. package/src/core/supervisor/state.ts +235 -6
  48. package/src/lib/tunnel/gate.ts +7 -1
@@ -1,5 +1,5 @@
1
- import { readFileSync } from "node:fs";
2
- import { resolve } from "node:path";
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, isAbsolute, resolve } from "node:path";
3
3
  import type { Command } from "commander";
4
4
  import type { EmitContext } from "../commander.ts";
5
5
  import { workflowSubscriptionOnly } from "../core/config.ts";
@@ -8,12 +8,27 @@ import { findCoordRoot } from "../core/hooks/resolve/coord-root.ts";
8
8
  import type { PolicyIsolation } from "../core/policy/index.ts";
9
9
  import { loadPolicyFile } from "../core/policy/index.ts";
10
10
  import {
11
+ approveSupervisorPlan,
12
+ type CreateSupervisorReplanningInput,
13
+ configureSupervisorService,
11
14
  createSupervisor,
12
15
  listSupervisors,
13
16
  readSupervisor,
17
+ readSupervisorPlan,
18
+ readSupervisorServiceConfig,
19
+ readSupervisorServiceStatus,
20
+ rejectSupervisorPlan,
21
+ requestSupervisorServiceStop,
14
22
  runSupervisor,
23
+ runSupervisorServiceDaemon,
24
+ type SupervisorPlanOutcome,
25
+ type SupervisorPlanRecord,
15
26
  type SupervisorRecord,
16
27
  type SupervisorRunReport,
28
+ type SupervisorServiceConfig,
29
+ type SupervisorServiceStatus,
30
+ spawnSupervisorService,
31
+ supervisorServiceLogPath,
17
32
  } from "../core/supervisor/index.ts";
18
33
  import type { WorkflowSpecialistProfile } from "../core/workflow/index.ts";
19
34
 
@@ -30,6 +45,7 @@ interface CreateOpts {
30
45
  acceptPassingProof?: boolean;
31
46
  resumeApproved?: boolean;
32
47
  retryBlocked?: boolean;
48
+ replanning?: string;
33
49
  json?: boolean;
34
50
  }
35
51
 
@@ -45,6 +61,13 @@ interface RunOpts {
45
61
  json?: boolean;
46
62
  }
47
63
 
64
+ interface ServiceOpts extends RunOpts {
65
+ wakeIntervalMs?: string;
66
+ heartbeatIntervalMs?: string;
67
+ errorBackoffBaseMs?: string;
68
+ errorBackoffMaxMs?: string;
69
+ }
70
+
48
71
  export function registerSupervisorCommand(program: Command, emit: EmitContext): void {
49
72
  const registry = createBuiltinHarnessRegistry();
50
73
  const supervisor = program
@@ -66,6 +89,7 @@ export function registerSupervisorCommand(program: Command, emit: EmitContext):
66
89
  .option("--accept-passing-proof", "Allow the supervisor to explicitly accept passing proof")
67
90
  .option("--no-resume-approved", "Stop after an approval instead of resuming its parked run")
68
91
  .option("--retry-blocked", "Allow bounded retry of blocked work")
92
+ .option("--replanning <file>", "Frozen planner policy and allowed workflow-template catalog")
69
93
  .option("--json", "Emit the complete supervisor record as JSON")
70
94
  .action((rootWorkId: string, opts: CreateOpts) => {
71
95
  withSupervisorRoot(emit, (coordRoot) => {
@@ -97,6 +121,7 @@ export function registerSupervisorCommand(program: Command, emit: EmitContext):
97
121
  resume_approved: opts.resumeApproved,
98
122
  retry_blocked: opts.retryBlocked,
99
123
  },
124
+ replanning: opts.replanning ? readReplanningFile(opts.replanning) : undefined,
100
125
  }),
101
126
  opts.json,
102
127
  emit,
@@ -133,10 +158,341 @@ export function registerSupervisorCommand(program: Command, emit: EmitContext):
133
158
  });
134
159
  });
135
160
 
161
+ registerPlanCommand(supervisor, emit);
162
+ registerServiceCommand(supervisor, registry, emit);
136
163
  registerRunCommand(supervisor, "tick", registry, emit);
137
164
  registerRunCommand(supervisor, "run", registry, emit);
138
165
  }
139
166
 
167
+ function registerPlanCommand(supervisor: Command, emit: EmitContext): void {
168
+ const plan = supervisor
169
+ .command("plan")
170
+ .description("Inspect and resolve bounded planner proposals for a durable goal.");
171
+
172
+ plan
173
+ .command("list <goal-id>")
174
+ .description("List append-only replanning attempts for one goal.")
175
+ .option("--json", "Emit complete plan records as JSON")
176
+ .action((goalId: string, opts: { json?: boolean }) => {
177
+ withSupervisorRoot(emit, (coordRoot) => {
178
+ const plans = readSupervisor(coordRoot, goalId).plans;
179
+ if (opts.json) {
180
+ emit.config({ format: "json" });
181
+ emit.data(plans);
182
+ } else if (plans.length === 0) {
183
+ emit.text(`supervisor ${goalId} has no replanning attempts\n`);
184
+ } else {
185
+ emit.text(`${plans.map(renderPlanRow).join("\n")}\n`);
186
+ }
187
+ });
188
+ });
189
+
190
+ plan
191
+ .command("show <goal-id> <plan-id>")
192
+ .description("Show one planner request, schema-gated proposal, and audit events.")
193
+ .option("--json", "Emit the complete plan record as JSON")
194
+ .action((goalId: string, planId: string, opts: { json?: boolean }) => {
195
+ withSupervisorRoot(emit, (coordRoot) => {
196
+ emitPlan(readSupervisorPlan(coordRoot, goalId, planId), opts.json, emit);
197
+ });
198
+ });
199
+
200
+ plan
201
+ .command("approve <goal-id> <plan-id>")
202
+ .description("Materialize a reviewed proposal and advance the active immutable root.")
203
+ .option("--actor <name>", "Actor recorded on the plan decision")
204
+ .option("--reason <text>", "Reason for approval")
205
+ .option("--json", "Emit the plan outcome as JSON")
206
+ .action(
207
+ (
208
+ goalId: string,
209
+ planId: string,
210
+ opts: { actor?: string; reason?: string; json?: boolean },
211
+ ) => {
212
+ withSupervisorRoot(emit, (coordRoot) => {
213
+ emitPlanOutcome(
214
+ approveSupervisorPlan({
215
+ coordRoot,
216
+ goalId,
217
+ planId,
218
+ actor: opts.actor,
219
+ reason: opts.reason,
220
+ }),
221
+ opts.json,
222
+ emit,
223
+ );
224
+ });
225
+ },
226
+ );
227
+
228
+ plan
229
+ .command("reject <goal-id> <plan-id>")
230
+ .description("Reject a pending proposal without mutating durable work.")
231
+ .requiredOption("--reason <text>", "Reason the proposal is rejected")
232
+ .option("--actor <name>", "Actor recorded on the plan decision")
233
+ .option("--json", "Emit the plan outcome as JSON")
234
+ .action(
235
+ (
236
+ goalId: string,
237
+ planId: string,
238
+ opts: { actor?: string; reason: string; json?: boolean },
239
+ ) => {
240
+ withSupervisorRoot(emit, (coordRoot) => {
241
+ emitPlanOutcome(
242
+ rejectSupervisorPlan({
243
+ coordRoot,
244
+ goalId,
245
+ planId,
246
+ actor: opts.actor,
247
+ reason: opts.reason,
248
+ }),
249
+ opts.json,
250
+ emit,
251
+ );
252
+ });
253
+ },
254
+ );
255
+ }
256
+
257
+ function registerServiceCommand(
258
+ supervisor: Command,
259
+ registry: ReturnType<typeof createBuiltinHarnessRegistry>,
260
+ emit: EmitContext,
261
+ ): void {
262
+ const service = supervisor
263
+ .command("service")
264
+ .description("Run explicitly enrolled durable goals in a restartable background service.");
265
+
266
+ addServiceOptions(
267
+ service
268
+ .command("start [goal-ids...]")
269
+ .description("Configure and start the detached per-repository supervisor service."),
270
+ )
271
+ .option("--json", "Emit complete service status as JSON")
272
+ .action(async (goalIds: string[], opts: ServiceOpts) => {
273
+ await withSupervisorRootAsync(emit, async (coordRoot) => {
274
+ prepareServiceConfig(coordRoot, goalIds, opts, registry);
275
+ const status = await spawnSupervisorService(coordRoot);
276
+ emitServiceStatus(status, opts.json, emit);
277
+ });
278
+ });
279
+
280
+ addServiceOptions(
281
+ service
282
+ .command("run [goal-ids...]")
283
+ .description("Run the persistent service loop in the foreground for a process manager."),
284
+ )
285
+ .option("--json", "Emit terminal service status as JSON")
286
+ .action(async (goalIds: string[], opts: ServiceOpts) => {
287
+ await withSupervisorRootAsync(emit, async (coordRoot) => {
288
+ const config = prepareServiceConfig(coordRoot, goalIds, opts, registry);
289
+ const status = await runSupervisorServiceDaemon({
290
+ coordRoot,
291
+ engine: serviceEngine(config, registry),
292
+ onLog: opts.json ? undefined : (line) => emit.text(`${line}\n`),
293
+ });
294
+ if (opts.json) {
295
+ emit.config({ format: "json" });
296
+ emit.data(status);
297
+ }
298
+ });
299
+ });
300
+
301
+ service
302
+ .command("status")
303
+ .description("Show service liveness, heartbeat, enrolled goals, and durable wake state.")
304
+ .option("--json", "Emit complete service status as JSON")
305
+ .action((opts: { json?: boolean }) => {
306
+ withSupervisorRoot(emit, (coordRoot) => {
307
+ emitServiceStatus(readSupervisorServiceStatus(coordRoot), opts.json, emit);
308
+ });
309
+ });
310
+
311
+ service
312
+ .command("stop")
313
+ .description("Request a graceful stop; an active goal tick is allowed to finish safely.")
314
+ .option("--json", "Emit complete service status as JSON")
315
+ .action(async (opts: { json?: boolean }) => {
316
+ await withSupervisorRootAsync(emit, async (coordRoot) => {
317
+ let status = requestSupervisorServiceStop(coordRoot);
318
+ const deadline = Date.now() + 5_000;
319
+ while (status.running && Date.now() < deadline) {
320
+ await delay(50);
321
+ status = readSupervisorServiceStatus(coordRoot);
322
+ }
323
+ emitServiceStatus(status, opts.json, emit);
324
+ });
325
+ });
326
+
327
+ service
328
+ .command("logs")
329
+ .description("Show the tail of the private background-service log.")
330
+ .option("--lines <n>", "Number of lines to show (default 50)", "50")
331
+ .action((opts: { lines: string }) => {
332
+ withSupervisorRoot(emit, (coordRoot) => {
333
+ const lines = integer(opts.lines) ?? 50;
334
+ if (lines > 10_000) throw new Error("service log lines must not exceed 10000");
335
+ const path = supervisorServiceLogPath(coordRoot);
336
+ if (!existsSync(path)) {
337
+ emit.text("no supervisor service log\n");
338
+ return;
339
+ }
340
+ const selected = readFileSync(path, "utf8")
341
+ .split("\n")
342
+ .slice(-(lines + 1))
343
+ .join("\n");
344
+ emit.text(selected.endsWith("\n") ? selected : `${selected}\n`);
345
+ });
346
+ });
347
+
348
+ service
349
+ .command("daemon", { hidden: true })
350
+ .description("Internal detached service entrypoint.")
351
+ .action(async () => {
352
+ await withSupervisorRootAsync(emit, async (coordRoot) => {
353
+ const config = readSupervisorServiceConfig(coordRoot);
354
+ await runSupervisorServiceDaemon({
355
+ coordRoot,
356
+ engine: serviceEngine(config, registry),
357
+ onLog: (line) => emit.text(`${line}\n`),
358
+ });
359
+ });
360
+ });
361
+ }
362
+
363
+ function addServiceOptions(command: Command): Command {
364
+ return command
365
+ .option("--wake-interval-ms <n>", "State-change poll interval (default 5000)")
366
+ .option("--heartbeat-interval-ms <n>", "Heartbeat interval (default 2000)")
367
+ .option("--error-backoff-base-ms <n>", "Initial service-error backoff (default 2000)")
368
+ .option("--error-backoff-max-ms <n>", "Maximum service-error backoff (default 300000)")
369
+ .option("--harness <name>", "Fallback harness for agent calls without a specialist")
370
+ .option("--cwd <dir>", "Working directory for child agents")
371
+ .option("--subscription-only", "Require stored harness-login billing")
372
+ .option("--allow-api-billing", "Permit API-key override billing")
373
+ .option("--policy <file>", "Host policy JSON/JSONC")
374
+ .option("--isolation <mode>", "shared | worktree | sandbox | remote")
375
+ .option("--approval-to <address>", "Address durable ASK requests");
376
+ }
377
+
378
+ function prepareServiceConfig(
379
+ coordRoot: string,
380
+ goalIds: string[],
381
+ opts: ServiceOpts,
382
+ registry: ReturnType<typeof createBuiltinHarnessRegistry>,
383
+ ): SupervisorServiceConfig {
384
+ if (goalIds.length === 0) {
385
+ if (hasServiceConfigOverrides(opts)) {
386
+ throw new Error("goal ids are required when changing supervisor service options");
387
+ }
388
+ return readSupervisorServiceConfig(coordRoot);
389
+ }
390
+ if (opts.harness && !registry.get(opts.harness)) {
391
+ throw new Error(`unknown harness ${JSON.stringify(opts.harness)}`);
392
+ }
393
+ const policy = opts.policy ? loadPolicyFile(opts.policy) : undefined;
394
+ return configureSupervisorService({
395
+ coordRoot,
396
+ goalIds,
397
+ wakeIntervalMs: integer(opts.wakeIntervalMs),
398
+ heartbeatIntervalMs: integer(opts.heartbeatIntervalMs),
399
+ errorBackoffBaseMs: integer(opts.errorBackoffBaseMs),
400
+ errorBackoffMaxMs: integer(opts.errorBackoffMaxMs),
401
+ engine: {
402
+ default_harness: opts.harness,
403
+ cwd: opts.cwd,
404
+ subscription_only:
405
+ opts.subscriptionOnly === true ? true : workflowSubscriptionOnly(coordRoot),
406
+ allow_api_billing: opts.allowApiBilling,
407
+ policy,
408
+ isolation: opts.isolation,
409
+ approval_addressee: opts.approvalTo,
410
+ },
411
+ });
412
+ }
413
+
414
+ function hasServiceConfigOverrides(opts: ServiceOpts): boolean {
415
+ return Boolean(
416
+ opts.wakeIntervalMs ||
417
+ opts.heartbeatIntervalMs ||
418
+ opts.errorBackoffBaseMs ||
419
+ opts.errorBackoffMaxMs ||
420
+ opts.harness ||
421
+ opts.cwd ||
422
+ opts.subscriptionOnly ||
423
+ opts.allowApiBilling ||
424
+ opts.policy ||
425
+ opts.isolation ||
426
+ opts.approvalTo,
427
+ );
428
+ }
429
+
430
+ function serviceEngine(
431
+ config: SupervisorServiceConfig,
432
+ registry: ReturnType<typeof createBuiltinHarnessRegistry>,
433
+ ) {
434
+ return {
435
+ spawners: registry.spawners(),
436
+ defaultHarness: config.engine.default_harness,
437
+ cwd: config.engine.cwd,
438
+ subscriptionOnly: config.engine.subscription_only,
439
+ allowApiBilling: config.engine.allow_api_billing,
440
+ harnessEvidence: Object.fromEntries(
441
+ registry
442
+ .list()
443
+ .map((adapter) => [
444
+ adapter.profile.id,
445
+ { toolEvidence: adapter.profile.capabilities.toolEvidence },
446
+ ]),
447
+ ),
448
+ policy: config.engine.policy,
449
+ approvalMode: "park" as const,
450
+ approvalAddressee: config.engine.approval_addressee,
451
+ isolation: config.engine.isolation,
452
+ networkAccess: "enabled" as const,
453
+ };
454
+ }
455
+
456
+ function emitServiceStatus(
457
+ status: SupervisorServiceStatus,
458
+ json: boolean | undefined,
459
+ emit: EmitContext,
460
+ ): void {
461
+ if (json) {
462
+ emit.config({ format: "json" });
463
+ emit.data(status);
464
+ return;
465
+ }
466
+ if (!status.config && !status.record) {
467
+ emit.text("supervisor service: unconfigured\n");
468
+ return;
469
+ }
470
+ const state = status.running
471
+ ? (status.record?.state ?? "running")
472
+ : status.stale
473
+ ? "stale"
474
+ : "stopped";
475
+ const lines = [
476
+ `supervisor service: ${state}`,
477
+ `goals: ${status.config?.goal_ids.join(", ") || "none"}`,
478
+ ];
479
+ if (status.record) {
480
+ lines.push(`pid: ${status.record.pid} on ${status.record.host}`);
481
+ lines.push(`heartbeat: ${status.record.heartbeat_at}`);
482
+ lines.push(`sweeps: ${status.record.sweep_count}`);
483
+ if (status.record.active_goal_id) lines.push(`active: ${status.record.active_goal_id}`);
484
+ if (status.record.next_wake_at) lines.push(`next wake: ${status.record.next_wake_at}`);
485
+ if (status.record.last_error) lines.push(`error: ${status.record.last_error}`);
486
+ }
487
+ const backingOff = Object.entries(status.runtime?.goals ?? {}).filter(
488
+ ([, runtime]) => runtime.state === "backoff",
489
+ );
490
+ if (backingOff.length) {
491
+ lines.push(`backoff: ${backingOff.map(([goalId]) => goalId).join(", ")}`);
492
+ }
493
+ emit.text(`${lines.join("\n")}\n`);
494
+ }
495
+
140
496
  function registerRunCommand(
141
497
  supervisor: Command,
142
498
  mode: "tick" | "run",
@@ -211,6 +567,65 @@ function readTeamFile(path: string): Record<string, WorkflowSpecialistProfile> {
211
567
  return value as Record<string, WorkflowSpecialistProfile>;
212
568
  }
213
569
 
570
+ function readReplanningFile(path: string): CreateSupervisorReplanningInput {
571
+ const absolute = resolve(path);
572
+ let value: unknown;
573
+ try {
574
+ value = JSON.parse(readFileSync(absolute, "utf8"));
575
+ } catch (error) {
576
+ throw new Error(
577
+ `cannot read supervisor replanning policy at ${absolute}: ${(error as Error).message}`,
578
+ );
579
+ }
580
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
581
+ throw new Error("supervisor replanning policy must be a JSON object");
582
+ }
583
+ const config = value as Record<string, unknown>;
584
+ if (typeof config.planner_specialist !== "string") {
585
+ throw new Error("supervisor replanning planner_specialist must be a string");
586
+ }
587
+ if (
588
+ !config.templates ||
589
+ typeof config.templates !== "object" ||
590
+ Array.isArray(config.templates)
591
+ ) {
592
+ throw new Error("supervisor replanning templates must be an object");
593
+ }
594
+ const base = dirname(absolute);
595
+ const templates = Object.fromEntries(
596
+ Object.entries(config.templates as Record<string, unknown>).map(([id, raw]) => {
597
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
598
+ throw new Error(`supervisor replanning template ${id} must be an object`);
599
+ }
600
+ const template = raw as Record<string, unknown>;
601
+ if (typeof template.workflow !== "string") {
602
+ throw new Error(`supervisor replanning template ${id} workflow must be a string`);
603
+ }
604
+ return [
605
+ id,
606
+ {
607
+ workflowPath: isAbsolute(template.workflow)
608
+ ? template.workflow
609
+ : resolve(base, template.workflow),
610
+ maxAttempts: jsonInteger(template.max_attempts, `template ${id} max_attempts`),
611
+ root: jsonBoolean(template.root, `template ${id} root`),
612
+ },
613
+ ];
614
+ }),
615
+ );
616
+ return {
617
+ plannerSpecialist: config.planner_specialist,
618
+ autoApply: jsonBoolean(config.auto_apply, "replanning auto_apply"),
619
+ maxReplans: jsonInteger(config.max_replans, "replanning max_replans"),
620
+ maxWorkItemsPerPlan: jsonInteger(
621
+ config.max_work_items_per_plan,
622
+ "replanning max_work_items_per_plan",
623
+ ),
624
+ maxTotalWorkItems: jsonInteger(config.max_total_work_items, "replanning max_total_work_items"),
625
+ templates,
626
+ };
627
+ }
628
+
214
629
  function emitSupervisor(
215
630
  record: SupervisorRecord,
216
631
  json: boolean | undefined,
@@ -230,7 +645,7 @@ function emitSupervisor(
230
645
  const lines = [
231
646
  `${record.intent.id}: ${record.intent.title}`,
232
647
  `state: ${projection.state}`,
233
- `root: ${record.intent.root_work_id}`,
648
+ `root: ${projection.root_work_id}`,
234
649
  `reason: ${projection.reason}`,
235
650
  `next: ${projection.next_action}`,
236
651
  `work: ${projection.work_ids.length}`,
@@ -239,6 +654,15 @@ function emitSupervisor(
239
654
  `automation: accept=${record.intent.automation.accept_passing_proof}, ` +
240
655
  `resume=${record.intent.automation.resume_approved}, retry=${record.intent.automation.retry_blocked}`,
241
656
  ];
657
+ if (record.intent.replanning) {
658
+ lines.push(
659
+ `replanning: generation=${projection.plan_generation}, used=${projection.replans_used}/${record.intent.replanning.max_replans}, auto_apply=${record.intent.replanning.auto_apply}`,
660
+ );
661
+ if (projection.root_work_id !== record.intent.root_work_id) {
662
+ lines.push(`original root: ${record.intent.root_work_id}`);
663
+ }
664
+ if (projection.pending_plan_id) lines.push(`pending plan: ${projection.pending_plan_id}`);
665
+ }
242
666
  if (projection.attention_work.length) {
243
667
  lines.push(`attention: ${projection.attention_work.join(", ")}`);
244
668
  }
@@ -259,6 +683,7 @@ function emitSupervisorReport(
259
683
  `supervisor ${report.goal_id}: ${report.stop_reason}\n` +
260
684
  `reason: ${report.reason}\n` +
261
685
  `cycles: ${report.cycles}; dispatches: ${report.dispatches}; acceptances: ${report.acceptances}\n` +
686
+ `replans: ${report.replans}\n` +
262
687
  `state: ${report.projection.state}; next: ${report.projection.next_action}\n`,
263
688
  );
264
689
  }
@@ -275,6 +700,68 @@ function integer(value: string | undefined): number | undefined {
275
700
  return Number.parseInt(value, 10);
276
701
  }
277
702
 
703
+ function jsonInteger(value: unknown, field: string): number | undefined {
704
+ if (value === undefined) return undefined;
705
+ if (!Number.isSafeInteger(value) || (value as number) < 1) {
706
+ throw new Error(`${field} must be a positive integer`);
707
+ }
708
+ return value as number;
709
+ }
710
+
711
+ function jsonBoolean(value: unknown, field: string): boolean | undefined {
712
+ if (value === undefined) return undefined;
713
+ if (typeof value !== "boolean") throw new Error(`${field} must be boolean`);
714
+ return value;
715
+ }
716
+
717
+ function emitPlan(plan: SupervisorPlanRecord, json: boolean | undefined, emit: EmitContext): void {
718
+ if (json) {
719
+ emit.config({ format: "json" });
720
+ emit.data(plan);
721
+ return;
722
+ }
723
+ const lines = [
724
+ `${plan.request.id}: ${plan.status}`,
725
+ `goal: ${plan.request.goal_id}`,
726
+ `sequence: ${plan.request.sequence}`,
727
+ `prior root: ${plan.request.prior_root_work_id}`,
728
+ `planner run: ${plan.request.workflow_run_id}`,
729
+ ];
730
+ if (plan.proposal) {
731
+ lines.push(`decision: ${plan.proposal.decision}`);
732
+ lines.push(`rationale: ${plan.proposal.rationale}`);
733
+ lines.push(`proposed work: ${plan.proposal.work.length}`);
734
+ }
735
+ if (plan.root_work_id) lines.push(`applied root: ${plan.root_work_id}`);
736
+ if (plan.approval_id) lines.push(`approval: ${plan.approval_id}`);
737
+ emit.text(`${lines.join("\n")}\n`);
738
+ }
739
+
740
+ function emitPlanOutcome(
741
+ outcome: SupervisorPlanOutcome,
742
+ json: boolean | undefined,
743
+ emit: EmitContext,
744
+ ): void {
745
+ if (json) {
746
+ emit.config({ format: "json" });
747
+ emit.data(outcome);
748
+ return;
749
+ }
750
+ emit.text(
751
+ `supervisor plan ${outcome.plan_id}: ${outcome.status}\n` +
752
+ `${outcome.reason ? `reason: ${outcome.reason}\n` : ""}` +
753
+ `${outcome.root_work_id ? `root: ${outcome.root_work_id}\n` : ""}`,
754
+ );
755
+ }
756
+
757
+ function renderPlanRow(plan: SupervisorPlanRecord): string {
758
+ return `${plan.request.id} ${plan.status.padEnd(18)} ${plan.request.prior_root_work_id}`;
759
+ }
760
+
761
+ function delay(ms: number): Promise<void> {
762
+ return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
763
+ }
764
+
278
765
  function withSupervisorRoot(emit: EmitContext, fn: (coordRoot: string) => void): void {
279
766
  const coordRoot = findCoordRoot();
280
767
  if (!coordRoot) {
@@ -1,3 +1,21 @@
1
+ export { approveSupervisorPlan, rejectSupervisorPlan } from "./plan-api.ts";
2
+ export { readSupervisorPlan, readSupervisorPlans } from "./plan-read.ts";
3
+ export {
4
+ type CreateSupervisorPlanTemplateInput,
5
+ type CreateSupervisorReplanningInput,
6
+ SUPERVISOR_PLAN_SCHEMA_VERSION,
7
+ type SupervisorPlanEvent,
8
+ type SupervisorPlanEventType,
9
+ type SupervisorPlanHistory,
10
+ type SupervisorPlanOutcome,
11
+ type SupervisorPlanProposal,
12
+ type SupervisorPlanRecord,
13
+ type SupervisorPlanRequest,
14
+ type SupervisorPlanStatus,
15
+ type SupervisorPlanTemplate,
16
+ type SupervisorPlanWorkSpec,
17
+ type SupervisorReplanningPolicy,
18
+ } from "./plan-types.ts";
1
19
  export {
2
20
  type RunSupervisorInput,
3
21
  runSupervisor,
@@ -5,6 +23,33 @@ export {
5
23
  type SupervisorRunReport,
6
24
  type SupervisorStopReason,
7
25
  } from "./runner.ts";
26
+ export {
27
+ type ConfigureSupervisorServiceInput,
28
+ configureSupervisorService,
29
+ type RunSupervisorServiceDaemonInput,
30
+ type RunSupervisorServiceSweepInput,
31
+ readSupervisorServiceConfig,
32
+ readSupervisorServiceRuntime,
33
+ readSupervisorServiceStatus,
34
+ requestSupervisorServiceStop,
35
+ runSupervisorServiceDaemon,
36
+ runSupervisorServiceSweep,
37
+ SUPERVISOR_SERVICE_CONFIG_SCHEMA_VERSION,
38
+ SUPERVISOR_SERVICE_RUNTIME_SCHEMA_VERSION,
39
+ SUPERVISOR_SERVICE_STATUS_SCHEMA_VERSION,
40
+ type SupervisorServiceConfig,
41
+ type SupervisorServiceEngineConfig,
42
+ type SupervisorServiceGoalRuntime,
43
+ type SupervisorServiceGoalState,
44
+ type SupervisorServiceProcessState,
45
+ type SupervisorServiceRuntime,
46
+ type SupervisorServiceStatus,
47
+ type SupervisorServiceStatusRecord,
48
+ type SupervisorServiceSweepOutcome,
49
+ type SupervisorServiceSweepReport,
50
+ spawnSupervisorService,
51
+ supervisorServiceLogPath,
52
+ } from "./service.ts";
8
53
  export {
9
54
  assertSupervisorId,
10
55
  type CreateSupervisorInput,
@@ -0,0 +1,47 @@
1
+ import type { SupervisorPlanOutcome } from "./plan-types.ts";
2
+ import { applySupervisorPlanProposal, rejectSupervisorPlanProposal } from "./planning.ts";
3
+ import { acquireSupervisorLease, readSupervisorIgnoringLease } from "./state.ts";
4
+
5
+ export function approveSupervisorPlan(input: {
6
+ coordRoot: string;
7
+ goalId: string;
8
+ planId: string;
9
+ actor?: string;
10
+ reason?: string;
11
+ }): SupervisorPlanOutcome {
12
+ const actor = input.actor?.trim() || `supervisor:${input.goalId}`;
13
+ const release = acquireSupervisorLease(input.coordRoot, input.goalId);
14
+ try {
15
+ return applySupervisorPlanProposal({
16
+ coordRoot: input.coordRoot,
17
+ record: readSupervisorIgnoringLease(input.coordRoot, input.goalId),
18
+ planId: input.planId,
19
+ actor,
20
+ reason: input.reason,
21
+ });
22
+ } finally {
23
+ release();
24
+ }
25
+ }
26
+
27
+ export function rejectSupervisorPlan(input: {
28
+ coordRoot: string;
29
+ goalId: string;
30
+ planId: string;
31
+ actor?: string;
32
+ reason: string;
33
+ }): SupervisorPlanOutcome {
34
+ const actor = input.actor?.trim() || `supervisor:${input.goalId}`;
35
+ const release = acquireSupervisorLease(input.coordRoot, input.goalId);
36
+ try {
37
+ return rejectSupervisorPlanProposal({
38
+ coordRoot: input.coordRoot,
39
+ goalId: input.goalId,
40
+ planId: input.planId,
41
+ actor,
42
+ reason: input.reason,
43
+ });
44
+ } finally {
45
+ release();
46
+ }
47
+ }