@os-eco/overstory-cli 0.6.1

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 (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +381 -0
  3. package/agents/builder.md +137 -0
  4. package/agents/coordinator.md +263 -0
  5. package/agents/lead.md +301 -0
  6. package/agents/merger.md +160 -0
  7. package/agents/monitor.md +214 -0
  8. package/agents/reviewer.md +140 -0
  9. package/agents/scout.md +119 -0
  10. package/agents/supervisor.md +423 -0
  11. package/package.json +47 -0
  12. package/src/agents/checkpoint.test.ts +88 -0
  13. package/src/agents/checkpoint.ts +101 -0
  14. package/src/agents/hooks-deployer.test.ts +2040 -0
  15. package/src/agents/hooks-deployer.ts +607 -0
  16. package/src/agents/identity.test.ts +603 -0
  17. package/src/agents/identity.ts +384 -0
  18. package/src/agents/lifecycle.test.ts +196 -0
  19. package/src/agents/lifecycle.ts +183 -0
  20. package/src/agents/manifest.test.ts +746 -0
  21. package/src/agents/manifest.ts +354 -0
  22. package/src/agents/overlay.test.ts +676 -0
  23. package/src/agents/overlay.ts +308 -0
  24. package/src/beads/client.test.ts +217 -0
  25. package/src/beads/client.ts +202 -0
  26. package/src/beads/molecules.test.ts +338 -0
  27. package/src/beads/molecules.ts +198 -0
  28. package/src/commands/agents.test.ts +322 -0
  29. package/src/commands/agents.ts +287 -0
  30. package/src/commands/clean.test.ts +670 -0
  31. package/src/commands/clean.ts +618 -0
  32. package/src/commands/completions.test.ts +342 -0
  33. package/src/commands/completions.ts +887 -0
  34. package/src/commands/coordinator.test.ts +1530 -0
  35. package/src/commands/coordinator.ts +733 -0
  36. package/src/commands/costs.test.ts +1119 -0
  37. package/src/commands/costs.ts +564 -0
  38. package/src/commands/dashboard.test.ts +308 -0
  39. package/src/commands/dashboard.ts +838 -0
  40. package/src/commands/doctor.test.ts +294 -0
  41. package/src/commands/doctor.ts +213 -0
  42. package/src/commands/errors.test.ts +647 -0
  43. package/src/commands/errors.ts +248 -0
  44. package/src/commands/feed.test.ts +578 -0
  45. package/src/commands/feed.ts +361 -0
  46. package/src/commands/group.test.ts +262 -0
  47. package/src/commands/group.ts +511 -0
  48. package/src/commands/hooks.test.ts +458 -0
  49. package/src/commands/hooks.ts +253 -0
  50. package/src/commands/init.test.ts +347 -0
  51. package/src/commands/init.ts +650 -0
  52. package/src/commands/inspect.test.ts +670 -0
  53. package/src/commands/inspect.ts +431 -0
  54. package/src/commands/log.test.ts +1454 -0
  55. package/src/commands/log.ts +724 -0
  56. package/src/commands/logs.test.ts +379 -0
  57. package/src/commands/logs.ts +546 -0
  58. package/src/commands/mail.test.ts +1270 -0
  59. package/src/commands/mail.ts +771 -0
  60. package/src/commands/merge.test.ts +670 -0
  61. package/src/commands/merge.ts +355 -0
  62. package/src/commands/metrics.test.ts +444 -0
  63. package/src/commands/metrics.ts +143 -0
  64. package/src/commands/monitor.test.ts +191 -0
  65. package/src/commands/monitor.ts +390 -0
  66. package/src/commands/nudge.test.ts +230 -0
  67. package/src/commands/nudge.ts +372 -0
  68. package/src/commands/prime.test.ts +470 -0
  69. package/src/commands/prime.ts +381 -0
  70. package/src/commands/replay.test.ts +741 -0
  71. package/src/commands/replay.ts +360 -0
  72. package/src/commands/run.test.ts +431 -0
  73. package/src/commands/run.ts +351 -0
  74. package/src/commands/sling.test.ts +657 -0
  75. package/src/commands/sling.ts +661 -0
  76. package/src/commands/spec.test.ts +203 -0
  77. package/src/commands/spec.ts +168 -0
  78. package/src/commands/status.test.ts +430 -0
  79. package/src/commands/status.ts +398 -0
  80. package/src/commands/stop.test.ts +420 -0
  81. package/src/commands/stop.ts +151 -0
  82. package/src/commands/supervisor.test.ts +187 -0
  83. package/src/commands/supervisor.ts +535 -0
  84. package/src/commands/trace.test.ts +745 -0
  85. package/src/commands/trace.ts +325 -0
  86. package/src/commands/watch.test.ts +145 -0
  87. package/src/commands/watch.ts +247 -0
  88. package/src/commands/worktree.test.ts +786 -0
  89. package/src/commands/worktree.ts +311 -0
  90. package/src/config.test.ts +822 -0
  91. package/src/config.ts +829 -0
  92. package/src/doctor/agents.test.ts +454 -0
  93. package/src/doctor/agents.ts +396 -0
  94. package/src/doctor/config-check.test.ts +190 -0
  95. package/src/doctor/config-check.ts +183 -0
  96. package/src/doctor/consistency.test.ts +651 -0
  97. package/src/doctor/consistency.ts +294 -0
  98. package/src/doctor/databases.test.ts +290 -0
  99. package/src/doctor/databases.ts +218 -0
  100. package/src/doctor/dependencies.test.ts +184 -0
  101. package/src/doctor/dependencies.ts +175 -0
  102. package/src/doctor/logs.test.ts +251 -0
  103. package/src/doctor/logs.ts +295 -0
  104. package/src/doctor/merge-queue.test.ts +216 -0
  105. package/src/doctor/merge-queue.ts +144 -0
  106. package/src/doctor/structure.test.ts +291 -0
  107. package/src/doctor/structure.ts +198 -0
  108. package/src/doctor/types.ts +37 -0
  109. package/src/doctor/version.test.ts +136 -0
  110. package/src/doctor/version.ts +129 -0
  111. package/src/e2e/init-sling-lifecycle.test.ts +277 -0
  112. package/src/errors.ts +217 -0
  113. package/src/events/store.test.ts +660 -0
  114. package/src/events/store.ts +369 -0
  115. package/src/events/tool-filter.test.ts +330 -0
  116. package/src/events/tool-filter.ts +126 -0
  117. package/src/index.ts +316 -0
  118. package/src/insights/analyzer.test.ts +466 -0
  119. package/src/insights/analyzer.ts +203 -0
  120. package/src/logging/color.test.ts +142 -0
  121. package/src/logging/color.ts +71 -0
  122. package/src/logging/logger.test.ts +813 -0
  123. package/src/logging/logger.ts +266 -0
  124. package/src/logging/reporter.test.ts +259 -0
  125. package/src/logging/reporter.ts +109 -0
  126. package/src/logging/sanitizer.test.ts +190 -0
  127. package/src/logging/sanitizer.ts +57 -0
  128. package/src/mail/broadcast.test.ts +203 -0
  129. package/src/mail/broadcast.ts +92 -0
  130. package/src/mail/client.test.ts +773 -0
  131. package/src/mail/client.ts +223 -0
  132. package/src/mail/store.test.ts +705 -0
  133. package/src/mail/store.ts +387 -0
  134. package/src/merge/queue.test.ts +359 -0
  135. package/src/merge/queue.ts +231 -0
  136. package/src/merge/resolver.test.ts +1345 -0
  137. package/src/merge/resolver.ts +645 -0
  138. package/src/metrics/store.test.ts +667 -0
  139. package/src/metrics/store.ts +445 -0
  140. package/src/metrics/summary.test.ts +398 -0
  141. package/src/metrics/summary.ts +178 -0
  142. package/src/metrics/transcript.test.ts +356 -0
  143. package/src/metrics/transcript.ts +175 -0
  144. package/src/mulch/client.test.ts +671 -0
  145. package/src/mulch/client.ts +332 -0
  146. package/src/sessions/compat.test.ts +280 -0
  147. package/src/sessions/compat.ts +104 -0
  148. package/src/sessions/store.test.ts +873 -0
  149. package/src/sessions/store.ts +494 -0
  150. package/src/test-helpers.test.ts +124 -0
  151. package/src/test-helpers.ts +126 -0
  152. package/src/tracker/beads.ts +56 -0
  153. package/src/tracker/factory.test.ts +80 -0
  154. package/src/tracker/factory.ts +64 -0
  155. package/src/tracker/seeds.ts +182 -0
  156. package/src/tracker/types.ts +52 -0
  157. package/src/types.ts +724 -0
  158. package/src/watchdog/daemon.test.ts +1975 -0
  159. package/src/watchdog/daemon.ts +671 -0
  160. package/src/watchdog/health.test.ts +431 -0
  161. package/src/watchdog/health.ts +264 -0
  162. package/src/watchdog/triage.test.ts +164 -0
  163. package/src/watchdog/triage.ts +179 -0
  164. package/src/worktree/manager.test.ts +439 -0
  165. package/src/worktree/manager.ts +198 -0
  166. package/src/worktree/tmux.test.ts +1009 -0
  167. package/src/worktree/tmux.ts +509 -0
  168. package/templates/CLAUDE.md.tmpl +89 -0
  169. package/templates/hooks.json.tmpl +105 -0
  170. package/templates/overlay.md.tmpl +81 -0
@@ -0,0 +1,535 @@
1
+ /**
2
+ * CLI command: overstory supervisor start|stop|status
3
+ *
4
+ * Manages per-project supervisor agent lifecycle. The supervisor is a persistent
5
+ * agent that runs at the project root (NOT in a worktree), assigned to a specific
6
+ * bead task, and operates at depth 1 (between coordinator and leaf workers).
7
+ *
8
+ * Unlike the coordinator:
9
+ * - Has a bead assignment (required via --task flag)
10
+ * - Has a parent agent (typically "coordinator")
11
+ * - Has depth 1 (default)
12
+ * - Multiple supervisors can run concurrently (distinguished by --name)
13
+ */
14
+
15
+ import { mkdir } from "node:fs/promises";
16
+ import { join } from "node:path";
17
+ import { deployHooks } from "../agents/hooks-deployer.ts";
18
+ import { createIdentity, loadIdentity } from "../agents/identity.ts";
19
+ import { createManifestLoader, resolveModel } from "../agents/manifest.ts";
20
+ import { loadConfig } from "../config.ts";
21
+ import { AgentError, ValidationError } from "../errors.ts";
22
+ import { openSessionStore } from "../sessions/compat.ts";
23
+ import { createTrackerClient, resolveBackend, trackerCliName } from "../tracker/factory.ts";
24
+ import type { AgentSession } from "../types.ts";
25
+ import {
26
+ createSession,
27
+ isSessionAlive,
28
+ killSession,
29
+ sendKeys,
30
+ waitForTuiReady,
31
+ } from "../worktree/tmux.ts";
32
+ import { isRunningAsRoot } from "./sling.ts";
33
+
34
+ /**
35
+ * Build the supervisor startup beacon.
36
+ *
37
+ * @param opts.name - Supervisor agent name
38
+ * @param opts.beadId - Bead task ID
39
+ * @param opts.depth - Hierarchy depth (default 1)
40
+ * @param opts.parent - Parent agent name (default "coordinator")
41
+ */
42
+ export function buildSupervisorBeacon(opts: {
43
+ name: string;
44
+ beadId: string;
45
+ depth: number;
46
+ parent: string;
47
+ trackerCli?: string;
48
+ }): string {
49
+ const cli = opts.trackerCli ?? "bd";
50
+ const timestamp = new Date().toISOString();
51
+ const parts = [
52
+ `[OVERSTORY] ${opts.name} (supervisor) ${timestamp} task:${opts.beadId}`,
53
+ `Depth: ${opts.depth} | Parent: ${opts.parent} | Role: per-project supervisor`,
54
+ `Startup: run mulch prime, check mail (overstory mail check --agent ${opts.name}), read task (${cli} show ${opts.beadId}), then begin supervising`,
55
+ ];
56
+ return parts.join(" — ");
57
+ }
58
+
59
+ /**
60
+ * Parse flags from command args.
61
+ */
62
+ function parseFlags(args: string[]): {
63
+ task: string | null;
64
+ name: string | null;
65
+ parent: string;
66
+ depth: number;
67
+ json: boolean;
68
+ } {
69
+ const flags = {
70
+ task: null as string | null,
71
+ name: null as string | null,
72
+ parent: "coordinator",
73
+ depth: 1,
74
+ json: false,
75
+ };
76
+
77
+ for (let i = 0; i < args.length; i++) {
78
+ const arg = args[i];
79
+ if (arg === "--task" && i + 1 < args.length) {
80
+ const val = args[i + 1];
81
+ if (val !== undefined) {
82
+ flags.task = val;
83
+ }
84
+ i++;
85
+ } else if (arg === "--name" && i + 1 < args.length) {
86
+ const val = args[i + 1];
87
+ if (val !== undefined) {
88
+ flags.name = val;
89
+ }
90
+ i++;
91
+ } else if (arg === "--parent" && i + 1 < args.length) {
92
+ const val = args[i + 1];
93
+ if (val !== undefined) {
94
+ flags.parent = val;
95
+ }
96
+ i++;
97
+ } else if (arg === "--depth" && i + 1 < args.length) {
98
+ const val = args[i + 1];
99
+ if (val !== undefined) {
100
+ flags.depth = Number.parseInt(val, 10);
101
+ }
102
+ i++;
103
+ } else if (arg === "--json") {
104
+ flags.json = true;
105
+ }
106
+ }
107
+
108
+ return flags;
109
+ }
110
+
111
+ /**
112
+ * Start a supervisor agent.
113
+ *
114
+ * 1. Parse flags (--task required, --name required)
115
+ * 2. Load config
116
+ * 3. Validate: name is unique in sessions, bead exists and is workable
117
+ * 4. Check no supervisor with same name is already running
118
+ * 5. Deploy hooks with capability "supervisor"
119
+ * 6. Create identity if first run
120
+ * 7. Spawn tmux session at project root with Claude Code
121
+ * 8. Send startup beacon
122
+ * 9. Record session in SessionStore (sessions.db)
123
+ */
124
+ async function startSupervisor(args: string[]): Promise<void> {
125
+ const flags = parseFlags(args);
126
+
127
+ if (!flags.task) {
128
+ throw new ValidationError("--task <bead-id> is required", {
129
+ field: "task",
130
+ value: flags.task ?? "",
131
+ });
132
+ }
133
+ if (!flags.name) {
134
+ throw new ValidationError("--name <name> is required", {
135
+ field: "name",
136
+ value: flags.name ?? "",
137
+ });
138
+ }
139
+
140
+ if (isRunningAsRoot()) {
141
+ throw new AgentError(
142
+ "Cannot spawn agents as root (UID 0). The claude CLI rejects --dangerously-skip-permissions when run as root, causing the tmux session to die immediately. Run overstory as a non-root user.",
143
+ );
144
+ }
145
+
146
+ const cwd = process.cwd();
147
+ const config = await loadConfig(cwd);
148
+ const projectRoot = config.project.root;
149
+
150
+ // Validate task exists and is workable (open or in_progress)
151
+ const resolvedBackend = await resolveBackend(config.taskTracker.backend, projectRoot);
152
+ const tracker = createTrackerClient(resolvedBackend, projectRoot);
153
+ const issue = await tracker.show(flags.task);
154
+ if (issue.status !== "open" && issue.status !== "in_progress") {
155
+ throw new ValidationError(`Task ${flags.task} is not workable (status: ${issue.status})`, {
156
+ field: "task",
157
+ value: flags.task,
158
+ });
159
+ }
160
+
161
+ // Check for existing supervisor with same name
162
+ const overstoryDir = join(projectRoot, ".overstory");
163
+ const { store } = openSessionStore(overstoryDir);
164
+ try {
165
+ const existing = store.getByName(flags.name);
166
+
167
+ if (
168
+ existing &&
169
+ existing.capability === "supervisor" &&
170
+ existing.state !== "completed" &&
171
+ existing.state !== "zombie"
172
+ ) {
173
+ const alive = await isSessionAlive(existing.tmuxSession);
174
+ if (alive) {
175
+ throw new AgentError(
176
+ `Supervisor '${flags.name}' is already running (tmux: ${existing.tmuxSession}, since: ${existing.startedAt})`,
177
+ { agentName: flags.name },
178
+ );
179
+ }
180
+ // Session recorded but tmux is dead — mark as completed and continue
181
+ store.updateState(flags.name, "completed");
182
+ }
183
+
184
+ // Deploy supervisor-specific hooks to the project root's .claude/ directory.
185
+ await deployHooks(projectRoot, flags.name, "supervisor");
186
+
187
+ // Create supervisor identity if first run
188
+ const identityBaseDir = join(projectRoot, ".overstory", "agents");
189
+ await mkdir(identityBaseDir, { recursive: true });
190
+ const existingIdentity = await loadIdentity(identityBaseDir, flags.name);
191
+ if (!existingIdentity) {
192
+ await createIdentity(identityBaseDir, {
193
+ name: flags.name,
194
+ capability: "supervisor",
195
+ created: new Date().toISOString(),
196
+ sessionsCompleted: 0,
197
+ expertiseDomains: config.mulch.enabled ? config.mulch.domains : [],
198
+ recentTasks: [],
199
+ });
200
+ }
201
+
202
+ // Resolve model from config > manifest > fallback
203
+ const manifestLoader = createManifestLoader(
204
+ join(projectRoot, config.agents.manifestPath),
205
+ join(projectRoot, config.agents.baseDir),
206
+ );
207
+ const manifest = await manifestLoader.load();
208
+ const { model, env } = resolveModel(config, manifest, "supervisor", "opus");
209
+
210
+ // Spawn tmux session at project root with Claude Code (interactive mode).
211
+ // Inject the supervisor base definition via --append-system-prompt.
212
+ const tmuxSession = `overstory-${config.project.name}-supervisor-${flags.name}`;
213
+ const agentDefPath = join(projectRoot, ".overstory", "agent-defs", "supervisor.md");
214
+ const agentDefFile = Bun.file(agentDefPath);
215
+ let claudeCmd = `claude --model ${model} --dangerously-skip-permissions`;
216
+ if (await agentDefFile.exists()) {
217
+ const agentDef = await agentDefFile.text();
218
+ const escaped = agentDef.replace(/'/g, "'\\''");
219
+ claudeCmd += ` --append-system-prompt '${escaped}'`;
220
+ }
221
+ const pid = await createSession(tmuxSession, projectRoot, claudeCmd, {
222
+ ...env,
223
+ OVERSTORY_AGENT_NAME: flags.name,
224
+ });
225
+
226
+ // Wait for Claude Code TUI to render before sending input
227
+ await waitForTuiReady(tmuxSession);
228
+ await Bun.sleep(1_000);
229
+
230
+ const beacon = buildSupervisorBeacon({
231
+ name: flags.name,
232
+ beadId: flags.task,
233
+ depth: flags.depth,
234
+ parent: flags.parent,
235
+ trackerCli: trackerCliName(resolvedBackend),
236
+ });
237
+ await sendKeys(tmuxSession, beacon);
238
+
239
+ // Follow-up Enters with increasing delays to ensure submission
240
+ for (const delay of [1_000, 2_000]) {
241
+ await Bun.sleep(delay);
242
+ await sendKeys(tmuxSession, "");
243
+ }
244
+
245
+ // Record session
246
+ const session: AgentSession = {
247
+ id: `session-${Date.now()}-${flags.name}`,
248
+ agentName: flags.name,
249
+ capability: "supervisor",
250
+ worktreePath: projectRoot, // Supervisor uses project root, not a worktree
251
+ branchName: config.project.canonicalBranch, // Operates on canonical branch
252
+ beadId: flags.task,
253
+ tmuxSession,
254
+ state: "booting",
255
+ pid,
256
+ parentAgent: flags.parent,
257
+ depth: flags.depth,
258
+ runId: null,
259
+ startedAt: new Date().toISOString(),
260
+ lastActivity: new Date().toISOString(),
261
+ escalationLevel: 0,
262
+ stalledSince: null,
263
+ };
264
+
265
+ store.upsert(session);
266
+
267
+ const output = {
268
+ agentName: flags.name,
269
+ capability: "supervisor",
270
+ tmuxSession,
271
+ projectRoot,
272
+ beadId: flags.task,
273
+ parent: flags.parent,
274
+ depth: flags.depth,
275
+ pid,
276
+ };
277
+
278
+ if (flags.json) {
279
+ process.stdout.write(`${JSON.stringify(output)}\n`);
280
+ } else {
281
+ process.stdout.write(`Supervisor '${flags.name}' started\n`);
282
+ process.stdout.write(` Tmux: ${tmuxSession}\n`);
283
+ process.stdout.write(` Root: ${projectRoot}\n`);
284
+ process.stdout.write(` Task: ${flags.task}\n`);
285
+ process.stdout.write(` Parent: ${flags.parent}\n`);
286
+ process.stdout.write(` Depth: ${flags.depth}\n`);
287
+ process.stdout.write(` PID: ${pid}\n`);
288
+ }
289
+ } finally {
290
+ store.close();
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Stop a supervisor agent.
296
+ *
297
+ * 1. Find the active supervisor session by name
298
+ * 2. Kill the tmux session (with process tree cleanup)
299
+ * 3. Mark session as completed in SessionStore
300
+ */
301
+ async function stopSupervisor(args: string[]): Promise<void> {
302
+ const flags = parseFlags(args);
303
+
304
+ if (!flags.name) {
305
+ throw new ValidationError("--name <name> is required", {
306
+ field: "name",
307
+ value: flags.name ?? "",
308
+ });
309
+ }
310
+
311
+ const cwd = process.cwd();
312
+ const config = await loadConfig(cwd);
313
+ const projectRoot = config.project.root;
314
+
315
+ const overstoryDir = join(projectRoot, ".overstory");
316
+ const { store } = openSessionStore(overstoryDir);
317
+ try {
318
+ const session = store.getByName(flags.name);
319
+
320
+ if (
321
+ !session ||
322
+ session.capability !== "supervisor" ||
323
+ session.state === "completed" ||
324
+ session.state === "zombie"
325
+ ) {
326
+ throw new AgentError(`No active supervisor session found for '${flags.name}'`, {
327
+ agentName: flags.name,
328
+ });
329
+ }
330
+
331
+ // Kill tmux session with process tree cleanup
332
+ const alive = await isSessionAlive(session.tmuxSession);
333
+ if (alive) {
334
+ await killSession(session.tmuxSession);
335
+ }
336
+
337
+ // Update session state
338
+ store.updateState(flags.name, "completed");
339
+ store.updateLastActivity(flags.name);
340
+
341
+ if (flags.json) {
342
+ process.stdout.write(`${JSON.stringify({ stopped: true, sessionId: session.id })}\n`);
343
+ } else {
344
+ process.stdout.write(`Supervisor '${flags.name}' stopped (session: ${session.id})\n`);
345
+ }
346
+ } finally {
347
+ store.close();
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Show supervisor status.
353
+ *
354
+ * If --name is provided, show status for that specific supervisor.
355
+ * Otherwise, list all supervisors.
356
+ */
357
+ async function statusSupervisor(args: string[]): Promise<void> {
358
+ const flags = parseFlags(args);
359
+ const cwd = process.cwd();
360
+ const config = await loadConfig(cwd);
361
+ const projectRoot = config.project.root;
362
+
363
+ const overstoryDir = join(projectRoot, ".overstory");
364
+ const { store } = openSessionStore(overstoryDir);
365
+ try {
366
+ if (flags.name) {
367
+ // Show specific supervisor
368
+ const session = store.getByName(flags.name);
369
+
370
+ if (
371
+ !session ||
372
+ session.capability !== "supervisor" ||
373
+ session.state === "completed" ||
374
+ session.state === "zombie"
375
+ ) {
376
+ if (flags.json) {
377
+ process.stdout.write(`${JSON.stringify({ running: false })}\n`);
378
+ } else {
379
+ process.stdout.write(`Supervisor '${flags.name}' is not running\n`);
380
+ }
381
+ return;
382
+ }
383
+
384
+ const alive = await isSessionAlive(session.tmuxSession);
385
+
386
+ // Reconcile state: we already filtered out completed/zombie above,
387
+ // so if tmux is dead this session needs to be marked as zombie.
388
+ if (!alive) {
389
+ store.updateState(flags.name, "zombie");
390
+ store.updateLastActivity(flags.name);
391
+ session.state = "zombie";
392
+ }
393
+
394
+ const status = {
395
+ running: alive,
396
+ sessionId: session.id,
397
+ agentName: session.agentName,
398
+ state: session.state,
399
+ tmuxSession: session.tmuxSession,
400
+ beadId: session.beadId,
401
+ parentAgent: session.parentAgent,
402
+ depth: session.depth,
403
+ pid: session.pid,
404
+ startedAt: session.startedAt,
405
+ lastActivity: session.lastActivity,
406
+ };
407
+
408
+ if (flags.json) {
409
+ process.stdout.write(`${JSON.stringify(status)}\n`);
410
+ } else {
411
+ const stateLabel = alive ? "running" : session.state;
412
+ process.stdout.write(`Supervisor '${flags.name}': ${stateLabel}\n`);
413
+ process.stdout.write(` Session: ${session.id}\n`);
414
+ process.stdout.write(` Tmux: ${session.tmuxSession}\n`);
415
+ process.stdout.write(` Task: ${session.beadId}\n`);
416
+ process.stdout.write(` Parent: ${session.parentAgent}\n`);
417
+ process.stdout.write(` Depth: ${session.depth}\n`);
418
+ process.stdout.write(` PID: ${session.pid}\n`);
419
+ process.stdout.write(` Started: ${session.startedAt}\n`);
420
+ process.stdout.write(` Activity: ${session.lastActivity}\n`);
421
+ }
422
+ } else {
423
+ // List all supervisors
424
+ const allSessions = store.getAll();
425
+ const supervisors = allSessions.filter((s) => s.capability === "supervisor");
426
+
427
+ if (supervisors.length === 0) {
428
+ if (flags.json) {
429
+ process.stdout.write(`${JSON.stringify([])}\n`);
430
+ } else {
431
+ process.stdout.write("No supervisor sessions found\n");
432
+ }
433
+ return;
434
+ }
435
+
436
+ const statuses = await Promise.all(
437
+ supervisors.map(async (session) => {
438
+ const alive = await isSessionAlive(session.tmuxSession);
439
+
440
+ // Reconcile state
441
+ if (!alive && session.state !== "completed" && session.state !== "zombie") {
442
+ store.updateState(session.agentName, "zombie");
443
+ store.updateLastActivity(session.agentName);
444
+ }
445
+
446
+ return {
447
+ agentName: session.agentName,
448
+ running: alive,
449
+ state:
450
+ !alive && session.state !== "completed" && session.state !== "zombie"
451
+ ? ("zombie" as const)
452
+ : session.state,
453
+ tmuxSession: session.tmuxSession,
454
+ beadId: session.beadId,
455
+ parentAgent: session.parentAgent,
456
+ depth: session.depth,
457
+ startedAt: session.startedAt,
458
+ };
459
+ }),
460
+ );
461
+
462
+ if (flags.json) {
463
+ process.stdout.write(`${JSON.stringify(statuses)}\n`);
464
+ } else {
465
+ process.stdout.write("Supervisor sessions:\n");
466
+ for (const status of statuses) {
467
+ const stateLabel = status.running ? "running" : status.state;
468
+ process.stdout.write(
469
+ ` ${status.agentName}: ${stateLabel} (task: ${status.beadId}, parent: ${status.parentAgent})\n`,
470
+ );
471
+ }
472
+ }
473
+ }
474
+ } finally {
475
+ store.close();
476
+ }
477
+ }
478
+
479
+ const SUPERVISOR_HELP = `overstory supervisor — Manage per-project supervisor agents
480
+
481
+ Usage: overstory supervisor <subcommand> [flags]
482
+
483
+ Subcommands:
484
+ start Start a supervisor (spawns Claude Code at project root)
485
+ stop Stop a supervisor (kills tmux session)
486
+ status Show supervisor state
487
+
488
+ Options (start):
489
+ --task <bead-id> Bead task ID (required)
490
+ --name <name> Unique supervisor name (required)
491
+ --parent <agent> Parent agent name (default: "coordinator")
492
+ --depth <n> Hierarchy depth (default: 1)
493
+ --json Output as JSON
494
+
495
+ Options (stop):
496
+ --name <name> Supervisor name to stop (required)
497
+ --json Output as JSON
498
+
499
+ Options (status):
500
+ --name <name> Show specific supervisor (optional, lists all if omitted)
501
+ --json Output as JSON
502
+
503
+ The supervisor runs at the project root (like the coordinator) but is assigned
504
+ to a specific bead task and operates at depth 1. Supervisors can spawn workers
505
+ via overstory sling and coordinate their work.`;
506
+
507
+ /**
508
+ * Entry point for `overstory supervisor <subcommand>`.
509
+ */
510
+ export async function supervisorCommand(args: string[]): Promise<void> {
511
+ if (args.includes("--help") || args.includes("-h") || args.length === 0) {
512
+ process.stdout.write(`${SUPERVISOR_HELP}\n`);
513
+ return;
514
+ }
515
+
516
+ const subcommand = args[0];
517
+ const subArgs = args.slice(1);
518
+
519
+ switch (subcommand) {
520
+ case "start":
521
+ await startSupervisor(subArgs);
522
+ break;
523
+ case "stop":
524
+ await stopSupervisor(subArgs);
525
+ break;
526
+ case "status":
527
+ await statusSupervisor(subArgs);
528
+ break;
529
+ default:
530
+ throw new ValidationError(
531
+ `Unknown supervisor subcommand: ${subcommand}. Run 'overstory supervisor --help' for usage.`,
532
+ { field: "subcommand", value: subcommand },
533
+ );
534
+ }
535
+ }