@os-eco/overstory-cli 0.6.1 → 0.6.5
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 +8 -7
- package/package.json +12 -4
- package/src/agents/checkpoint.test.ts +2 -2
- package/src/agents/hooks-deployer.test.ts +131 -16
- package/src/agents/hooks-deployer.ts +33 -1
- package/src/agents/identity.test.ts +27 -27
- package/src/agents/identity.ts +10 -10
- package/src/agents/lifecycle.test.ts +6 -6
- package/src/agents/lifecycle.ts +2 -2
- package/src/agents/manifest.test.ts +86 -0
- package/src/agents/overlay.test.ts +9 -9
- package/src/agents/overlay.ts +4 -4
- package/src/commands/agents.test.ts +8 -8
- package/src/commands/agents.ts +62 -91
- package/src/commands/clean.test.ts +36 -51
- package/src/commands/clean.ts +28 -49
- package/src/commands/completions.ts +14 -0
- package/src/commands/coordinator.test.ts +133 -26
- package/src/commands/coordinator.ts +101 -64
- package/src/commands/costs.test.ts +47 -47
- package/src/commands/costs.ts +96 -75
- package/src/commands/dashboard.test.ts +2 -2
- package/src/commands/dashboard.ts +75 -95
- package/src/commands/doctor.test.ts +2 -2
- package/src/commands/doctor.ts +92 -79
- package/src/commands/errors.test.ts +2 -2
- package/src/commands/errors.ts +56 -50
- package/src/commands/feed.test.ts +2 -2
- package/src/commands/feed.ts +86 -83
- package/src/commands/group.ts +167 -177
- package/src/commands/hooks.test.ts +2 -2
- package/src/commands/hooks.ts +52 -42
- package/src/commands/init.test.ts +19 -19
- package/src/commands/init.ts +7 -16
- package/src/commands/inspect.test.ts +18 -18
- package/src/commands/inspect.ts +55 -58
- package/src/commands/log.test.ts +26 -31
- package/src/commands/log.ts +97 -91
- package/src/commands/logs.test.ts +1 -1
- package/src/commands/logs.ts +101 -104
- package/src/commands/mail.test.ts +5 -5
- package/src/commands/mail.ts +157 -169
- package/src/commands/merge.test.ts +28 -66
- package/src/commands/merge.ts +21 -51
- package/src/commands/metrics.test.ts +8 -8
- package/src/commands/metrics.ts +34 -35
- package/src/commands/monitor.test.ts +3 -3
- package/src/commands/monitor.ts +57 -62
- package/src/commands/nudge.test.ts +1 -1
- package/src/commands/nudge.ts +41 -89
- package/src/commands/prime.test.ts +19 -51
- package/src/commands/prime.ts +13 -50
- package/src/commands/replay.test.ts +2 -2
- package/src/commands/replay.ts +79 -86
- package/src/commands/run.test.ts +1 -1
- package/src/commands/run.ts +97 -77
- package/src/commands/sling.test.ts +201 -5
- package/src/commands/sling.ts +37 -64
- package/src/commands/spec.test.ts +14 -40
- package/src/commands/spec.ts +32 -101
- package/src/commands/status.test.ts +97 -1
- package/src/commands/status.ts +63 -58
- package/src/commands/stop.test.ts +22 -40
- package/src/commands/stop.ts +18 -33
- package/src/commands/supervisor.test.ts +12 -14
- package/src/commands/supervisor.ts +144 -165
- package/src/commands/trace.test.ts +15 -15
- package/src/commands/trace.ts +59 -82
- package/src/commands/watch.test.ts +2 -2
- package/src/commands/watch.ts +38 -45
- package/src/commands/worktree.test.ts +213 -37
- package/src/commands/worktree.ts +110 -55
- package/src/config.test.ts +96 -0
- package/src/doctor/consistency.test.ts +14 -14
- package/src/doctor/databases.test.ts +22 -2
- package/src/doctor/databases.ts +16 -0
- package/src/doctor/dependencies.test.ts +55 -1
- package/src/doctor/dependencies.ts +113 -18
- package/src/doctor/merge-queue.test.ts +4 -4
- package/src/e2e/init-sling-lifecycle.test.ts +8 -8
- package/src/errors.ts +1 -1
- package/src/index.ts +223 -213
- package/src/logging/color.test.ts +74 -91
- package/src/logging/color.ts +52 -46
- package/src/logging/reporter.test.ts +10 -10
- package/src/logging/reporter.ts +6 -5
- package/src/mail/broadcast.test.ts +1 -1
- package/src/mail/client.test.ts +6 -6
- package/src/mail/store.test.ts +3 -3
- package/src/merge/queue.test.ts +73 -7
- package/src/merge/queue.ts +17 -2
- package/src/merge/resolver.test.ts +159 -7
- package/src/merge/resolver.ts +46 -2
- package/src/metrics/store.test.ts +44 -44
- package/src/metrics/store.ts +2 -2
- package/src/metrics/summary.test.ts +35 -35
- package/src/mulch/client.test.ts +1 -1
- package/src/schema-consistency.test.ts +239 -0
- package/src/sessions/compat.test.ts +3 -3
- package/src/sessions/compat.ts +2 -2
- package/src/sessions/store.test.ts +41 -4
- package/src/sessions/store.ts +13 -2
- package/src/types.ts +14 -14
- package/src/watchdog/daemon.test.ts +10 -10
- package/src/watchdog/daemon.ts +1 -1
- package/src/watchdog/health.test.ts +1 -1
- package/src/worktree/manager.test.ts +20 -20
- package/src/worktree/manager.ts +120 -4
- package/src/worktree/tmux.test.ts +98 -9
- package/src/worktree/tmux.ts +18 -0
|
@@ -23,9 +23,9 @@ import {
|
|
|
23
23
|
buildCoordinatorBeacon,
|
|
24
24
|
type CoordinatorDeps,
|
|
25
25
|
coordinatorCommand,
|
|
26
|
+
createCoordinatorCommand,
|
|
26
27
|
resolveAttach,
|
|
27
28
|
} from "./coordinator.ts";
|
|
28
|
-
import { isRunningAsRoot } from "./sling.ts";
|
|
29
29
|
|
|
30
30
|
// --- Fake Tmux ---
|
|
31
31
|
|
|
@@ -41,6 +41,7 @@ interface TmuxCallTracker {
|
|
|
41
41
|
killSession: Array<{ name: string }>;
|
|
42
42
|
sendKeys: Array<{ name: string; keys: string }>;
|
|
43
43
|
waitForTuiReady: Array<{ name: string }>;
|
|
44
|
+
ensureTmuxAvailable: number;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
// --- Fake Watchdog ---
|
|
@@ -62,7 +63,13 @@ interface MonitorCallTracker {
|
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
/** Build a fake tmux DI object with configurable session liveness. */
|
|
65
|
-
function makeFakeTmux(
|
|
66
|
+
function makeFakeTmux(
|
|
67
|
+
sessionAliveMap: Record<string, boolean> = {},
|
|
68
|
+
options: {
|
|
69
|
+
waitForTuiReadyResult?: boolean;
|
|
70
|
+
ensureTmuxAvailableError?: Error;
|
|
71
|
+
} = {},
|
|
72
|
+
): {
|
|
66
73
|
tmux: NonNullable<CoordinatorDeps["_tmux"]>;
|
|
67
74
|
calls: TmuxCallTracker;
|
|
68
75
|
} {
|
|
@@ -72,6 +79,7 @@ function makeFakeTmux(sessionAliveMap: Record<string, boolean> = {}): {
|
|
|
72
79
|
killSession: [],
|
|
73
80
|
sendKeys: [],
|
|
74
81
|
waitForTuiReady: [],
|
|
82
|
+
ensureTmuxAvailable: 0,
|
|
75
83
|
};
|
|
76
84
|
|
|
77
85
|
const tmux: NonNullable<CoordinatorDeps["_tmux"]> = {
|
|
@@ -97,7 +105,13 @@ function makeFakeTmux(sessionAliveMap: Record<string, boolean> = {}): {
|
|
|
97
105
|
},
|
|
98
106
|
waitForTuiReady: async (name: string): Promise<boolean> => {
|
|
99
107
|
calls.waitForTuiReady.push({ name });
|
|
100
|
-
return true;
|
|
108
|
+
return options.waitForTuiReadyResult ?? true;
|
|
109
|
+
},
|
|
110
|
+
ensureTmuxAvailable: async (): Promise<void> => {
|
|
111
|
+
calls.ensureTmuxAvailable++;
|
|
112
|
+
if (options.ensureTmuxAvailableError) {
|
|
113
|
+
throw options.ensureTmuxAvailableError;
|
|
114
|
+
}
|
|
101
115
|
},
|
|
102
116
|
};
|
|
103
117
|
|
|
@@ -272,7 +286,7 @@ function makeCoordinatorSession(overrides: Partial<AgentSession> = {}): AgentSes
|
|
|
272
286
|
capability: "coordinator",
|
|
273
287
|
worktreePath: tempDir,
|
|
274
288
|
branchName: "main",
|
|
275
|
-
|
|
289
|
+
taskId: "",
|
|
276
290
|
tmuxSession: "overstory-test-project-coordinator",
|
|
277
291
|
state: "working",
|
|
278
292
|
pid: 99999,
|
|
@@ -310,13 +324,14 @@ function makeDeps(
|
|
|
310
324
|
sessionAliveMap: Record<string, boolean> = {},
|
|
311
325
|
watchdogConfig?: { running?: boolean; startSuccess?: boolean; stopSuccess?: boolean },
|
|
312
326
|
monitorConfig?: { running?: boolean; startSuccess?: boolean; stopSuccess?: boolean },
|
|
327
|
+
tmuxOptions?: { waitForTuiReadyResult?: boolean; ensureTmuxAvailableError?: Error },
|
|
313
328
|
): {
|
|
314
329
|
deps: CoordinatorDeps;
|
|
315
330
|
calls: TmuxCallTracker;
|
|
316
331
|
watchdogCalls: WatchdogCallTracker;
|
|
317
332
|
monitorCalls: MonitorCallTracker;
|
|
318
333
|
} {
|
|
319
|
-
const { tmux, calls } = makeFakeTmux(sessionAliveMap);
|
|
334
|
+
const { tmux, calls } = makeFakeTmux(sessionAliveMap, tmuxOptions);
|
|
320
335
|
const { watchdog, calls: watchdogCalls } = makeFakeWatchdog(
|
|
321
336
|
watchdogConfig?.running,
|
|
322
337
|
watchdogConfig?.startSuccess,
|
|
@@ -347,27 +362,33 @@ function makeDeps(
|
|
|
347
362
|
describe("coordinatorCommand help", () => {
|
|
348
363
|
test("--help outputs help text", async () => {
|
|
349
364
|
const output = await captureStdout(() => coordinatorCommand(["--help"]));
|
|
350
|
-
expect(output).toContain("
|
|
365
|
+
expect(output).toContain("coordinator");
|
|
351
366
|
expect(output).toContain("start");
|
|
352
367
|
expect(output).toContain("stop");
|
|
353
368
|
expect(output).toContain("status");
|
|
354
369
|
});
|
|
355
370
|
|
|
356
|
-
test("--help includes --attach and --no-attach flags", async () => {
|
|
357
|
-
const
|
|
371
|
+
test("start --help includes --attach and --no-attach flags", async () => {
|
|
372
|
+
const cmd = createCoordinatorCommand({});
|
|
373
|
+
for (const sub of cmd.commands) {
|
|
374
|
+
sub.exitOverride();
|
|
375
|
+
}
|
|
376
|
+
const output = await captureStdout(async () => {
|
|
377
|
+
await cmd.parseAsync(["start", "--help"], { from: "user" }).catch(() => {});
|
|
378
|
+
});
|
|
358
379
|
expect(output).toContain("--attach");
|
|
359
380
|
expect(output).toContain("--no-attach");
|
|
360
381
|
});
|
|
361
382
|
|
|
362
383
|
test("-h outputs help text", async () => {
|
|
363
384
|
const output = await captureStdout(() => coordinatorCommand(["-h"]));
|
|
364
|
-
expect(output).toContain("
|
|
385
|
+
expect(output).toContain("coordinator");
|
|
365
386
|
});
|
|
366
387
|
|
|
367
388
|
test("empty args outputs help text", async () => {
|
|
368
389
|
const output = await captureStdout(() => coordinatorCommand([]));
|
|
369
|
-
expect(output).toContain("
|
|
370
|
-
expect(output).toContain("
|
|
390
|
+
expect(output).toContain("coordinator");
|
|
391
|
+
expect(output).toContain("Commands:");
|
|
371
392
|
});
|
|
372
393
|
});
|
|
373
394
|
|
|
@@ -385,7 +406,6 @@ describe("coordinatorCommand unknown subcommand", () => {
|
|
|
385
406
|
const ve = err as ValidationError;
|
|
386
407
|
expect(ve.message).toContain("frobnicate");
|
|
387
408
|
expect(ve.field).toBe("subcommand");
|
|
388
|
-
expect(ve.value).toBe("frobnicate");
|
|
389
409
|
}
|
|
390
410
|
});
|
|
391
411
|
});
|
|
@@ -417,7 +437,7 @@ describe("startCoordinator", () => {
|
|
|
417
437
|
expect(session?.pid).toBe(99999);
|
|
418
438
|
expect(session?.parentAgent).toBeNull();
|
|
419
439
|
expect(session?.depth).toBe(0);
|
|
420
|
-
expect(session?.
|
|
440
|
+
expect(session?.taskId).toBe("");
|
|
421
441
|
expect(session?.branchName).toBe("main");
|
|
422
442
|
expect(session?.worktreePath).toBe(tempDir);
|
|
423
443
|
expect(session?.id).toMatch(/^session-\d+-coordinator$/);
|
|
@@ -630,6 +650,88 @@ describe("startCoordinator", () => {
|
|
|
630
650
|
// The new session should have a different ID than the dead one
|
|
631
651
|
expect(newSession?.id).not.toBe("session-dead-coordinator");
|
|
632
652
|
});
|
|
653
|
+
|
|
654
|
+
test("throws AgentError when tmux is not available", async () => {
|
|
655
|
+
const { deps } = makeDeps({}, undefined, undefined, {
|
|
656
|
+
ensureTmuxAvailableError: new AgentError(
|
|
657
|
+
"tmux is not installed or not on PATH. Install tmux to use overstory agent orchestration.",
|
|
658
|
+
),
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
await expect(coordinatorCommand(["start"], deps)).rejects.toThrow(AgentError);
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
test("AgentError message mentions tmux not installed when tmux unavailable", async () => {
|
|
665
|
+
const { deps } = makeDeps({}, undefined, undefined, {
|
|
666
|
+
ensureTmuxAvailableError: new AgentError(
|
|
667
|
+
"tmux is not installed or not on PATH. Install tmux to use overstory agent orchestration.",
|
|
668
|
+
),
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
try {
|
|
672
|
+
await coordinatorCommand(["start"], deps);
|
|
673
|
+
expect(true).toBe(false); // Should have thrown
|
|
674
|
+
} catch (err: unknown) {
|
|
675
|
+
expect(err).toBeInstanceOf(AgentError);
|
|
676
|
+
const agentErr = err as AgentError;
|
|
677
|
+
expect(agentErr.message).toContain("tmux is not installed");
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
test("throws AgentError when session dies during startup", async () => {
|
|
682
|
+
// waitForTuiReady returns false AND isSessionAlive returns false — session died
|
|
683
|
+
const { deps } = makeDeps(
|
|
684
|
+
{ "overstory-test-project-coordinator": false },
|
|
685
|
+
undefined,
|
|
686
|
+
undefined,
|
|
687
|
+
{ waitForTuiReadyResult: false },
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
await expect(coordinatorCommand(["start"], deps)).rejects.toThrow(AgentError);
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
test("AgentError message mentions session dying when session dies during startup", async () => {
|
|
694
|
+
const { deps } = makeDeps(
|
|
695
|
+
{ "overstory-test-project-coordinator": false },
|
|
696
|
+
undefined,
|
|
697
|
+
undefined,
|
|
698
|
+
{ waitForTuiReadyResult: false },
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
try {
|
|
702
|
+
await coordinatorCommand(["start"], deps);
|
|
703
|
+
expect(true).toBe(false); // Should have thrown
|
|
704
|
+
} catch (err: unknown) {
|
|
705
|
+
expect(err).toBeInstanceOf(AgentError);
|
|
706
|
+
const agentErr = err as AgentError;
|
|
707
|
+
expect(agentErr.message).toContain("died during startup");
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
test("continues when waitForTuiReady times out but session is still alive", async () => {
|
|
712
|
+
// waitForTuiReady returns false (timeout) but session IS alive
|
|
713
|
+
const { deps } = makeDeps(
|
|
714
|
+
{ "overstory-test-project-coordinator": true },
|
|
715
|
+
undefined,
|
|
716
|
+
undefined,
|
|
717
|
+
{ waitForTuiReadyResult: false },
|
|
718
|
+
);
|
|
719
|
+
|
|
720
|
+
const originalSleep = Bun.sleep;
|
|
721
|
+
Bun.sleep = (() => Promise.resolve()) as typeof Bun.sleep;
|
|
722
|
+
|
|
723
|
+
let thrownError: unknown;
|
|
724
|
+
try {
|
|
725
|
+
await captureStdout(() => coordinatorCommand(["start"], deps));
|
|
726
|
+
} catch (err: unknown) {
|
|
727
|
+
thrownError = err;
|
|
728
|
+
} finally {
|
|
729
|
+
Bun.sleep = originalSleep;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Should NOT throw — session is alive, just slow TUI
|
|
733
|
+
expect(thrownError).toBeUndefined();
|
|
734
|
+
});
|
|
633
735
|
});
|
|
634
736
|
|
|
635
737
|
describe("stopCoordinator", () => {
|
|
@@ -1202,10 +1304,16 @@ describe("watchdog integration", () => {
|
|
|
1202
1304
|
});
|
|
1203
1305
|
|
|
1204
1306
|
describe("COORDINATOR_HELP", () => {
|
|
1205
|
-
test("help text includes --watchdog flag", async () => {
|
|
1206
|
-
const
|
|
1307
|
+
test("start help text includes --watchdog flag", async () => {
|
|
1308
|
+
const cmd = createCoordinatorCommand({});
|
|
1309
|
+
for (const sub of cmd.commands) {
|
|
1310
|
+
sub.exitOverride();
|
|
1311
|
+
}
|
|
1312
|
+
const output = await captureStdout(async () => {
|
|
1313
|
+
await cmd.parseAsync(["start", "--help"], { from: "user" }).catch(() => {});
|
|
1314
|
+
});
|
|
1207
1315
|
expect(output).toContain("--watchdog");
|
|
1208
|
-
expect(output).toContain("
|
|
1316
|
+
expect(output).toContain("watchdog");
|
|
1209
1317
|
});
|
|
1210
1318
|
});
|
|
1211
1319
|
});
|
|
@@ -1490,10 +1598,16 @@ describe("monitor integration", () => {
|
|
|
1490
1598
|
});
|
|
1491
1599
|
|
|
1492
1600
|
describe("COORDINATOR_HELP", () => {
|
|
1493
|
-
test("help text includes --monitor flag", async () => {
|
|
1494
|
-
const
|
|
1601
|
+
test("start help text includes --monitor flag", async () => {
|
|
1602
|
+
const cmd = createCoordinatorCommand({});
|
|
1603
|
+
for (const sub of cmd.commands) {
|
|
1604
|
+
sub.exitOverride();
|
|
1605
|
+
}
|
|
1606
|
+
const output = await captureStdout(async () => {
|
|
1607
|
+
await cmd.parseAsync(["start", "--help"], { from: "user" }).catch(() => {});
|
|
1608
|
+
});
|
|
1495
1609
|
expect(output).toContain("--monitor");
|
|
1496
|
-
expect(output).toContain("
|
|
1610
|
+
expect(output).toContain("monitor");
|
|
1497
1611
|
});
|
|
1498
1612
|
});
|
|
1499
1613
|
});
|
|
@@ -1521,10 +1635,3 @@ describe("SessionStore round-trip", () => {
|
|
|
1521
1635
|
expect(exists).toBe(true);
|
|
1522
1636
|
});
|
|
1523
1637
|
});
|
|
1524
|
-
|
|
1525
|
-
describe("isRunningAsRoot (imported from sling)", () => {
|
|
1526
|
-
test("is accessible from coordinator test file", () => {
|
|
1527
|
-
expect(isRunningAsRoot(() => 0)).toBe(true);
|
|
1528
|
-
expect(isRunningAsRoot(() => 1000)).toBe(false);
|
|
1529
|
-
});
|
|
1530
|
-
});
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { mkdir, unlink } from "node:fs/promises";
|
|
16
16
|
import { join } from "node:path";
|
|
17
|
+
import { Command } from "commander";
|
|
17
18
|
import { deployHooks } from "../agents/hooks-deployer.ts";
|
|
18
19
|
import { createIdentity, loadIdentity } from "../agents/identity.ts";
|
|
19
20
|
import { createManifestLoader, resolveModel } from "../agents/manifest.ts";
|
|
@@ -26,6 +27,7 @@ import type { AgentSession } from "../types.ts";
|
|
|
26
27
|
import { isProcessRunning } from "../watchdog/health.ts";
|
|
27
28
|
import {
|
|
28
29
|
createSession,
|
|
30
|
+
ensureTmuxAvailable,
|
|
29
31
|
isSessionAlive,
|
|
30
32
|
killSession,
|
|
31
33
|
sendKeys,
|
|
@@ -61,6 +63,7 @@ export interface CoordinatorDeps {
|
|
|
61
63
|
timeoutMs?: number,
|
|
62
64
|
pollIntervalMs?: number,
|
|
63
65
|
) => Promise<boolean>;
|
|
66
|
+
ensureTmuxAvailable: () => Promise<void>;
|
|
64
67
|
};
|
|
65
68
|
_watchdog?: {
|
|
66
69
|
start: () => Promise<{ pid: number } | null>;
|
|
@@ -252,17 +255,6 @@ export function buildCoordinatorBeacon(cliName = "bd"): string {
|
|
|
252
255
|
return parts.join(" — ");
|
|
253
256
|
}
|
|
254
257
|
|
|
255
|
-
/**
|
|
256
|
-
* Start the coordinator agent.
|
|
257
|
-
*
|
|
258
|
-
* 1. Verify no coordinator is already running
|
|
259
|
-
* 2. Load config
|
|
260
|
-
* 3. Create agent identity (if first time)
|
|
261
|
-
* 4. Deploy hooks to project root's .claude/settings.local.json
|
|
262
|
-
* 5. Spawn tmux session at project root with Claude Code
|
|
263
|
-
* 6. Send startup beacon
|
|
264
|
-
* 7. Record session in SessionStore (sessions.db)
|
|
265
|
-
*/
|
|
266
258
|
/**
|
|
267
259
|
* Determine whether to auto-attach to the tmux session after starting.
|
|
268
260
|
* Exported for testing.
|
|
@@ -273,19 +265,20 @@ export function resolveAttach(args: string[], isTTY: boolean): boolean {
|
|
|
273
265
|
return isTTY;
|
|
274
266
|
}
|
|
275
267
|
|
|
276
|
-
async function startCoordinator(
|
|
268
|
+
async function startCoordinator(
|
|
269
|
+
opts: { json: boolean; attach: boolean; watchdog: boolean; monitor: boolean },
|
|
270
|
+
deps: CoordinatorDeps = {},
|
|
271
|
+
): Promise<void> {
|
|
277
272
|
const tmux = deps._tmux ?? {
|
|
278
273
|
createSession,
|
|
279
274
|
isSessionAlive,
|
|
280
275
|
killSession,
|
|
281
276
|
sendKeys,
|
|
282
277
|
waitForTuiReady,
|
|
278
|
+
ensureTmuxAvailable,
|
|
283
279
|
};
|
|
284
280
|
|
|
285
|
-
const json =
|
|
286
|
-
const shouldAttach = resolveAttach(args, !!process.stdout.isTTY);
|
|
287
|
-
const watchdogFlag = args.includes("--watchdog");
|
|
288
|
-
const monitorFlag = args.includes("--monitor");
|
|
281
|
+
const { json, attach: shouldAttach, watchdog: watchdogFlag, monitor: monitorFlag } = opts;
|
|
289
282
|
|
|
290
283
|
if (isRunningAsRoot()) {
|
|
291
284
|
throw new AgentError(
|
|
@@ -354,6 +347,10 @@ async function startCoordinator(args: string[], deps: CoordinatorDeps = {}): Pro
|
|
|
354
347
|
const manifest = await manifestLoader.load();
|
|
355
348
|
const { model, env } = resolveModel(config, manifest, "coordinator", "opus");
|
|
356
349
|
|
|
350
|
+
// Preflight: verify tmux is installed before attempting to spawn.
|
|
351
|
+
// Without this check, a missing tmux leads to cryptic errors later.
|
|
352
|
+
await tmux.ensureTmuxAvailable();
|
|
353
|
+
|
|
357
354
|
// Spawn tmux session at project root with Claude Code (interactive mode).
|
|
358
355
|
// Inject the coordinator base definition via --append-system-prompt so the
|
|
359
356
|
// coordinator knows its role, hierarchy rules, and delegation patterns
|
|
@@ -382,7 +379,7 @@ async function startCoordinator(args: string[], deps: CoordinatorDeps = {}): Pro
|
|
|
382
379
|
capability: "coordinator",
|
|
383
380
|
worktreePath: projectRoot, // Coordinator uses project root, not a worktree
|
|
384
381
|
branchName: config.project.canonicalBranch, // Operates on canonical branch
|
|
385
|
-
|
|
382
|
+
taskId: "", // No specific bead assignment
|
|
386
383
|
tmuxSession,
|
|
387
384
|
state: "booting",
|
|
388
385
|
pid,
|
|
@@ -398,7 +395,20 @@ async function startCoordinator(args: string[], deps: CoordinatorDeps = {}): Pro
|
|
|
398
395
|
store.upsert(session);
|
|
399
396
|
|
|
400
397
|
// Wait for Claude Code TUI to render before sending input
|
|
401
|
-
await tmux.waitForTuiReady(tmuxSession);
|
|
398
|
+
const tuiReady = await tmux.waitForTuiReady(tmuxSession);
|
|
399
|
+
if (!tuiReady) {
|
|
400
|
+
// Session may have died — check liveness before proceeding
|
|
401
|
+
const alive = await tmux.isSessionAlive(tmuxSession);
|
|
402
|
+
if (!alive) {
|
|
403
|
+
// Clean up the stale session record
|
|
404
|
+
store.updateState(COORDINATOR_NAME, "completed");
|
|
405
|
+
throw new AgentError(
|
|
406
|
+
`Coordinator tmux session "${tmuxSession}" died during startup. The Claude Code process may have crashed or exited immediately. Check tmux logs or try running the claude command manually.`,
|
|
407
|
+
{ agentName: COORDINATOR_NAME },
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
// Session is alive but TUI didn't render in time — proceed with warning
|
|
411
|
+
}
|
|
402
412
|
await Bun.sleep(1_000);
|
|
403
413
|
|
|
404
414
|
const resolvedBackend = await resolveBackend(config.taskTracker.backend, config.project.root);
|
|
@@ -478,16 +488,17 @@ async function startCoordinator(args: string[], deps: CoordinatorDeps = {}): Pro
|
|
|
478
488
|
* 3. Mark session as completed in SessionStore
|
|
479
489
|
* 4. Auto-complete the active run (if current-run.txt exists)
|
|
480
490
|
*/
|
|
481
|
-
async function stopCoordinator(
|
|
491
|
+
async function stopCoordinator(opts: { json: boolean }, deps: CoordinatorDeps = {}): Promise<void> {
|
|
482
492
|
const tmux = deps._tmux ?? {
|
|
483
493
|
createSession,
|
|
484
494
|
isSessionAlive,
|
|
485
495
|
killSession,
|
|
486
496
|
sendKeys,
|
|
487
497
|
waitForTuiReady,
|
|
498
|
+
ensureTmuxAvailable,
|
|
488
499
|
};
|
|
489
500
|
|
|
490
|
-
const json =
|
|
501
|
+
const { json } = opts;
|
|
491
502
|
const cwd = process.cwd();
|
|
492
503
|
const config = await loadConfig(cwd);
|
|
493
504
|
const projectRoot = config.project.root;
|
|
@@ -584,16 +595,20 @@ async function stopCoordinator(args: string[], deps: CoordinatorDeps = {}): Prom
|
|
|
584
595
|
*
|
|
585
596
|
* Checks session registry and tmux liveness to report actual state.
|
|
586
597
|
*/
|
|
587
|
-
async function statusCoordinator(
|
|
598
|
+
async function statusCoordinator(
|
|
599
|
+
opts: { json: boolean },
|
|
600
|
+
deps: CoordinatorDeps = {},
|
|
601
|
+
): Promise<void> {
|
|
588
602
|
const tmux = deps._tmux ?? {
|
|
589
603
|
createSession,
|
|
590
604
|
isSessionAlive,
|
|
591
605
|
killSession,
|
|
592
606
|
sendKeys,
|
|
593
607
|
waitForTuiReady,
|
|
608
|
+
ensureTmuxAvailable,
|
|
594
609
|
};
|
|
595
610
|
|
|
596
|
-
const json =
|
|
611
|
+
const { json } = opts;
|
|
597
612
|
const cwd = process.cwd();
|
|
598
613
|
const config = await loadConfig(cwd);
|
|
599
614
|
const projectRoot = config.project.root;
|
|
@@ -670,31 +685,54 @@ async function statusCoordinator(args: string[], deps: CoordinatorDeps = {}): Pr
|
|
|
670
685
|
}
|
|
671
686
|
}
|
|
672
687
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
688
|
+
/**
|
|
689
|
+
* Create the Commander command for `overstory coordinator`.
|
|
690
|
+
*/
|
|
691
|
+
export function createCoordinatorCommand(deps: CoordinatorDeps = {}): Command {
|
|
692
|
+
const cmd = new Command("coordinator").description("Manage the persistent coordinator agent");
|
|
693
|
+
|
|
694
|
+
cmd
|
|
695
|
+
.command("start")
|
|
696
|
+
.description("Start the coordinator (spawns Claude Code at project root)")
|
|
697
|
+
.option("--attach", "Always attach to tmux session after start")
|
|
698
|
+
.option("--no-attach", "Never attach to tmux session after start")
|
|
699
|
+
.option("--watchdog", "Auto-start watchdog daemon with coordinator")
|
|
700
|
+
.option("--monitor", "Auto-start Tier 2 monitor agent with coordinator")
|
|
701
|
+
.option("--json", "Output as JSON")
|
|
702
|
+
.action(
|
|
703
|
+
async (opts: { attach?: boolean; watchdog?: boolean; monitor?: boolean; json?: boolean }) => {
|
|
704
|
+
// opts.attach = true if --attach, false if --no-attach, undefined if neither
|
|
705
|
+
const shouldAttach = opts.attach !== undefined ? opts.attach : !!process.stdout.isTTY;
|
|
706
|
+
await startCoordinator(
|
|
707
|
+
{
|
|
708
|
+
json: opts.json ?? false,
|
|
709
|
+
attach: shouldAttach,
|
|
710
|
+
watchdog: opts.watchdog ?? false,
|
|
711
|
+
monitor: opts.monitor ?? false,
|
|
712
|
+
},
|
|
713
|
+
deps,
|
|
714
|
+
);
|
|
715
|
+
},
|
|
716
|
+
);
|
|
681
717
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
718
|
+
cmd
|
|
719
|
+
.command("stop")
|
|
720
|
+
.description("Stop the coordinator (kills tmux session)")
|
|
721
|
+
.option("--json", "Output as JSON")
|
|
722
|
+
.action(async (opts: { json?: boolean }) => {
|
|
723
|
+
await stopCoordinator({ json: opts.json ?? false }, deps);
|
|
724
|
+
});
|
|
688
725
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
726
|
+
cmd
|
|
727
|
+
.command("status")
|
|
728
|
+
.description("Show coordinator state")
|
|
729
|
+
.option("--json", "Output as JSON")
|
|
730
|
+
.action(async (opts: { json?: boolean }) => {
|
|
731
|
+
await statusCoordinator({ json: opts.json ?? false }, deps);
|
|
732
|
+
});
|
|
692
733
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
- Dispatching agents via overstory sling
|
|
696
|
-
- Tracking batches via task groups
|
|
697
|
-
- Handling escalations from agents and watchdog`;
|
|
734
|
+
return cmd;
|
|
735
|
+
}
|
|
698
736
|
|
|
699
737
|
/**
|
|
700
738
|
* Entry point for `overstory coordinator <subcommand>`.
|
|
@@ -706,28 +744,27 @@ export async function coordinatorCommand(
|
|
|
706
744
|
args: string[],
|
|
707
745
|
deps: CoordinatorDeps = {},
|
|
708
746
|
): Promise<void> {
|
|
709
|
-
|
|
710
|
-
|
|
747
|
+
const cmd = createCoordinatorCommand(deps);
|
|
748
|
+
cmd.exitOverride();
|
|
749
|
+
|
|
750
|
+
if (args.length === 0) {
|
|
751
|
+
process.stdout.write(cmd.helpInformation());
|
|
711
752
|
return;
|
|
712
753
|
}
|
|
713
754
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
throw new ValidationError(
|
|
729
|
-
`Unknown coordinator subcommand: ${subcommand}. Run 'overstory coordinator --help' for usage.`,
|
|
730
|
-
{ field: "subcommand", value: subcommand },
|
|
731
|
-
);
|
|
755
|
+
try {
|
|
756
|
+
await cmd.parseAsync(args, { from: "user" });
|
|
757
|
+
} catch (err: unknown) {
|
|
758
|
+
if (err && typeof err === "object" && "code" in err) {
|
|
759
|
+
const code = (err as { code: string }).code;
|
|
760
|
+
if (code === "commander.helpDisplayed" || code === "commander.version") {
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if (code === "commander.unknownCommand") {
|
|
764
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
765
|
+
throw new ValidationError(message, { field: "subcommand" });
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
throw err;
|
|
732
769
|
}
|
|
733
770
|
}
|