@os-eco/overstory-cli 0.6.1 → 0.6.4

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 (80) hide show
  1. package/README.md +7 -6
  2. package/package.json +12 -4
  3. package/src/agents/hooks-deployer.test.ts +94 -16
  4. package/src/agents/hooks-deployer.ts +18 -0
  5. package/src/agents/manifest.test.ts +86 -0
  6. package/src/commands/agents.test.ts +3 -3
  7. package/src/commands/agents.ts +59 -88
  8. package/src/commands/clean.test.ts +31 -46
  9. package/src/commands/clean.ts +28 -49
  10. package/src/commands/completions.ts +14 -0
  11. package/src/commands/coordinator.test.ts +131 -24
  12. package/src/commands/coordinator.ts +100 -63
  13. package/src/commands/costs.test.ts +2 -2
  14. package/src/commands/costs.ts +96 -75
  15. package/src/commands/dashboard.test.ts +2 -2
  16. package/src/commands/dashboard.ts +73 -93
  17. package/src/commands/doctor.test.ts +2 -2
  18. package/src/commands/doctor.ts +92 -79
  19. package/src/commands/errors.test.ts +2 -2
  20. package/src/commands/errors.ts +56 -50
  21. package/src/commands/feed.test.ts +2 -2
  22. package/src/commands/feed.ts +86 -83
  23. package/src/commands/group.ts +167 -177
  24. package/src/commands/hooks.test.ts +2 -2
  25. package/src/commands/hooks.ts +52 -42
  26. package/src/commands/init.test.ts +19 -19
  27. package/src/commands/init.ts +7 -16
  28. package/src/commands/inspect.test.ts +2 -2
  29. package/src/commands/inspect.ts +54 -57
  30. package/src/commands/log.test.ts +5 -10
  31. package/src/commands/log.ts +90 -84
  32. package/src/commands/logs.test.ts +1 -1
  33. package/src/commands/logs.ts +101 -104
  34. package/src/commands/mail.ts +157 -169
  35. package/src/commands/merge.test.ts +20 -58
  36. package/src/commands/merge.ts +13 -43
  37. package/src/commands/metrics.test.ts +2 -2
  38. package/src/commands/metrics.ts +33 -34
  39. package/src/commands/monitor.test.ts +3 -3
  40. package/src/commands/monitor.ts +56 -61
  41. package/src/commands/nudge.ts +41 -89
  42. package/src/commands/prime.test.ts +15 -47
  43. package/src/commands/prime.ts +7 -44
  44. package/src/commands/replay.test.ts +2 -2
  45. package/src/commands/replay.ts +79 -86
  46. package/src/commands/run.ts +97 -77
  47. package/src/commands/sling.test.ts +196 -0
  48. package/src/commands/sling.ts +24 -54
  49. package/src/commands/spec.test.ts +13 -39
  50. package/src/commands/spec.ts +30 -99
  51. package/src/commands/status.ts +46 -42
  52. package/src/commands/stop.test.ts +21 -39
  53. package/src/commands/stop.ts +18 -33
  54. package/src/commands/supervisor.test.ts +3 -5
  55. package/src/commands/supervisor.ts +136 -157
  56. package/src/commands/trace.test.ts +9 -9
  57. package/src/commands/trace.ts +54 -77
  58. package/src/commands/watch.test.ts +2 -2
  59. package/src/commands/watch.ts +38 -45
  60. package/src/commands/worktree.test.ts +8 -8
  61. package/src/commands/worktree.ts +63 -46
  62. package/src/config.test.ts +96 -0
  63. package/src/doctor/databases.test.ts +22 -2
  64. package/src/doctor/databases.ts +16 -0
  65. package/src/doctor/dependencies.test.ts +55 -1
  66. package/src/doctor/dependencies.ts +113 -18
  67. package/src/e2e/init-sling-lifecycle.test.ts +6 -6
  68. package/src/index.ts +223 -213
  69. package/src/logging/color.test.ts +74 -91
  70. package/src/logging/color.ts +52 -46
  71. package/src/logging/reporter.test.ts +10 -10
  72. package/src/logging/reporter.ts +6 -5
  73. package/src/merge/queue.test.ts +66 -0
  74. package/src/merge/queue.ts +15 -0
  75. package/src/schema-consistency.test.ts +239 -0
  76. package/src/sessions/compat.ts +1 -1
  77. package/src/sessions/store.test.ts +37 -0
  78. package/src/sessions/store.ts +11 -0
  79. package/src/worktree/tmux.test.ts +98 -9
  80. 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";
@@ -56,58 +57,6 @@ export function buildSupervisorBeacon(opts: {
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
+ beadId: 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
+ beadId: 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
+ beadId: 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
 
@@ -405,11 +355,11 @@ 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
365
  process.stdout.write(` Task: ${session.beadId}\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");
@@ -459,7 +409,7 @@ 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");
@@ -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 () => {