@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.
Files changed (110) hide show
  1. package/README.md +8 -7
  2. package/package.json +12 -4
  3. package/src/agents/checkpoint.test.ts +2 -2
  4. package/src/agents/hooks-deployer.test.ts +131 -16
  5. package/src/agents/hooks-deployer.ts +33 -1
  6. package/src/agents/identity.test.ts +27 -27
  7. package/src/agents/identity.ts +10 -10
  8. package/src/agents/lifecycle.test.ts +6 -6
  9. package/src/agents/lifecycle.ts +2 -2
  10. package/src/agents/manifest.test.ts +86 -0
  11. package/src/agents/overlay.test.ts +9 -9
  12. package/src/agents/overlay.ts +4 -4
  13. package/src/commands/agents.test.ts +8 -8
  14. package/src/commands/agents.ts +62 -91
  15. package/src/commands/clean.test.ts +36 -51
  16. package/src/commands/clean.ts +28 -49
  17. package/src/commands/completions.ts +14 -0
  18. package/src/commands/coordinator.test.ts +133 -26
  19. package/src/commands/coordinator.ts +101 -64
  20. package/src/commands/costs.test.ts +47 -47
  21. package/src/commands/costs.ts +96 -75
  22. package/src/commands/dashboard.test.ts +2 -2
  23. package/src/commands/dashboard.ts +75 -95
  24. package/src/commands/doctor.test.ts +2 -2
  25. package/src/commands/doctor.ts +92 -79
  26. package/src/commands/errors.test.ts +2 -2
  27. package/src/commands/errors.ts +56 -50
  28. package/src/commands/feed.test.ts +2 -2
  29. package/src/commands/feed.ts +86 -83
  30. package/src/commands/group.ts +167 -177
  31. package/src/commands/hooks.test.ts +2 -2
  32. package/src/commands/hooks.ts +52 -42
  33. package/src/commands/init.test.ts +19 -19
  34. package/src/commands/init.ts +7 -16
  35. package/src/commands/inspect.test.ts +18 -18
  36. package/src/commands/inspect.ts +55 -58
  37. package/src/commands/log.test.ts +26 -31
  38. package/src/commands/log.ts +97 -91
  39. package/src/commands/logs.test.ts +1 -1
  40. package/src/commands/logs.ts +101 -104
  41. package/src/commands/mail.test.ts +5 -5
  42. package/src/commands/mail.ts +157 -169
  43. package/src/commands/merge.test.ts +28 -66
  44. package/src/commands/merge.ts +21 -51
  45. package/src/commands/metrics.test.ts +8 -8
  46. package/src/commands/metrics.ts +34 -35
  47. package/src/commands/monitor.test.ts +3 -3
  48. package/src/commands/monitor.ts +57 -62
  49. package/src/commands/nudge.test.ts +1 -1
  50. package/src/commands/nudge.ts +41 -89
  51. package/src/commands/prime.test.ts +19 -51
  52. package/src/commands/prime.ts +13 -50
  53. package/src/commands/replay.test.ts +2 -2
  54. package/src/commands/replay.ts +79 -86
  55. package/src/commands/run.test.ts +1 -1
  56. package/src/commands/run.ts +97 -77
  57. package/src/commands/sling.test.ts +201 -5
  58. package/src/commands/sling.ts +37 -64
  59. package/src/commands/spec.test.ts +14 -40
  60. package/src/commands/spec.ts +32 -101
  61. package/src/commands/status.test.ts +97 -1
  62. package/src/commands/status.ts +63 -58
  63. package/src/commands/stop.test.ts +22 -40
  64. package/src/commands/stop.ts +18 -33
  65. package/src/commands/supervisor.test.ts +12 -14
  66. package/src/commands/supervisor.ts +144 -165
  67. package/src/commands/trace.test.ts +15 -15
  68. package/src/commands/trace.ts +59 -82
  69. package/src/commands/watch.test.ts +2 -2
  70. package/src/commands/watch.ts +38 -45
  71. package/src/commands/worktree.test.ts +213 -37
  72. package/src/commands/worktree.ts +110 -55
  73. package/src/config.test.ts +96 -0
  74. package/src/doctor/consistency.test.ts +14 -14
  75. package/src/doctor/databases.test.ts +22 -2
  76. package/src/doctor/databases.ts +16 -0
  77. package/src/doctor/dependencies.test.ts +55 -1
  78. package/src/doctor/dependencies.ts +113 -18
  79. package/src/doctor/merge-queue.test.ts +4 -4
  80. package/src/e2e/init-sling-lifecycle.test.ts +8 -8
  81. package/src/errors.ts +1 -1
  82. package/src/index.ts +223 -213
  83. package/src/logging/color.test.ts +74 -91
  84. package/src/logging/color.ts +52 -46
  85. package/src/logging/reporter.test.ts +10 -10
  86. package/src/logging/reporter.ts +6 -5
  87. package/src/mail/broadcast.test.ts +1 -1
  88. package/src/mail/client.test.ts +6 -6
  89. package/src/mail/store.test.ts +3 -3
  90. package/src/merge/queue.test.ts +73 -7
  91. package/src/merge/queue.ts +17 -2
  92. package/src/merge/resolver.test.ts +159 -7
  93. package/src/merge/resolver.ts +46 -2
  94. package/src/metrics/store.test.ts +44 -44
  95. package/src/metrics/store.ts +2 -2
  96. package/src/metrics/summary.test.ts +35 -35
  97. package/src/mulch/client.test.ts +1 -1
  98. package/src/schema-consistency.test.ts +239 -0
  99. package/src/sessions/compat.test.ts +3 -3
  100. package/src/sessions/compat.ts +2 -2
  101. package/src/sessions/store.test.ts +41 -4
  102. package/src/sessions/store.ts +13 -2
  103. package/src/types.ts +14 -14
  104. package/src/watchdog/daemon.test.ts +10 -10
  105. package/src/watchdog/daemon.ts +1 -1
  106. package/src/watchdog/health.test.ts +1 -1
  107. package/src/worktree/manager.test.ts +20 -20
  108. package/src/worktree/manager.ts +120 -4
  109. package/src/worktree/tmux.test.ts +98 -9
  110. package/src/worktree/tmux.ts +18 -0
@@ -14,6 +14,7 @@
14
14
 
15
15
  import { mkdir } 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";
@@ -35,13 +36,13 @@ import { isRunningAsRoot } from "./sling.ts";
35
36
  * Build the supervisor startup beacon.
36
37
  *
37
38
  * @param opts.name - Supervisor agent name
38
- * @param opts.beadId - Bead task ID
39
+ * @param opts.taskId - Bead task ID
39
40
  * @param opts.depth - Hierarchy depth (default 1)
40
41
  * @param opts.parent - Parent agent name (default "coordinator")
41
42
  */
42
43
  export function buildSupervisorBeacon(opts: {
43
44
  name: string;
44
- beadId: string;
45
+ taskId: string;
45
46
  depth: number;
46
47
  parent: string;
47
48
  trackerCli?: string;
@@ -49,65 +50,13 @@ export function buildSupervisorBeacon(opts: {
49
50
  const cli = opts.trackerCli ?? "bd";
50
51
  const timestamp = new Date().toISOString();
51
52
  const parts = [
52
- `[OVERSTORY] ${opts.name} (supervisor) ${timestamp} task:${opts.beadId}`,
53
+ `[OVERSTORY] ${opts.name} (supervisor) ${timestamp} task:${opts.taskId}`,
53
54
  `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
+ `Startup: run mulch prime, check mail (overstory mail check --agent ${opts.name}), read task (${cli} show ${opts.taskId}), then begin supervising`,
55
56
  ];
56
57
  return parts.join(" — ");
57
58
  }
58
59
 
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
60
  /**
112
61
  * Start a supervisor agent.
113
62
  *
@@ -121,19 +70,23 @@ function parseFlags(args: string[]): {
121
70
  * 8. Send startup beacon
122
71
  * 9. Record session in SessionStore (sessions.db)
123
72
  */
124
- async function startSupervisor(args: string[]): Promise<void> {
125
- const flags = parseFlags(args);
126
-
127
- if (!flags.task) {
73
+ async function startSupervisor(opts: {
74
+ task: string;
75
+ name: string;
76
+ parent: string;
77
+ depth: number;
78
+ json: boolean;
79
+ }): Promise<void> {
80
+ if (!opts.task) {
128
81
  throw new ValidationError("--task <bead-id> is required", {
129
82
  field: "task",
130
- value: flags.task ?? "",
83
+ value: opts.task,
131
84
  });
132
85
  }
133
- if (!flags.name) {
86
+ if (!opts.name) {
134
87
  throw new ValidationError("--name <name> is required", {
135
88
  field: "name",
136
- value: flags.name ?? "",
89
+ value: opts.name,
137
90
  });
138
91
  }
139
92
 
@@ -150,11 +103,11 @@ async function startSupervisor(args: string[]): Promise<void> {
150
103
  // Validate task exists and is workable (open or in_progress)
151
104
  const resolvedBackend = await resolveBackend(config.taskTracker.backend, projectRoot);
152
105
  const tracker = createTrackerClient(resolvedBackend, projectRoot);
153
- const issue = await tracker.show(flags.task);
106
+ const issue = await tracker.show(opts.task);
154
107
  if (issue.status !== "open" && issue.status !== "in_progress") {
155
- throw new ValidationError(`Task ${flags.task} is not workable (status: ${issue.status})`, {
108
+ throw new ValidationError(`Task ${opts.task} is not workable (status: ${issue.status})`, {
156
109
  field: "task",
157
- value: flags.task,
110
+ value: opts.task,
158
111
  });
159
112
  }
160
113
 
@@ -162,7 +115,7 @@ async function startSupervisor(args: string[]): Promise<void> {
162
115
  const overstoryDir = join(projectRoot, ".overstory");
163
116
  const { store } = openSessionStore(overstoryDir);
164
117
  try {
165
- const existing = store.getByName(flags.name);
118
+ const existing = store.getByName(opts.name);
166
119
 
167
120
  if (
168
121
  existing &&
@@ -173,24 +126,24 @@ async function startSupervisor(args: string[]): Promise<void> {
173
126
  const alive = await isSessionAlive(existing.tmuxSession);
174
127
  if (alive) {
175
128
  throw new AgentError(
176
- `Supervisor '${flags.name}' is already running (tmux: ${existing.tmuxSession}, since: ${existing.startedAt})`,
177
- { agentName: flags.name },
129
+ `Supervisor '${opts.name}' is already running (tmux: ${existing.tmuxSession}, since: ${existing.startedAt})`,
130
+ { agentName: opts.name },
178
131
  );
179
132
  }
180
133
  // Session recorded but tmux is dead — mark as completed and continue
181
- store.updateState(flags.name, "completed");
134
+ store.updateState(opts.name, "completed");
182
135
  }
183
136
 
184
137
  // Deploy supervisor-specific hooks to the project root's .claude/ directory.
185
- await deployHooks(projectRoot, flags.name, "supervisor");
138
+ await deployHooks(projectRoot, opts.name, "supervisor");
186
139
 
187
140
  // Create supervisor identity if first run
188
141
  const identityBaseDir = join(projectRoot, ".overstory", "agents");
189
142
  await mkdir(identityBaseDir, { recursive: true });
190
- const existingIdentity = await loadIdentity(identityBaseDir, flags.name);
143
+ const existingIdentity = await loadIdentity(identityBaseDir, opts.name);
191
144
  if (!existingIdentity) {
192
145
  await createIdentity(identityBaseDir, {
193
- name: flags.name,
146
+ name: opts.name,
194
147
  capability: "supervisor",
195
148
  created: new Date().toISOString(),
196
149
  sessionsCompleted: 0,
@@ -209,7 +162,7 @@ async function startSupervisor(args: string[]): Promise<void> {
209
162
 
210
163
  // Spawn tmux session at project root with Claude Code (interactive mode).
211
164
  // Inject the supervisor base definition via --append-system-prompt.
212
- const tmuxSession = `overstory-${config.project.name}-supervisor-${flags.name}`;
165
+ const tmuxSession = `overstory-${config.project.name}-supervisor-${opts.name}`;
213
166
  const agentDefPath = join(projectRoot, ".overstory", "agent-defs", "supervisor.md");
214
167
  const agentDefFile = Bun.file(agentDefPath);
215
168
  let claudeCmd = `claude --model ${model} --dangerously-skip-permissions`;
@@ -220,7 +173,7 @@ async function startSupervisor(args: string[]): Promise<void> {
220
173
  }
221
174
  const pid = await createSession(tmuxSession, projectRoot, claudeCmd, {
222
175
  ...env,
223
- OVERSTORY_AGENT_NAME: flags.name,
176
+ OVERSTORY_AGENT_NAME: opts.name,
224
177
  });
225
178
 
226
179
  // Wait for Claude Code TUI to render before sending input
@@ -228,10 +181,10 @@ async function startSupervisor(args: string[]): Promise<void> {
228
181
  await Bun.sleep(1_000);
229
182
 
230
183
  const beacon = buildSupervisorBeacon({
231
- name: flags.name,
232
- beadId: flags.task,
233
- depth: flags.depth,
234
- parent: flags.parent,
184
+ name: opts.name,
185
+ taskId: opts.task,
186
+ depth: opts.depth,
187
+ parent: opts.parent,
235
188
  trackerCli: trackerCliName(resolvedBackend),
236
189
  });
237
190
  await sendKeys(tmuxSession, beacon);
@@ -244,17 +197,17 @@ async function startSupervisor(args: string[]): Promise<void> {
244
197
 
245
198
  // Record session
246
199
  const session: AgentSession = {
247
- id: `session-${Date.now()}-${flags.name}`,
248
- agentName: flags.name,
200
+ id: `session-${Date.now()}-${opts.name}`,
201
+ agentName: opts.name,
249
202
  capability: "supervisor",
250
203
  worktreePath: projectRoot, // Supervisor uses project root, not a worktree
251
204
  branchName: config.project.canonicalBranch, // Operates on canonical branch
252
- beadId: flags.task,
205
+ taskId: opts.task,
253
206
  tmuxSession,
254
207
  state: "booting",
255
208
  pid,
256
- parentAgent: flags.parent,
257
- depth: flags.depth,
209
+ parentAgent: opts.parent,
210
+ depth: opts.depth,
258
211
  runId: null,
259
212
  startedAt: new Date().toISOString(),
260
213
  lastActivity: new Date().toISOString(),
@@ -265,25 +218,25 @@ async function startSupervisor(args: string[]): Promise<void> {
265
218
  store.upsert(session);
266
219
 
267
220
  const output = {
268
- agentName: flags.name,
221
+ agentName: opts.name,
269
222
  capability: "supervisor",
270
223
  tmuxSession,
271
224
  projectRoot,
272
- beadId: flags.task,
273
- parent: flags.parent,
274
- depth: flags.depth,
225
+ taskId: opts.task,
226
+ parent: opts.parent,
227
+ depth: opts.depth,
275
228
  pid,
276
229
  };
277
230
 
278
- if (flags.json) {
231
+ if (opts.json) {
279
232
  process.stdout.write(`${JSON.stringify(output)}\n`);
280
233
  } else {
281
- process.stdout.write(`Supervisor '${flags.name}' started\n`);
234
+ process.stdout.write(`Supervisor '${opts.name}' started\n`);
282
235
  process.stdout.write(` Tmux: ${tmuxSession}\n`);
283
236
  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`);
237
+ process.stdout.write(` Task: ${opts.task}\n`);
238
+ process.stdout.write(` Parent: ${opts.parent}\n`);
239
+ process.stdout.write(` Depth: ${opts.depth}\n`);
287
240
  process.stdout.write(` PID: ${pid}\n`);
288
241
  }
289
242
  } finally {
@@ -298,13 +251,11 @@ async function startSupervisor(args: string[]): Promise<void> {
298
251
  * 2. Kill the tmux session (with process tree cleanup)
299
252
  * 3. Mark session as completed in SessionStore
300
253
  */
301
- async function stopSupervisor(args: string[]): Promise<void> {
302
- const flags = parseFlags(args);
303
-
304
- if (!flags.name) {
254
+ async function stopSupervisor(opts: { name: string; json: boolean }): Promise<void> {
255
+ if (!opts.name) {
305
256
  throw new ValidationError("--name <name> is required", {
306
257
  field: "name",
307
- value: flags.name ?? "",
258
+ value: opts.name,
308
259
  });
309
260
  }
310
261
 
@@ -315,7 +266,7 @@ async function stopSupervisor(args: string[]): Promise<void> {
315
266
  const overstoryDir = join(projectRoot, ".overstory");
316
267
  const { store } = openSessionStore(overstoryDir);
317
268
  try {
318
- const session = store.getByName(flags.name);
269
+ const session = store.getByName(opts.name);
319
270
 
320
271
  if (
321
272
  !session ||
@@ -323,8 +274,8 @@ async function stopSupervisor(args: string[]): Promise<void> {
323
274
  session.state === "completed" ||
324
275
  session.state === "zombie"
325
276
  ) {
326
- throw new AgentError(`No active supervisor session found for '${flags.name}'`, {
327
- agentName: flags.name,
277
+ throw new AgentError(`No active supervisor session found for '${opts.name}'`, {
278
+ agentName: opts.name,
328
279
  });
329
280
  }
330
281
 
@@ -335,13 +286,13 @@ async function stopSupervisor(args: string[]): Promise<void> {
335
286
  }
336
287
 
337
288
  // Update session state
338
- store.updateState(flags.name, "completed");
339
- store.updateLastActivity(flags.name);
289
+ store.updateState(opts.name, "completed");
290
+ store.updateLastActivity(opts.name);
340
291
 
341
- if (flags.json) {
292
+ if (opts.json) {
342
293
  process.stdout.write(`${JSON.stringify({ stopped: true, sessionId: session.id })}\n`);
343
294
  } else {
344
- process.stdout.write(`Supervisor '${flags.name}' stopped (session: ${session.id})\n`);
295
+ process.stdout.write(`Supervisor '${opts.name}' stopped (session: ${session.id})\n`);
345
296
  }
346
297
  } finally {
347
298
  store.close();
@@ -354,8 +305,7 @@ async function stopSupervisor(args: string[]): Promise<void> {
354
305
  * If --name is provided, show status for that specific supervisor.
355
306
  * Otherwise, list all supervisors.
356
307
  */
357
- async function statusSupervisor(args: string[]): Promise<void> {
358
- const flags = parseFlags(args);
308
+ async function statusSupervisor(opts: { name?: string; json: boolean }): Promise<void> {
359
309
  const cwd = process.cwd();
360
310
  const config = await loadConfig(cwd);
361
311
  const projectRoot = config.project.root;
@@ -363,9 +313,9 @@ async function statusSupervisor(args: string[]): Promise<void> {
363
313
  const overstoryDir = join(projectRoot, ".overstory");
364
314
  const { store } = openSessionStore(overstoryDir);
365
315
  try {
366
- if (flags.name) {
316
+ if (opts.name) {
367
317
  // Show specific supervisor
368
- const session = store.getByName(flags.name);
318
+ const session = store.getByName(opts.name);
369
319
 
370
320
  if (
371
321
  !session ||
@@ -373,10 +323,10 @@ async function statusSupervisor(args: string[]): Promise<void> {
373
323
  session.state === "completed" ||
374
324
  session.state === "zombie"
375
325
  ) {
376
- if (flags.json) {
326
+ if (opts.json) {
377
327
  process.stdout.write(`${JSON.stringify({ running: false })}\n`);
378
328
  } else {
379
- process.stdout.write(`Supervisor '${flags.name}' is not running\n`);
329
+ process.stdout.write(`Supervisor '${opts.name}' is not running\n`);
380
330
  }
381
331
  return;
382
332
  }
@@ -386,8 +336,8 @@ async function statusSupervisor(args: string[]): Promise<void> {
386
336
  // Reconcile state: we already filtered out completed/zombie above,
387
337
  // so if tmux is dead this session needs to be marked as zombie.
388
338
  if (!alive) {
389
- store.updateState(flags.name, "zombie");
390
- store.updateLastActivity(flags.name);
339
+ store.updateState(opts.name, "zombie");
340
+ store.updateLastActivity(opts.name);
391
341
  session.state = "zombie";
392
342
  }
393
343
 
@@ -397,7 +347,7 @@ async function statusSupervisor(args: string[]): Promise<void> {
397
347
  agentName: session.agentName,
398
348
  state: session.state,
399
349
  tmuxSession: session.tmuxSession,
400
- beadId: session.beadId,
350
+ taskId: session.taskId,
401
351
  parentAgent: session.parentAgent,
402
352
  depth: session.depth,
403
353
  pid: session.pid,
@@ -405,14 +355,14 @@ async function statusSupervisor(args: string[]): Promise<void> {
405
355
  lastActivity: session.lastActivity,
406
356
  };
407
357
 
408
- if (flags.json) {
358
+ if (opts.json) {
409
359
  process.stdout.write(`${JSON.stringify(status)}\n`);
410
360
  } else {
411
361
  const stateLabel = alive ? "running" : session.state;
412
- process.stdout.write(`Supervisor '${flags.name}': ${stateLabel}\n`);
362
+ process.stdout.write(`Supervisor '${opts.name}': ${stateLabel}\n`);
413
363
  process.stdout.write(` Session: ${session.id}\n`);
414
364
  process.stdout.write(` Tmux: ${session.tmuxSession}\n`);
415
- process.stdout.write(` Task: ${session.beadId}\n`);
365
+ process.stdout.write(` Task: ${session.taskId}\n`);
416
366
  process.stdout.write(` Parent: ${session.parentAgent}\n`);
417
367
  process.stdout.write(` Depth: ${session.depth}\n`);
418
368
  process.stdout.write(` PID: ${session.pid}\n`);
@@ -425,7 +375,7 @@ async function statusSupervisor(args: string[]): Promise<void> {
425
375
  const supervisors = allSessions.filter((s) => s.capability === "supervisor");
426
376
 
427
377
  if (supervisors.length === 0) {
428
- if (flags.json) {
378
+ if (opts.json) {
429
379
  process.stdout.write(`${JSON.stringify([])}\n`);
430
380
  } else {
431
381
  process.stdout.write("No supervisor sessions found\n");
@@ -451,7 +401,7 @@ async function statusSupervisor(args: string[]): Promise<void> {
451
401
  ? ("zombie" as const)
452
402
  : session.state,
453
403
  tmuxSession: session.tmuxSession,
454
- beadId: session.beadId,
404
+ taskId: session.taskId,
455
405
  parentAgent: session.parentAgent,
456
406
  depth: session.depth,
457
407
  startedAt: session.startedAt,
@@ -459,14 +409,14 @@ async function statusSupervisor(args: string[]): Promise<void> {
459
409
  }),
460
410
  );
461
411
 
462
- if (flags.json) {
412
+ if (opts.json) {
463
413
  process.stdout.write(`${JSON.stringify(statuses)}\n`);
464
414
  } else {
465
415
  process.stdout.write("Supervisor sessions:\n");
466
416
  for (const status of statuses) {
467
417
  const stateLabel = status.running ? "running" : status.state;
468
418
  process.stdout.write(
469
- ` ${status.agentName}: ${stateLabel} (task: ${status.beadId}, parent: ${status.parentAgent})\n`,
419
+ ` ${status.agentName}: ${stateLabel} (task: ${status.taskId}, parent: ${status.parentAgent})\n`,
470
420
  );
471
421
  }
472
422
  }
@@ -476,60 +426,89 @@ async function statusSupervisor(args: string[]): Promise<void> {
476
426
  }
477
427
  }
478
428
 
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
429
+ /**
430
+ * Create the Commander command for `overstory supervisor`.
431
+ */
432
+ export function createSupervisorCommand(): Command {
433
+ const cmd = new Command("supervisor").description("Manage per-project supervisor agents");
434
+
435
+ cmd
436
+ .command("start")
437
+ .description("Start a supervisor (spawns Claude Code at project root)")
438
+ .requiredOption("--task <bead-id>", "Bead task ID (required)")
439
+ .requiredOption("--name <name>", "Unique supervisor name (required)")
440
+ .option("--parent <agent>", "Parent agent name", "coordinator")
441
+ .option("--depth <n>", "Hierarchy depth", "1")
442
+ .option("--json", "Output as JSON")
443
+ .action(
444
+ async (opts: {
445
+ task: string;
446
+ name: string;
447
+ parent: string;
448
+ depth: string;
449
+ json?: boolean;
450
+ }) => {
451
+ await startSupervisor({
452
+ task: opts.task,
453
+ name: opts.name,
454
+ parent: opts.parent,
455
+ depth: Number.parseInt(opts.depth, 10),
456
+ json: opts.json ?? false,
457
+ });
458
+ },
459
+ );
494
460
 
495
- Options (stop):
496
- --name <name> Supervisor name to stop (required)
497
- --json Output as JSON
461
+ cmd
462
+ .command("stop")
463
+ .description("Stop a supervisor (kills tmux session)")
464
+ .requiredOption("--name <name>", "Supervisor name to stop (required)")
465
+ .option("--json", "Output as JSON")
466
+ .action(async (opts: { name: string; json?: boolean }) => {
467
+ await stopSupervisor({ name: opts.name, json: opts.json ?? false });
468
+ });
498
469
 
499
- Options (status):
500
- --name <name> Show specific supervisor (optional, lists all if omitted)
501
- --json Output as JSON
470
+ cmd
471
+ .command("status")
472
+ .description("Show supervisor state")
473
+ .option("--name <name>", "Show specific supervisor (optional, lists all if omitted)")
474
+ .option("--json", "Output as JSON")
475
+ .action(async (opts: { name?: string; json?: boolean }) => {
476
+ await statusSupervisor({ name: opts.name, json: opts.json ?? false });
477
+ });
502
478
 
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.`;
479
+ return cmd;
480
+ }
506
481
 
507
482
  /**
508
483
  * Entry point for `overstory supervisor <subcommand>`.
509
484
  */
510
485
  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`);
486
+ const cmd = createSupervisorCommand();
487
+ cmd.exitOverride();
488
+ cmd.configureOutput({ writeErr: () => {} });
489
+ for (const sub of cmd.commands) {
490
+ sub.exitOverride();
491
+ sub.configureOutput({ writeErr: () => {} });
492
+ }
493
+
494
+ if (args.length === 0) {
495
+ process.stdout.write(cmd.helpInformation());
513
496
  return;
514
497
  }
515
498
 
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
- );
499
+ try {
500
+ await cmd.parseAsync(args, { from: "user" });
501
+ } catch (err: unknown) {
502
+ if (err && typeof err === "object" && "code" in err) {
503
+ const code = (err as { code: string }).code;
504
+ if (code === "commander.helpDisplayed" || code === "commander.version") {
505
+ return;
506
+ }
507
+ if (code === "commander.unknownCommand") {
508
+ const message = err instanceof Error ? err.message : String(err);
509
+ throw new ValidationError(message, { field: "subcommand" });
510
+ }
511
+ }
512
+ throw err;
534
513
  }
535
514
  }
@@ -79,7 +79,7 @@ describe("traceCommand", () => {
79
79
  await traceCommand(["--help"]);
80
80
  const out = output();
81
81
 
82
- expect(out).toContain("overstory trace");
82
+ expect(out).toContain("trace");
83
83
  expect(out).toContain("<target>");
84
84
  expect(out).toContain("--json");
85
85
  expect(out).toContain("--since");
@@ -91,29 +91,29 @@ describe("traceCommand", () => {
91
91
  await traceCommand(["-h"]);
92
92
  const out = output();
93
93
 
94
- expect(out).toContain("overstory trace");
94
+ expect(out).toContain("trace");
95
95
  });
96
96
  });
97
97
 
98
98
  // === Argument parsing ===
99
99
 
100
100
  describe("argument parsing", () => {
101
- test("missing target throws ValidationError", async () => {
102
- await expect(traceCommand([])).rejects.toThrow(ValidationError);
101
+ test("missing target throws an error", async () => {
102
+ await expect(traceCommand([])).rejects.toThrow();
103
103
  });
104
104
 
105
- test("missing target error mentions usage", async () => {
105
+ test("missing target error mentions the argument name", async () => {
106
106
  try {
107
107
  await traceCommand([]);
108
108
  expect.unreachable("should have thrown");
109
109
  } catch (err) {
110
- expect(err).toBeInstanceOf(ValidationError);
111
- expect((err as ValidationError).message).toContain("Missing target");
110
+ expect(err).toBeInstanceOf(Error);
111
+ expect((err as Error).message).toContain("target");
112
112
  }
113
113
  });
114
114
 
115
- test("only flags with no target throws ValidationError", async () => {
116
- await expect(traceCommand(["--json"])).rejects.toThrow(ValidationError);
115
+ test("only flags with no target throws an error", async () => {
116
+ await expect(traceCommand(["--json"])).rejects.toThrow();
117
117
  });
118
118
 
119
119
  test("--limit with non-numeric value throws ValidationError", async () => {
@@ -509,7 +509,7 @@ describe("traceCommand", () => {
509
509
  // === Target resolution ===
510
510
 
511
511
  describe("target resolution", () => {
512
- test("agent name is used as-is when not a bead ID pattern", async () => {
512
+ test("agent name is used as-is when not a task ID pattern", async () => {
513
513
  const dbPath = join(tempDir, ".overstory", "events.db");
514
514
  const store = createEventStore(dbPath);
515
515
  store.insert(makeEvent({ agentName: "my-custom-agent" }));
@@ -523,8 +523,8 @@ describe("traceCommand", () => {
523
523
  expect(parsed[0]?.agentName).toBe("my-custom-agent");
524
524
  });
525
525
 
526
- test("bead ID pattern is detected and resolved to agent name via SessionStore", async () => {
527
- // Create a session that maps bead ID to agent name
526
+ test("task ID pattern is detected and resolved to agent name via SessionStore", async () => {
527
+ // Create a session that maps task ID to agent name
528
528
  const sessDbPath = join(tempDir, ".overstory", "sessions.db");
529
529
  const sessionStore = createSessionStore(sessDbPath);
530
530
  sessionStore.upsert({
@@ -533,7 +533,7 @@ describe("traceCommand", () => {
533
533
  capability: "builder",
534
534
  worktreePath: "/tmp/wt",
535
535
  branchName: "feat/task",
536
- beadId: "overstory-rj1k",
536
+ taskId: "overstory-rj1k",
537
537
  tmuxSession: "tmux-001",
538
538
  state: "completed",
539
539
  pid: null,
@@ -561,7 +561,7 @@ describe("traceCommand", () => {
561
561
  expect(parsed[0]?.agentName).toBe("builder-for-task");
562
562
  });
563
563
 
564
- test("unresolved bead ID falls back to using bead ID as agent name", async () => {
564
+ test("unresolved task ID falls back to using task ID as agent name", async () => {
565
565
  // Create sessions.db but with no matching bead
566
566
  const sessDbPath = join(tempDir, ".overstory", "sessions.db");
567
567
  const sessionStore = createSessionStore(sessDbPath);
@@ -579,7 +579,7 @@ describe("traceCommand", () => {
579
579
  expect(parsed).toEqual([]);
580
580
  });
581
581
 
582
- test("short agent names without bead pattern are not resolved as bead IDs", async () => {
582
+ test("short agent names without bead pattern are not resolved as task IDs", async () => {
583
583
  const dbPath = join(tempDir, ".overstory", "events.db");
584
584
  const store = createEventStore(dbPath);
585
585
  store.insert(makeEvent({ agentName: "scout" }));