@os-eco/overstory-cli 0.8.7 → 0.9.2
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 +26 -8
- package/agents/coordinator.md +30 -6
- package/agents/lead.md +11 -1
- package/agents/ov-co-creation.md +90 -0
- package/package.json +1 -1
- package/src/agents/hooks-deployer.test.ts +9 -1
- package/src/agents/hooks-deployer.ts +2 -1
- package/src/agents/overlay.test.ts +26 -0
- package/src/agents/overlay.ts +31 -4
- package/src/canopy/client.test.ts +107 -0
- package/src/canopy/client.ts +179 -0
- package/src/commands/agents.ts +1 -1
- package/src/commands/clean.test.ts +3 -0
- package/src/commands/clean.ts +1 -58
- package/src/commands/completions.test.ts +18 -6
- package/src/commands/completions.ts +40 -1
- package/src/commands/coordinator.test.ts +77 -4
- package/src/commands/coordinator.ts +304 -146
- package/src/commands/dashboard.ts +47 -10
- package/src/commands/discover.test.ts +288 -0
- package/src/commands/discover.ts +202 -0
- package/src/commands/doctor.ts +3 -1
- package/src/commands/ecosystem.test.ts +126 -1
- package/src/commands/ecosystem.ts +7 -53
- package/src/commands/feed.test.ts +117 -2
- package/src/commands/feed.ts +46 -30
- package/src/commands/group.test.ts +274 -155
- package/src/commands/group.ts +11 -5
- package/src/commands/init.test.ts +2 -1
- package/src/commands/init.ts +8 -0
- package/src/commands/log.test.ts +35 -0
- package/src/commands/log.ts +10 -6
- package/src/commands/logs.test.ts +423 -1
- package/src/commands/logs.ts +99 -104
- package/src/commands/orchestrator.ts +42 -0
- package/src/commands/prime.test.ts +177 -2
- package/src/commands/prime.ts +4 -2
- package/src/commands/sling.ts +23 -3
- package/src/commands/update.test.ts +1 -0
- package/src/commands/upgrade.test.ts +2 -0
- package/src/commands/upgrade.ts +1 -17
- package/src/commands/watch.test.ts +67 -1
- package/src/commands/watch.ts +13 -88
- package/src/config.test.ts +250 -0
- package/src/config.ts +43 -0
- package/src/doctor/agents.test.ts +72 -5
- package/src/doctor/agents.ts +10 -10
- package/src/doctor/consistency.test.ts +35 -0
- package/src/doctor/consistency.ts +7 -3
- package/src/doctor/dependencies.test.ts +58 -1
- package/src/doctor/dependencies.ts +4 -2
- package/src/doctor/providers.test.ts +41 -5
- package/src/doctor/types.ts +2 -1
- package/src/doctor/version.test.ts +106 -2
- package/src/doctor/version.ts +4 -2
- package/src/doctor/watchdog.test.ts +167 -0
- package/src/doctor/watchdog.ts +158 -0
- package/src/e2e/init-sling-lifecycle.test.ts +4 -2
- package/src/errors.test.ts +350 -0
- package/src/events/tailer.test.ts +25 -0
- package/src/events/tailer.ts +8 -1
- package/src/index.ts +9 -1
- package/src/mail/store.test.ts +110 -0
- package/src/mail/store.ts +2 -1
- package/src/runtimes/aider.test.ts +124 -0
- package/src/runtimes/aider.ts +147 -0
- package/src/runtimes/amp.test.ts +164 -0
- package/src/runtimes/amp.ts +154 -0
- package/src/runtimes/claude.test.ts +4 -2
- package/src/runtimes/goose.test.ts +133 -0
- package/src/runtimes/goose.ts +157 -0
- package/src/runtimes/pi-guards.ts +2 -1
- package/src/runtimes/pi.test.ts +9 -9
- package/src/runtimes/pi.ts +6 -7
- package/src/runtimes/registry.test.ts +1 -1
- package/src/runtimes/registry.ts +13 -4
- package/src/runtimes/sapling.ts +2 -1
- package/src/runtimes/types.ts +2 -2
- package/src/schema-consistency.test.ts +1 -0
- package/src/sessions/store.ts +25 -4
- package/src/types.ts +65 -1
- package/src/utils/bin.test.ts +10 -0
- package/src/utils/bin.ts +37 -0
- package/src/utils/fs.test.ts +119 -0
- package/src/utils/fs.ts +62 -0
- package/src/utils/pid.test.ts +68 -0
- package/src/utils/pid.ts +45 -0
- package/src/utils/time.test.ts +43 -0
- package/src/utils/time.ts +37 -0
- package/src/utils/version.test.ts +33 -0
- package/src/utils/version.ts +70 -0
- package/src/watchdog/daemon.test.ts +255 -1
- package/src/watchdog/daemon.ts +87 -9
- package/src/watchdog/health.test.ts +15 -1
- package/src/watchdog/health.ts +1 -1
- package/src/watchdog/triage.test.ts +49 -9
- package/src/watchdog/triage.ts +21 -5
- package/templates/overlay.md.tmpl +2 -0
|
@@ -46,6 +46,24 @@ import { isRunningAsRoot } from "./sling.ts";
|
|
|
46
46
|
/** Default coordinator agent name. */
|
|
47
47
|
const COORDINATOR_NAME = "coordinator";
|
|
48
48
|
|
|
49
|
+
export interface PersistentAgentSpec {
|
|
50
|
+
commandName: string;
|
|
51
|
+
displayName: string;
|
|
52
|
+
agentName: string;
|
|
53
|
+
capability: string;
|
|
54
|
+
agentDefFile: string;
|
|
55
|
+
beaconBuilder: (trackerCli: string) => string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const COORDINATOR_SPEC: PersistentAgentSpec = {
|
|
59
|
+
commandName: "coordinator",
|
|
60
|
+
displayName: "Coordinator",
|
|
61
|
+
agentName: COORDINATOR_NAME,
|
|
62
|
+
capability: "coordinator",
|
|
63
|
+
agentDefFile: "coordinator.md",
|
|
64
|
+
beaconBuilder: buildCoordinatorBeacon,
|
|
65
|
+
};
|
|
66
|
+
|
|
49
67
|
/** Poll interval for the ask subcommand reply loop. */
|
|
50
68
|
const ASK_POLL_INTERVAL_MS = 2_000;
|
|
51
69
|
|
|
@@ -56,8 +74,8 @@ const ASK_DEFAULT_TIMEOUT_S = 120;
|
|
|
56
74
|
* Build the tmux session name for the coordinator.
|
|
57
75
|
* Includes the project name to prevent cross-project collisions (overstory-pcef).
|
|
58
76
|
*/
|
|
59
|
-
function coordinatorTmuxSession(projectName: string): string {
|
|
60
|
-
return `overstory-${projectName}-${
|
|
77
|
+
function coordinatorTmuxSession(projectName: string, name: string = COORDINATOR_NAME): string {
|
|
78
|
+
return `overstory-${projectName}-${name}`;
|
|
61
79
|
}
|
|
62
80
|
|
|
63
81
|
/** Dependency injection for testing. Uses real implementations when omitted. */
|
|
@@ -273,8 +291,8 @@ export function buildCoordinatorBeacon(cliName = "bd"): string {
|
|
|
273
291
|
const parts = [
|
|
274
292
|
`[OVERSTORY] ${COORDINATOR_NAME} (coordinator) ${timestamp}`,
|
|
275
293
|
"Depth: 0 | Parent: none | Role: persistent orchestrator",
|
|
276
|
-
"HIERARCHY:
|
|
277
|
-
"DELEGATION: For
|
|
294
|
+
"HIERARCHY: Default to leads (ov sling --capability lead). For low-budget or very narrow work, you may spawn scout/builder directly. NEVER spawn reviewer or merger directly.",
|
|
295
|
+
"DELEGATION: For substantial work streams, spawn a lead who will handle scouts/builders/reviewers. For tight agent budgets, compress roles by using direct scout/builder fallback or --dispatch-max-agents 1/2 on the lead.",
|
|
278
296
|
`Startup: run mulch prime, check mail (ov mail check --agent ${COORDINATOR_NAME}), check ${cliName} ready, check ov group status, then begin work`,
|
|
279
297
|
];
|
|
280
298
|
return parts.join(" — ");
|
|
@@ -290,8 +308,37 @@ export function resolveAttach(args: string[], isTTY: boolean): boolean {
|
|
|
290
308
|
return isTTY;
|
|
291
309
|
}
|
|
292
310
|
|
|
293
|
-
|
|
294
|
-
|
|
311
|
+
/**
|
|
312
|
+
* Options for the reusable coordinator session startup core.
|
|
313
|
+
* Used by startCoordinatorSession() and consumed by commands like ov discover.
|
|
314
|
+
*/
|
|
315
|
+
export interface CoordinatorSessionOptions {
|
|
316
|
+
json: boolean;
|
|
317
|
+
attach: boolean;
|
|
318
|
+
watchdog: boolean;
|
|
319
|
+
monitor: boolean;
|
|
320
|
+
profile?: string;
|
|
321
|
+
/** Override coordinator name (default: "coordinator"). */
|
|
322
|
+
coordinatorName?: string;
|
|
323
|
+
/** Generic persistent agent name override. Preferred over coordinatorName for new callers. */
|
|
324
|
+
agentName?: string;
|
|
325
|
+
/** Capability stored in the session registry and used for manifest/runtime resolution. */
|
|
326
|
+
capability?: string;
|
|
327
|
+
/** Agent definition file to append as the system prompt. */
|
|
328
|
+
agentDefFile?: string;
|
|
329
|
+
/** Human-readable label for output. */
|
|
330
|
+
displayName?: string;
|
|
331
|
+
/** Custom beacon builder. Receives tracker CLI name, returns beacon string. */
|
|
332
|
+
beaconBuilder?: (trackerCli: string) => string;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Core coordinator session startup logic. Reusable by commands that need to
|
|
337
|
+
* start a coordinator-like session with a custom name or beacon
|
|
338
|
+
* (e.g., ov discover uses coordinatorName: "discover-coordinator").
|
|
339
|
+
*/
|
|
340
|
+
export async function startCoordinatorSession(
|
|
341
|
+
opts: CoordinatorSessionOptions,
|
|
295
342
|
deps: CoordinatorDeps = {},
|
|
296
343
|
): Promise<void> {
|
|
297
344
|
const tmux = deps._tmux ?? {
|
|
@@ -304,7 +351,25 @@ async function startCoordinator(
|
|
|
304
351
|
ensureTmuxAvailable,
|
|
305
352
|
};
|
|
306
353
|
|
|
307
|
-
const {
|
|
354
|
+
const {
|
|
355
|
+
json,
|
|
356
|
+
attach: shouldAttach,
|
|
357
|
+
watchdog: watchdogFlag,
|
|
358
|
+
monitor: monitorFlag,
|
|
359
|
+
profile: profileFlag,
|
|
360
|
+
coordinatorName: coordinatorNameOpt,
|
|
361
|
+
agentName: agentNameOpt,
|
|
362
|
+
capability: capabilityOpt,
|
|
363
|
+
agentDefFile: agentDefFileOpt,
|
|
364
|
+
displayName: displayNameOpt,
|
|
365
|
+
beaconBuilder: beaconBuilderOpt,
|
|
366
|
+
} = opts;
|
|
367
|
+
|
|
368
|
+
const coordinatorName = agentNameOpt ?? coordinatorNameOpt ?? COORDINATOR_NAME;
|
|
369
|
+
const capability = capabilityOpt ?? COORDINATOR_SPEC.capability;
|
|
370
|
+
const agentDefFile = agentDefFileOpt ?? COORDINATOR_SPEC.agentDefFile;
|
|
371
|
+
const displayName = displayNameOpt ?? COORDINATOR_SPEC.displayName;
|
|
372
|
+
const beaconBuilder = beaconBuilderOpt ?? buildCoordinatorBeacon;
|
|
308
373
|
|
|
309
374
|
if (isRunningAsRoot()) {
|
|
310
375
|
throw new AgentError(
|
|
@@ -317,17 +382,17 @@ async function startCoordinator(
|
|
|
317
382
|
const projectRoot = config.project.root;
|
|
318
383
|
const watchdog = deps._watchdog ?? createDefaultWatchdog(projectRoot);
|
|
319
384
|
const monitor = deps._monitor ?? createDefaultMonitor(projectRoot);
|
|
320
|
-
const tmuxSession = coordinatorTmuxSession(config.project.name);
|
|
385
|
+
const tmuxSession = coordinatorTmuxSession(config.project.name, coordinatorName);
|
|
321
386
|
|
|
322
|
-
// Check for existing coordinator
|
|
387
|
+
// Check for existing coordinator session with the same name
|
|
323
388
|
const overstoryDir = join(projectRoot, ".overstory");
|
|
324
389
|
const { store } = openSessionStore(overstoryDir);
|
|
325
390
|
try {
|
|
326
|
-
const existing = store.getByName(
|
|
391
|
+
const existing = store.getByName(coordinatorName);
|
|
327
392
|
|
|
328
393
|
if (
|
|
329
394
|
existing &&
|
|
330
|
-
existing.capability ===
|
|
395
|
+
existing.capability === capability &&
|
|
331
396
|
existing.state !== "completed" &&
|
|
332
397
|
existing.state !== "zombie"
|
|
333
398
|
) {
|
|
@@ -340,19 +405,19 @@ async function startCoordinator(
|
|
|
340
405
|
// Zombie: tmux pane exists but agent process has exited.
|
|
341
406
|
// Kill the empty session and reclaim the slot.
|
|
342
407
|
await tmux.killSession(existing.tmuxSession);
|
|
343
|
-
store.updateState(
|
|
408
|
+
store.updateState(coordinatorName, "completed");
|
|
344
409
|
} else {
|
|
345
410
|
// Either the process is genuinely running (pid alive), or pid is null
|
|
346
411
|
// (e.g. sessions migrated from an older schema). In both cases we
|
|
347
412
|
// cannot prove the session is a zombie, so treat it as active.
|
|
348
413
|
throw new AgentError(
|
|
349
|
-
|
|
350
|
-
{ agentName:
|
|
414
|
+
`${displayName} is already running (tmux: ${existing.tmuxSession}, since: ${existing.startedAt})`,
|
|
415
|
+
{ agentName: coordinatorName },
|
|
351
416
|
);
|
|
352
417
|
}
|
|
353
418
|
} else {
|
|
354
419
|
// Session is dead or tmux server is not running -- clean up stale DB entry.
|
|
355
|
-
store.updateState(
|
|
420
|
+
store.updateState(coordinatorName, "completed");
|
|
356
421
|
}
|
|
357
422
|
}
|
|
358
423
|
|
|
@@ -362,8 +427,8 @@ async function startCoordinator(
|
|
|
362
427
|
join(projectRoot, config.agents.baseDir),
|
|
363
428
|
);
|
|
364
429
|
const manifest = await manifestLoader.load();
|
|
365
|
-
const resolvedModel = resolveModel(config, manifest,
|
|
366
|
-
const runtime = getRuntime(undefined, config,
|
|
430
|
+
const resolvedModel = resolveModel(config, manifest, capability, "opus");
|
|
431
|
+
const runtime = getRuntime(undefined, config, capability);
|
|
367
432
|
|
|
368
433
|
// Deploy hooks to the project root so the coordinator gets event logging,
|
|
369
434
|
// mail check --inject, and activity tracking via the standard hook pipeline.
|
|
@@ -372,19 +437,19 @@ async function startCoordinator(
|
|
|
372
437
|
// the coordinator's tmux session), so the user's own Claude Code session
|
|
373
438
|
// at the project root is unaffected.
|
|
374
439
|
await runtime.deployConfig(projectRoot, undefined, {
|
|
375
|
-
agentName:
|
|
376
|
-
capability
|
|
440
|
+
agentName: coordinatorName,
|
|
441
|
+
capability,
|
|
377
442
|
worktreePath: projectRoot,
|
|
378
443
|
});
|
|
379
444
|
|
|
380
445
|
// Create coordinator identity if first run
|
|
381
446
|
const identityBaseDir = join(projectRoot, ".overstory", "agents");
|
|
382
447
|
await mkdir(identityBaseDir, { recursive: true });
|
|
383
|
-
const existingIdentity = await loadIdentity(identityBaseDir,
|
|
448
|
+
const existingIdentity = await loadIdentity(identityBaseDir, coordinatorName);
|
|
384
449
|
if (!existingIdentity) {
|
|
385
450
|
await createIdentity(identityBaseDir, {
|
|
386
|
-
name:
|
|
387
|
-
capability
|
|
451
|
+
name: coordinatorName,
|
|
452
|
+
capability,
|
|
388
453
|
created: new Date().toISOString(),
|
|
389
454
|
sessionsCompleted: 0,
|
|
390
455
|
expertiseDomains: config.mulch.enabled ? config.mulch.domains : [],
|
|
@@ -403,10 +468,10 @@ async function startCoordinator(
|
|
|
403
468
|
// Pass the file path (not content) so the shell inside the tmux pane reads
|
|
404
469
|
// it via $(cat ...) — avoids tmux IPC "command too long" errors with large
|
|
405
470
|
// agent definitions (overstory#45).
|
|
406
|
-
const agentDefPath = join(projectRoot, ".overstory", "agent-defs",
|
|
407
|
-
const
|
|
471
|
+
const agentDefPath = join(projectRoot, ".overstory", "agent-defs", agentDefFile);
|
|
472
|
+
const agentDefHandle = Bun.file(agentDefPath);
|
|
408
473
|
let appendSystemPromptFile: string | undefined;
|
|
409
|
-
if (await
|
|
474
|
+
if (await agentDefHandle.exists()) {
|
|
410
475
|
appendSystemPromptFile = agentDefPath;
|
|
411
476
|
}
|
|
412
477
|
const spawnCmd = runtime.buildSpawnCommand({
|
|
@@ -416,17 +481,19 @@ async function startCoordinator(
|
|
|
416
481
|
appendSystemPromptFile,
|
|
417
482
|
env: {
|
|
418
483
|
...runtime.buildEnv(resolvedModel),
|
|
419
|
-
OVERSTORY_AGENT_NAME:
|
|
484
|
+
OVERSTORY_AGENT_NAME: coordinatorName,
|
|
485
|
+
...(profileFlag ? { OVERSTORY_PROFILE: profileFlag } : {}),
|
|
420
486
|
},
|
|
421
487
|
});
|
|
422
488
|
const pid = await tmux.createSession(tmuxSession, projectRoot, spawnCmd, {
|
|
423
489
|
...runtime.buildEnv(resolvedModel),
|
|
424
|
-
OVERSTORY_AGENT_NAME:
|
|
490
|
+
OVERSTORY_AGENT_NAME: coordinatorName,
|
|
491
|
+
...(profileFlag ? { OVERSTORY_PROFILE: profileFlag } : {}),
|
|
425
492
|
});
|
|
426
493
|
|
|
427
494
|
// Create a run for this coordinator session BEFORE recording the session,
|
|
428
495
|
// so the session can reference the run ID from the start.
|
|
429
|
-
const sessionId = `session-${Date.now()}-${
|
|
496
|
+
const sessionId = `session-${Date.now()}-${coordinatorName}`;
|
|
430
497
|
const runId = `run-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
|
431
498
|
const runStore = createRunStore(join(overstoryDir, "sessions.db"));
|
|
432
499
|
try {
|
|
@@ -434,7 +501,7 @@ async function startCoordinator(
|
|
|
434
501
|
id: runId,
|
|
435
502
|
startedAt: new Date().toISOString(),
|
|
436
503
|
coordinatorSessionId: sessionId,
|
|
437
|
-
coordinatorName
|
|
504
|
+
coordinatorName,
|
|
438
505
|
status: "active",
|
|
439
506
|
});
|
|
440
507
|
} finally {
|
|
@@ -449,8 +516,8 @@ async function startCoordinator(
|
|
|
449
516
|
// leaving the coordinator stuck in "booting" (overstory-036f).
|
|
450
517
|
const session: AgentSession = {
|
|
451
518
|
id: sessionId,
|
|
452
|
-
agentName:
|
|
453
|
-
capability
|
|
519
|
+
agentName: coordinatorName,
|
|
520
|
+
capability,
|
|
454
521
|
worktreePath: projectRoot, // Coordinator uses project root, not a worktree
|
|
455
522
|
branchName: config.project.canonicalBranch, // Operates on canonical branch
|
|
456
523
|
taskId: "", // No specific task assignment
|
|
@@ -484,29 +551,29 @@ async function startCoordinator(
|
|
|
484
551
|
const alive = await tmux.isSessionAlive(tmuxSession);
|
|
485
552
|
if (!alive) {
|
|
486
553
|
// Clean up the stale session record
|
|
487
|
-
store.updateState(
|
|
554
|
+
store.updateState(coordinatorName, "completed");
|
|
488
555
|
const sessionState = await tmux.checkSessionState(tmuxSession);
|
|
489
556
|
const detail =
|
|
490
557
|
sessionState === "no_server"
|
|
491
558
|
? "The tmux server is no longer running. It may have crashed or been killed externally."
|
|
492
559
|
: "The Claude Code process may have crashed or exited immediately. Check tmux logs or try running the claude command manually.";
|
|
493
560
|
throw new AgentError(
|
|
494
|
-
|
|
495
|
-
{ agentName:
|
|
561
|
+
`${displayName} tmux session "${tmuxSession}" died during startup. ${detail}`,
|
|
562
|
+
{ agentName: coordinatorName },
|
|
496
563
|
);
|
|
497
564
|
}
|
|
498
565
|
await tmux.killSession(tmuxSession);
|
|
499
|
-
store.updateState(
|
|
566
|
+
store.updateState(coordinatorName, "completed");
|
|
500
567
|
throw new AgentError(
|
|
501
|
-
|
|
502
|
-
{ agentName:
|
|
568
|
+
`${displayName} tmux session "${tmuxSession}" did not become ready during startup. Claude Code may still be waiting on an interactive dialog or initializing too slowly.`,
|
|
569
|
+
{ agentName: coordinatorName },
|
|
503
570
|
);
|
|
504
571
|
}
|
|
505
572
|
await Bun.sleep(1_000);
|
|
506
573
|
|
|
507
574
|
const resolvedBackend = await resolveBackend(config.taskTracker.backend, config.project.root);
|
|
508
575
|
const trackerCli = trackerCliName(resolvedBackend);
|
|
509
|
-
const beacon =
|
|
576
|
+
const beacon = beaconBuilder(trackerCli);
|
|
510
577
|
await tmux.sendKeys(tmuxSession, beacon);
|
|
511
578
|
|
|
512
579
|
// Follow-up Enters with increasing delays to ensure submission
|
|
@@ -544,8 +611,8 @@ async function startCoordinator(
|
|
|
544
611
|
}
|
|
545
612
|
|
|
546
613
|
const output = {
|
|
547
|
-
agentName:
|
|
548
|
-
capability
|
|
614
|
+
agentName: coordinatorName,
|
|
615
|
+
capability,
|
|
549
616
|
tmuxSession,
|
|
550
617
|
projectRoot,
|
|
551
618
|
pid,
|
|
@@ -554,9 +621,9 @@ async function startCoordinator(
|
|
|
554
621
|
};
|
|
555
622
|
|
|
556
623
|
if (json) {
|
|
557
|
-
jsonOutput(
|
|
624
|
+
jsonOutput(`${capability} start`, output);
|
|
558
625
|
} else {
|
|
559
|
-
printSuccess(
|
|
626
|
+
printSuccess(`${displayName} started`);
|
|
560
627
|
process.stdout.write(` Tmux: ${tmuxSession}\n`);
|
|
561
628
|
process.stdout.write(` Root: ${projectRoot}\n`);
|
|
562
629
|
process.stdout.write(` PID: ${pid}\n`);
|
|
@@ -572,6 +639,36 @@ async function startCoordinator(
|
|
|
572
639
|
}
|
|
573
640
|
}
|
|
574
641
|
|
|
642
|
+
async function startPersistentAgent(
|
|
643
|
+
spec: PersistentAgentSpec,
|
|
644
|
+
opts: { json: boolean; attach: boolean; watchdog: boolean; monitor: boolean; profile?: string },
|
|
645
|
+
deps: CoordinatorDeps = {},
|
|
646
|
+
): Promise<void> {
|
|
647
|
+
await startCoordinatorSession(
|
|
648
|
+
{
|
|
649
|
+
...opts,
|
|
650
|
+
agentName: spec.agentName,
|
|
651
|
+
capability: spec.capability,
|
|
652
|
+
agentDefFile: spec.agentDefFile,
|
|
653
|
+
displayName: spec.displayName,
|
|
654
|
+
beaconBuilder: spec.beaconBuilder,
|
|
655
|
+
},
|
|
656
|
+
deps,
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function isActivePersistentAgentSession(
|
|
661
|
+
session: AgentSession | null,
|
|
662
|
+
spec: PersistentAgentSpec,
|
|
663
|
+
): session is AgentSession {
|
|
664
|
+
return (
|
|
665
|
+
session !== null &&
|
|
666
|
+
session.capability === spec.capability &&
|
|
667
|
+
session.state !== "completed" &&
|
|
668
|
+
session.state !== "zombie"
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
|
|
575
672
|
/**
|
|
576
673
|
* Stop the coordinator agent.
|
|
577
674
|
*
|
|
@@ -580,7 +677,11 @@ async function startCoordinator(
|
|
|
580
677
|
* 3. Mark session as completed in SessionStore
|
|
581
678
|
* 4. Auto-complete the active run (if current-run.txt exists)
|
|
582
679
|
*/
|
|
583
|
-
async function
|
|
680
|
+
async function stopPersistentAgent(
|
|
681
|
+
spec: PersistentAgentSpec,
|
|
682
|
+
opts: { json: boolean },
|
|
683
|
+
deps: CoordinatorDeps = {},
|
|
684
|
+
): Promise<void> {
|
|
584
685
|
const tmux = deps._tmux ?? {
|
|
585
686
|
createSession,
|
|
586
687
|
isSessionAlive,
|
|
@@ -601,16 +702,11 @@ async function stopCoordinator(opts: { json: boolean }, deps: CoordinatorDeps =
|
|
|
601
702
|
const overstoryDir = join(projectRoot, ".overstory");
|
|
602
703
|
const { store } = openSessionStore(overstoryDir);
|
|
603
704
|
try {
|
|
604
|
-
const session = store.getByName(
|
|
705
|
+
const session = store.getByName(spec.agentName);
|
|
605
706
|
|
|
606
|
-
if (
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
session.state === "completed" ||
|
|
610
|
-
session.state === "zombie"
|
|
611
|
-
) {
|
|
612
|
-
throw new AgentError("No active coordinator session found", {
|
|
613
|
-
agentName: COORDINATOR_NAME,
|
|
707
|
+
if (!isActivePersistentAgentSession(session, spec)) {
|
|
708
|
+
throw new AgentError(`No active ${spec.commandName} session found`, {
|
|
709
|
+
agentName: spec.agentName,
|
|
614
710
|
});
|
|
615
711
|
}
|
|
616
712
|
|
|
@@ -627,8 +723,8 @@ async function stopCoordinator(opts: { json: boolean }, deps: CoordinatorDeps =
|
|
|
627
723
|
const monitorStopped = await monitor.stop();
|
|
628
724
|
|
|
629
725
|
// Update session state
|
|
630
|
-
store.updateState(
|
|
631
|
-
store.updateLastActivity(
|
|
726
|
+
store.updateState(spec.agentName, "completed");
|
|
727
|
+
store.updateLastActivity(spec.agentName);
|
|
632
728
|
|
|
633
729
|
// Auto-complete the current run
|
|
634
730
|
let runCompleted = false;
|
|
@@ -657,7 +753,7 @@ async function stopCoordinator(opts: { json: boolean }, deps: CoordinatorDeps =
|
|
|
657
753
|
}
|
|
658
754
|
|
|
659
755
|
if (json) {
|
|
660
|
-
jsonOutput(
|
|
756
|
+
jsonOutput(`${spec.commandName} stop`, {
|
|
661
757
|
stopped: true,
|
|
662
758
|
sessionId: session.id,
|
|
663
759
|
watchdogStopped,
|
|
@@ -665,7 +761,7 @@ async function stopCoordinator(opts: { json: boolean }, deps: CoordinatorDeps =
|
|
|
665
761
|
runCompleted,
|
|
666
762
|
});
|
|
667
763
|
} else {
|
|
668
|
-
printSuccess(
|
|
764
|
+
printSuccess(`${spec.displayName} stopped`, session.id);
|
|
669
765
|
if (watchdogStopped) {
|
|
670
766
|
printHint("Watchdog stopped");
|
|
671
767
|
} else {
|
|
@@ -692,7 +788,8 @@ async function stopCoordinator(opts: { json: boolean }, deps: CoordinatorDeps =
|
|
|
692
788
|
*
|
|
693
789
|
* Checks session registry and tmux liveness to report actual state.
|
|
694
790
|
*/
|
|
695
|
-
async function
|
|
791
|
+
async function statusPersistentAgent(
|
|
792
|
+
spec: PersistentAgentSpec,
|
|
696
793
|
opts: { json: boolean },
|
|
697
794
|
deps: CoordinatorDeps = {},
|
|
698
795
|
): Promise<void> {
|
|
@@ -716,20 +813,19 @@ async function statusCoordinator(
|
|
|
716
813
|
const overstoryDir = join(projectRoot, ".overstory");
|
|
717
814
|
const { store } = openSessionStore(overstoryDir);
|
|
718
815
|
try {
|
|
719
|
-
const session = store.getByName(
|
|
816
|
+
const session = store.getByName(spec.agentName);
|
|
720
817
|
const watchdogRunning = await watchdog.isRunning();
|
|
721
818
|
const monitorRunning = await monitor.isRunning();
|
|
722
819
|
|
|
723
|
-
if (
|
|
724
|
-
!session ||
|
|
725
|
-
session.capability !== "coordinator" ||
|
|
726
|
-
session.state === "completed" ||
|
|
727
|
-
session.state === "zombie"
|
|
728
|
-
) {
|
|
820
|
+
if (!isActivePersistentAgentSession(session, spec)) {
|
|
729
821
|
if (json) {
|
|
730
|
-
jsonOutput(
|
|
822
|
+
jsonOutput(`${spec.commandName} status`, {
|
|
823
|
+
running: false,
|
|
824
|
+
watchdogRunning,
|
|
825
|
+
monitorRunning,
|
|
826
|
+
});
|
|
731
827
|
} else {
|
|
732
|
-
printHint(
|
|
828
|
+
printHint(`${spec.displayName} is not running`);
|
|
733
829
|
if (watchdogRunning) {
|
|
734
830
|
printHint("Watchdog: running");
|
|
735
831
|
}
|
|
@@ -746,8 +842,8 @@ async function statusCoordinator(
|
|
|
746
842
|
// We already filtered out completed/zombie states above, so if tmux is dead
|
|
747
843
|
// this session needs to be marked as zombie.
|
|
748
844
|
if (!alive) {
|
|
749
|
-
store.updateState(
|
|
750
|
-
store.updateLastActivity(
|
|
845
|
+
store.updateState(spec.agentName, "zombie");
|
|
846
|
+
store.updateLastActivity(spec.agentName);
|
|
751
847
|
session.state = "zombie";
|
|
752
848
|
}
|
|
753
849
|
|
|
@@ -764,10 +860,10 @@ async function statusCoordinator(
|
|
|
764
860
|
};
|
|
765
861
|
|
|
766
862
|
if (json) {
|
|
767
|
-
jsonOutput(
|
|
863
|
+
jsonOutput(`${spec.commandName} status`, status);
|
|
768
864
|
} else {
|
|
769
865
|
const stateLabel = alive ? "running" : session.state;
|
|
770
|
-
process.stdout.write(
|
|
866
|
+
process.stdout.write(`${spec.displayName}: ${stateLabel}\n`);
|
|
771
867
|
process.stdout.write(` Session: ${session.id}\n`);
|
|
772
868
|
process.stdout.write(` Tmux: ${session.tmuxSession}\n`);
|
|
773
869
|
process.stdout.write(` PID: ${session.pid}\n`);
|
|
@@ -787,7 +883,8 @@ async function statusCoordinator(
|
|
|
787
883
|
* Sends a mail message (from: operator, type: dispatch) and auto-nudges the
|
|
788
884
|
* coordinator via tmux sendKeys. Replaces the two-step `ov mail send + ov nudge` pattern.
|
|
789
885
|
*/
|
|
790
|
-
async function
|
|
886
|
+
async function sendToPersistentAgent(
|
|
887
|
+
spec: PersistentAgentSpec,
|
|
791
888
|
body: string,
|
|
792
889
|
opts: { subject: string; json: boolean },
|
|
793
890
|
deps: CoordinatorDeps = {},
|
|
@@ -811,26 +908,24 @@ async function sendToCoordinator(
|
|
|
811
908
|
const overstoryDir = join(projectRoot, ".overstory");
|
|
812
909
|
const { store } = openSessionStore(overstoryDir);
|
|
813
910
|
try {
|
|
814
|
-
const session = store.getByName(
|
|
911
|
+
const session = store.getByName(spec.agentName);
|
|
815
912
|
|
|
816
|
-
if (
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
session.state === "completed" ||
|
|
820
|
-
session.state === "zombie"
|
|
821
|
-
) {
|
|
822
|
-
throw new AgentError("No active coordinator session found", {
|
|
823
|
-
agentName: COORDINATOR_NAME,
|
|
913
|
+
if (!isActivePersistentAgentSession(session, spec)) {
|
|
914
|
+
throw new AgentError(`No active ${spec.commandName} session found`, {
|
|
915
|
+
agentName: spec.agentName,
|
|
824
916
|
});
|
|
825
917
|
}
|
|
826
918
|
|
|
827
919
|
const alive = await tmux.isSessionAlive(session.tmuxSession);
|
|
828
920
|
if (!alive) {
|
|
829
|
-
store.updateState(
|
|
830
|
-
store.updateLastActivity(
|
|
831
|
-
throw new AgentError(
|
|
832
|
-
|
|
833
|
-
|
|
921
|
+
store.updateState(spec.agentName, "zombie");
|
|
922
|
+
store.updateLastActivity(spec.agentName);
|
|
923
|
+
throw new AgentError(
|
|
924
|
+
`${spec.displayName} tmux session "${session.tmuxSession}" is not alive`,
|
|
925
|
+
{
|
|
926
|
+
agentName: spec.agentName,
|
|
927
|
+
},
|
|
928
|
+
);
|
|
834
929
|
}
|
|
835
930
|
|
|
836
931
|
// Send mail
|
|
@@ -841,7 +936,7 @@ async function sendToCoordinator(
|
|
|
841
936
|
try {
|
|
842
937
|
id = mailClient.send({
|
|
843
938
|
from: "operator",
|
|
844
|
-
to:
|
|
939
|
+
to: spec.agentName,
|
|
845
940
|
subject,
|
|
846
941
|
body,
|
|
847
942
|
type: "dispatch",
|
|
@@ -855,16 +950,16 @@ async function sendToCoordinator(
|
|
|
855
950
|
const nudgeMessage = `[DISPATCH] ${subject}: ${body.slice(0, 500)}`;
|
|
856
951
|
let nudged = false;
|
|
857
952
|
try {
|
|
858
|
-
const nudgeResult = await nudge(projectRoot,
|
|
953
|
+
const nudgeResult = await nudge(projectRoot, spec.agentName, nudgeMessage, true);
|
|
859
954
|
nudged = nudgeResult.delivered;
|
|
860
955
|
} catch {
|
|
861
956
|
// Nudge is fire-and-forget — silently ignore errors
|
|
862
957
|
}
|
|
863
958
|
|
|
864
959
|
if (json) {
|
|
865
|
-
jsonOutput(
|
|
960
|
+
jsonOutput(`${spec.commandName} send`, { id, nudged });
|
|
866
961
|
} else {
|
|
867
|
-
printSuccess(
|
|
962
|
+
printSuccess(`Sent to ${spec.commandName}`, id);
|
|
868
963
|
}
|
|
869
964
|
} finally {
|
|
870
965
|
store.close();
|
|
@@ -883,6 +978,15 @@ export async function askCoordinator(
|
|
|
883
978
|
body: string,
|
|
884
979
|
opts: { subject: string; timeout: number; json: boolean },
|
|
885
980
|
deps: CoordinatorDeps = {},
|
|
981
|
+
): Promise<void> {
|
|
982
|
+
await askPersistentAgent(COORDINATOR_SPEC, body, opts, deps);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
export async function askPersistentAgent(
|
|
986
|
+
spec: PersistentAgentSpec,
|
|
987
|
+
body: string,
|
|
988
|
+
opts: { subject: string; timeout: number; json: boolean },
|
|
989
|
+
deps: CoordinatorDeps = {},
|
|
886
990
|
): Promise<void> {
|
|
887
991
|
const tmux = deps._tmux ?? {
|
|
888
992
|
createSession,
|
|
@@ -904,26 +1008,24 @@ export async function askCoordinator(
|
|
|
904
1008
|
const overstoryDir = join(projectRoot, ".overstory");
|
|
905
1009
|
const { store } = openSessionStore(overstoryDir);
|
|
906
1010
|
try {
|
|
907
|
-
const session = store.getByName(
|
|
1011
|
+
const session = store.getByName(spec.agentName);
|
|
908
1012
|
|
|
909
|
-
if (
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
session.state === "completed" ||
|
|
913
|
-
session.state === "zombie"
|
|
914
|
-
) {
|
|
915
|
-
throw new AgentError("No active coordinator session found", {
|
|
916
|
-
agentName: COORDINATOR_NAME,
|
|
1013
|
+
if (!isActivePersistentAgentSession(session, spec)) {
|
|
1014
|
+
throw new AgentError(`No active ${spec.commandName} session found`, {
|
|
1015
|
+
agentName: spec.agentName,
|
|
917
1016
|
});
|
|
918
1017
|
}
|
|
919
1018
|
|
|
920
1019
|
const alive = await tmux.isSessionAlive(session.tmuxSession);
|
|
921
1020
|
if (!alive) {
|
|
922
|
-
store.updateState(
|
|
923
|
-
store.updateLastActivity(
|
|
924
|
-
throw new AgentError(
|
|
925
|
-
|
|
926
|
-
|
|
1021
|
+
store.updateState(spec.agentName, "zombie");
|
|
1022
|
+
store.updateLastActivity(spec.agentName);
|
|
1023
|
+
throw new AgentError(
|
|
1024
|
+
`${spec.displayName} tmux session "${session.tmuxSession}" is not alive`,
|
|
1025
|
+
{
|
|
1026
|
+
agentName: spec.agentName,
|
|
1027
|
+
},
|
|
1028
|
+
);
|
|
927
1029
|
}
|
|
928
1030
|
|
|
929
1031
|
// Generate correlation ID for tracking this request/response pair
|
|
@@ -937,7 +1039,7 @@ export async function askCoordinator(
|
|
|
937
1039
|
try {
|
|
938
1040
|
sentId = mailClient.send({
|
|
939
1041
|
from: "operator",
|
|
940
|
-
to:
|
|
1042
|
+
to: spec.agentName,
|
|
941
1043
|
subject,
|
|
942
1044
|
body,
|
|
943
1045
|
type: "dispatch",
|
|
@@ -951,7 +1053,7 @@ export async function askCoordinator(
|
|
|
951
1053
|
// Auto-nudge (fire-and-forget)
|
|
952
1054
|
const nudgeMessage = `[ASK] ${subject}: ${body.slice(0, 500)}`;
|
|
953
1055
|
try {
|
|
954
|
-
await nudge(projectRoot,
|
|
1056
|
+
await nudge(projectRoot, spec.agentName, nudgeMessage, true);
|
|
955
1057
|
} catch {
|
|
956
1058
|
// Nudge is fire-and-forget — silently ignore errors
|
|
957
1059
|
}
|
|
@@ -965,13 +1067,13 @@ export async function askCoordinator(
|
|
|
965
1067
|
let reply: import("../types.ts").MailMessage | undefined;
|
|
966
1068
|
try {
|
|
967
1069
|
const replies = pollStore.getByThread(sentId);
|
|
968
|
-
reply = replies.find((m) => m.from ===
|
|
1070
|
+
reply = replies.find((m) => m.from === spec.agentName && m.to === "operator");
|
|
969
1071
|
} finally {
|
|
970
1072
|
pollStore.close();
|
|
971
1073
|
}
|
|
972
1074
|
if (reply) {
|
|
973
1075
|
if (json) {
|
|
974
|
-
jsonOutput(
|
|
1076
|
+
jsonOutput(`${spec.commandName} ask`, {
|
|
975
1077
|
correlationId,
|
|
976
1078
|
sentId,
|
|
977
1079
|
replyId: reply.id,
|
|
@@ -987,8 +1089,8 @@ export async function askCoordinator(
|
|
|
987
1089
|
}
|
|
988
1090
|
|
|
989
1091
|
throw new AgentError(
|
|
990
|
-
`Timed out after ${timeout}s waiting for
|
|
991
|
-
{ agentName:
|
|
1092
|
+
`Timed out after ${timeout}s waiting for ${spec.commandName} reply (correlationId: ${correlationId})`,
|
|
1093
|
+
{ agentName: spec.agentName },
|
|
992
1094
|
);
|
|
993
1095
|
} finally {
|
|
994
1096
|
store.close();
|
|
@@ -1000,7 +1102,8 @@ export async function askCoordinator(
|
|
|
1000
1102
|
*
|
|
1001
1103
|
* Wraps capturePaneContent() from tmux.ts. Supports --follow for continuous polling.
|
|
1002
1104
|
*/
|
|
1003
|
-
async function
|
|
1105
|
+
async function outputPersistentAgent(
|
|
1106
|
+
spec: PersistentAgentSpec,
|
|
1004
1107
|
opts: { follow: boolean; lines: number; interval: number; json: boolean },
|
|
1005
1108
|
deps: CoordinatorDeps = {},
|
|
1006
1109
|
): Promise<void> {
|
|
@@ -1023,26 +1126,24 @@ async function outputCoordinator(
|
|
|
1023
1126
|
const overstoryDir = join(projectRoot, ".overstory");
|
|
1024
1127
|
const { store } = openSessionStore(overstoryDir);
|
|
1025
1128
|
try {
|
|
1026
|
-
const session = store.getByName(
|
|
1129
|
+
const session = store.getByName(spec.agentName);
|
|
1027
1130
|
|
|
1028
|
-
if (
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
session.state === "completed" ||
|
|
1032
|
-
session.state === "zombie"
|
|
1033
|
-
) {
|
|
1034
|
-
throw new AgentError("No active coordinator session found", {
|
|
1035
|
-
agentName: COORDINATOR_NAME,
|
|
1131
|
+
if (!isActivePersistentAgentSession(session, spec)) {
|
|
1132
|
+
throw new AgentError(`No active ${spec.commandName} session found`, {
|
|
1133
|
+
agentName: spec.agentName,
|
|
1036
1134
|
});
|
|
1037
1135
|
}
|
|
1038
1136
|
|
|
1039
1137
|
const alive = await tmux.isSessionAlive(session.tmuxSession);
|
|
1040
1138
|
if (!alive) {
|
|
1041
|
-
store.updateState(
|
|
1042
|
-
store.updateLastActivity(
|
|
1043
|
-
throw new AgentError(
|
|
1044
|
-
|
|
1045
|
-
|
|
1139
|
+
store.updateState(spec.agentName, "zombie");
|
|
1140
|
+
store.updateLastActivity(spec.agentName);
|
|
1141
|
+
throw new AgentError(
|
|
1142
|
+
`${spec.displayName} tmux session "${session.tmuxSession}" is not alive`,
|
|
1143
|
+
{
|
|
1144
|
+
agentName: spec.agentName,
|
|
1145
|
+
},
|
|
1146
|
+
);
|
|
1046
1147
|
}
|
|
1047
1148
|
|
|
1048
1149
|
const tmuxSession = session.tmuxSession;
|
|
@@ -1057,7 +1158,7 @@ async function outputCoordinator(
|
|
|
1057
1158
|
while (running) {
|
|
1058
1159
|
const content = await capturePane(tmuxSession, lines);
|
|
1059
1160
|
if (json) {
|
|
1060
|
-
jsonOutput(
|
|
1161
|
+
jsonOutput(`${spec.commandName} output`, { content, lines });
|
|
1061
1162
|
} else {
|
|
1062
1163
|
process.stdout.write(content ?? "");
|
|
1063
1164
|
}
|
|
@@ -1068,7 +1169,7 @@ async function outputCoordinator(
|
|
|
1068
1169
|
} else {
|
|
1069
1170
|
const content = await capturePane(tmuxSession, lines);
|
|
1070
1171
|
if (json) {
|
|
1071
|
-
jsonOutput(
|
|
1172
|
+
jsonOutput(`${spec.commandName} output`, { content, lines });
|
|
1072
1173
|
} else {
|
|
1073
1174
|
process.stdout.write(content ?? "");
|
|
1074
1175
|
}
|
|
@@ -1242,30 +1343,41 @@ export async function checkComplete(
|
|
|
1242
1343
|
return result;
|
|
1243
1344
|
}
|
|
1244
1345
|
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
const cmd = new Command(
|
|
1346
|
+
export function createPersistentAgentCommand(
|
|
1347
|
+
spec: PersistentAgentSpec,
|
|
1348
|
+
deps: CoordinatorDeps = {},
|
|
1349
|
+
): Command {
|
|
1350
|
+
const cmd = new Command(spec.commandName).description(
|
|
1351
|
+
`Manage the persistent ${spec.commandName} agent`,
|
|
1352
|
+
);
|
|
1250
1353
|
|
|
1251
1354
|
cmd
|
|
1252
1355
|
.command("start")
|
|
1253
|
-
.description(
|
|
1356
|
+
.description(`Start the ${spec.commandName} (spawns Claude Code at project root)`)
|
|
1254
1357
|
.option("--attach", "Always attach to tmux session after start")
|
|
1255
1358
|
.option("--no-attach", "Never attach to tmux session after start")
|
|
1256
|
-
.option("--watchdog",
|
|
1257
|
-
.option("--monitor",
|
|
1359
|
+
.option("--watchdog", `Auto-start watchdog daemon with ${spec.commandName}`)
|
|
1360
|
+
.option("--monitor", `Auto-start Tier 2 monitor agent with ${spec.commandName}`)
|
|
1361
|
+
.option("--profile <name>", "Canopy profile to apply to spawned agents")
|
|
1258
1362
|
.option("--json", "Output as JSON")
|
|
1259
1363
|
.action(
|
|
1260
|
-
async (opts: {
|
|
1364
|
+
async (opts: {
|
|
1365
|
+
attach?: boolean;
|
|
1366
|
+
watchdog?: boolean;
|
|
1367
|
+
monitor?: boolean;
|
|
1368
|
+
json?: boolean;
|
|
1369
|
+
profile?: string;
|
|
1370
|
+
}) => {
|
|
1261
1371
|
// opts.attach = true if --attach, false if --no-attach, undefined if neither
|
|
1262
1372
|
const shouldAttach = opts.attach !== undefined ? opts.attach : !!process.stdout.isTTY;
|
|
1263
|
-
await
|
|
1373
|
+
await startPersistentAgent(
|
|
1374
|
+
spec,
|
|
1264
1375
|
{
|
|
1265
1376
|
json: opts.json ?? false,
|
|
1266
1377
|
attach: shouldAttach,
|
|
1267
1378
|
watchdog: opts.watchdog ?? false,
|
|
1268
1379
|
monitor: opts.monitor ?? false,
|
|
1380
|
+
profile: opts.profile,
|
|
1269
1381
|
},
|
|
1270
1382
|
deps,
|
|
1271
1383
|
);
|
|
@@ -1274,39 +1386,45 @@ export function createCoordinatorCommand(deps: CoordinatorDeps = {}): Command {
|
|
|
1274
1386
|
|
|
1275
1387
|
cmd
|
|
1276
1388
|
.command("stop")
|
|
1277
|
-
.description(
|
|
1389
|
+
.description(`Stop the ${spec.commandName} (kills tmux session)`)
|
|
1278
1390
|
.option("--json", "Output as JSON")
|
|
1279
1391
|
.action(async (opts: { json?: boolean }) => {
|
|
1280
|
-
await
|
|
1392
|
+
await stopPersistentAgent(spec, { json: opts.json ?? false }, deps);
|
|
1281
1393
|
});
|
|
1282
1394
|
|
|
1283
1395
|
cmd
|
|
1284
1396
|
.command("status")
|
|
1285
|
-
.description(
|
|
1397
|
+
.description(`Show ${spec.commandName} state`)
|
|
1286
1398
|
.option("--json", "Output as JSON")
|
|
1287
1399
|
.action(async (opts: { json?: boolean }) => {
|
|
1288
|
-
await
|
|
1400
|
+
await statusPersistentAgent(spec, { json: opts.json ?? false }, deps);
|
|
1289
1401
|
});
|
|
1290
1402
|
|
|
1291
1403
|
cmd
|
|
1292
1404
|
.command("send")
|
|
1293
|
-
.description(
|
|
1405
|
+
.description(`Send a message to the ${spec.commandName} (fire-and-forget)`)
|
|
1294
1406
|
.requiredOption("--body <text>", "Message body")
|
|
1295
1407
|
.option("--subject <text>", "Message subject", "operator dispatch")
|
|
1296
1408
|
.option("--json", "Output as JSON")
|
|
1297
1409
|
.action(async (opts: { body: string; subject: string; json?: boolean }) => {
|
|
1298
|
-
await
|
|
1410
|
+
await sendToPersistentAgent(
|
|
1411
|
+
spec,
|
|
1412
|
+
opts.body,
|
|
1413
|
+
{ subject: opts.subject, json: opts.json ?? false },
|
|
1414
|
+
deps,
|
|
1415
|
+
);
|
|
1299
1416
|
});
|
|
1300
1417
|
|
|
1301
1418
|
cmd
|
|
1302
1419
|
.command("ask")
|
|
1303
|
-
.description(
|
|
1420
|
+
.description(`Send a request to the ${spec.commandName} and wait for a reply`)
|
|
1304
1421
|
.requiredOption("--body <text>", "Message body")
|
|
1305
1422
|
.option("--subject <text>", "Message subject", "operator request")
|
|
1306
1423
|
.option("--timeout <seconds>", "Timeout in seconds", String(ASK_DEFAULT_TIMEOUT_S))
|
|
1307
1424
|
.option("--json", "Output as JSON")
|
|
1308
1425
|
.action(async (opts: { body: string; subject: string; timeout?: string; json?: boolean }) => {
|
|
1309
|
-
await
|
|
1426
|
+
await askPersistentAgent(
|
|
1427
|
+
spec,
|
|
1310
1428
|
opts.body,
|
|
1311
1429
|
{
|
|
1312
1430
|
subject: opts.subject,
|
|
@@ -1319,14 +1437,15 @@ export function createCoordinatorCommand(deps: CoordinatorDeps = {}): Command {
|
|
|
1319
1437
|
|
|
1320
1438
|
cmd
|
|
1321
1439
|
.command("output")
|
|
1322
|
-
.description(
|
|
1440
|
+
.description(`Show recent ${spec.commandName} output (tmux pane content)`)
|
|
1323
1441
|
.option("--follow, -f", "Continuously poll for new output")
|
|
1324
1442
|
.option("--lines <n>", "Number of lines to capture", "50")
|
|
1325
1443
|
.option("--interval <ms>", "Poll interval in milliseconds (with --follow)", "2000")
|
|
1326
1444
|
.option("--json", "Output as JSON")
|
|
1327
1445
|
.action(
|
|
1328
1446
|
async (opts: { follow?: boolean; lines?: string; interval?: string; json?: boolean }) => {
|
|
1329
|
-
await
|
|
1447
|
+
await outputPersistentAgent(
|
|
1448
|
+
spec,
|
|
1330
1449
|
{
|
|
1331
1450
|
follow: opts.follow ?? false,
|
|
1332
1451
|
lines: Number.parseInt(opts.lines ?? "50", 10),
|
|
@@ -1338,6 +1457,15 @@ export function createCoordinatorCommand(deps: CoordinatorDeps = {}): Command {
|
|
|
1338
1457
|
},
|
|
1339
1458
|
);
|
|
1340
1459
|
|
|
1460
|
+
return cmd;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
/**
|
|
1464
|
+
* Create the Commander command for `ov coordinator`.
|
|
1465
|
+
*/
|
|
1466
|
+
export function createCoordinatorCommand(deps: CoordinatorDeps = {}): Command {
|
|
1467
|
+
const cmd = createPersistentAgentCommand(COORDINATOR_SPEC, deps);
|
|
1468
|
+
|
|
1341
1469
|
cmd
|
|
1342
1470
|
.command("check-complete")
|
|
1343
1471
|
.description("Evaluate exit triggers and report completion status")
|
|
@@ -1349,6 +1477,36 @@ export function createCoordinatorCommand(deps: CoordinatorDeps = {}): Command {
|
|
|
1349
1477
|
return cmd;
|
|
1350
1478
|
}
|
|
1351
1479
|
|
|
1480
|
+
export async function persistentAgentCommand(
|
|
1481
|
+
args: string[],
|
|
1482
|
+
spec: PersistentAgentSpec,
|
|
1483
|
+
deps: CoordinatorDeps = {},
|
|
1484
|
+
): Promise<void> {
|
|
1485
|
+
const cmd = createPersistentAgentCommand(spec, deps);
|
|
1486
|
+
cmd.exitOverride();
|
|
1487
|
+
|
|
1488
|
+
if (args.length === 0) {
|
|
1489
|
+
process.stdout.write(cmd.helpInformation());
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
try {
|
|
1494
|
+
await cmd.parseAsync(args, { from: "user" });
|
|
1495
|
+
} catch (err: unknown) {
|
|
1496
|
+
if (err && typeof err === "object" && "code" in err) {
|
|
1497
|
+
const code = (err as { code: string }).code;
|
|
1498
|
+
if (code === "commander.helpDisplayed" || code === "commander.version") {
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
if (code === "commander.unknownCommand") {
|
|
1502
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1503
|
+
throw new ValidationError(message, { field: "subcommand" });
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
throw err;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1352
1510
|
/**
|
|
1353
1511
|
* Entry point for `ov coordinator <subcommand>`.
|
|
1354
1512
|
*
|