@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
@@ -23,7 +23,7 @@ describe("lifecycle", () => {
23
23
  agentsDir,
24
24
  agentName: "builder-1",
25
25
  sessionId: "session-100",
26
- beadId: "overstory-xyz1",
26
+ taskId: "overstory-xyz1",
27
27
  reason: "compaction",
28
28
  progressSummary: "Built the widget",
29
29
  pendingWork: "Tests remain",
@@ -56,7 +56,7 @@ describe("lifecycle", () => {
56
56
  agentsDir,
57
57
  agentName: "builder-2",
58
58
  sessionId: "session-200",
59
- beadId: "overstory-abc2",
59
+ taskId: "overstory-abc2",
60
60
  reason: "crash",
61
61
  progressSummary: "Halfway done",
62
62
  pendingWork: "Finish implementation",
@@ -82,7 +82,7 @@ describe("lifecycle", () => {
82
82
  agentsDir,
83
83
  agentName: "builder-3",
84
84
  sessionId: "session-300",
85
- beadId: "overstory-def3",
85
+ taskId: "overstory-def3",
86
86
  reason: "manual",
87
87
  progressSummary: "Done with phase 1",
88
88
  pendingWork: "Phase 2",
@@ -116,7 +116,7 @@ describe("lifecycle", () => {
116
116
  agentsDir,
117
117
  agentName: "builder-4",
118
118
  sessionId: "session-400",
119
- beadId: "overstory-ghi4",
119
+ taskId: "overstory-ghi4",
120
120
  reason: "compaction",
121
121
  progressSummary: "First session work",
122
122
  pendingWork: "Continue",
@@ -137,7 +137,7 @@ describe("lifecycle", () => {
137
137
  agentsDir,
138
138
  agentName: "builder-4",
139
139
  sessionId: "session-401",
140
- beadId: "overstory-ghi4",
140
+ taskId: "overstory-ghi4",
141
141
  reason: "timeout",
142
142
  progressSummary: "Second session work",
143
143
  pendingWork: "Finish up",
@@ -172,7 +172,7 @@ describe("lifecycle", () => {
172
172
  agentsDir,
173
173
  agentName: "builder-5",
174
174
  sessionId: "session-500",
175
- beadId: "overstory-jkl5",
175
+ taskId: "overstory-jkl5",
176
176
  reason: "compaction",
177
177
  progressSummary: "Done",
178
178
  pendingWork: "Nothing",
@@ -73,7 +73,7 @@ export async function initiateHandoff(options: {
73
73
  agentsDir: string;
74
74
  agentName: string;
75
75
  sessionId: string;
76
- beadId: string;
76
+ taskId: string;
77
77
  reason: SessionHandoff["reason"];
78
78
  progressSummary: string;
79
79
  pendingWork: string;
@@ -83,7 +83,7 @@ export async function initiateHandoff(options: {
83
83
  }): Promise<SessionHandoff> {
84
84
  const checkpoint: SessionCheckpoint = {
85
85
  agentName: options.agentName,
86
- beadId: options.beadId,
86
+ taskId: options.taskId,
87
87
  sessionId: options.sessionId,
88
88
  timestamp: new Date().toISOString(),
89
89
  progressSummary: options.progressSummary,
@@ -635,6 +635,40 @@ describe("resolveModel", () => {
635
635
  const result = resolveModel(config, baseManifest, "coordinator", "opus");
636
636
  expect(result).toEqual({ model: "native-gw/claude-3-5-sonnet" });
637
637
  });
638
+
639
+ test("handles deeply nested model ID (slashes in model name)", () => {
640
+ const config = makeConfig(
641
+ { coordinator: "openrouter/openai/gpt-5.3" },
642
+ {
643
+ openrouter: {
644
+ type: "gateway",
645
+ baseUrl: "https://openrouter.ai/api/v1",
646
+ authTokenEnv: "OPENROUTER_API_KEY",
647
+ },
648
+ },
649
+ );
650
+ const result = resolveModel(config, baseManifest, "coordinator", "opus");
651
+ // First "/" splits provider "openrouter" from model ID "openai/gpt-5.3"
652
+ expect(result.model).toBe("sonnet");
653
+ expect(result.env?.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe("openai/gpt-5.3");
654
+ });
655
+
656
+ test("handles model ID with multiple slashes after provider", () => {
657
+ const config = makeConfig(
658
+ { coordinator: "mygateway/org/model/version" },
659
+ {
660
+ mygateway: {
661
+ type: "gateway",
662
+ baseUrl: "https://mygateway.example.com",
663
+ authTokenEnv: "MYGATEWAY_KEY",
664
+ },
665
+ },
666
+ );
667
+ const result = resolveModel(config, baseManifest, "coordinator", "opus");
668
+ // Provider is "mygateway", model ID is everything after the first "/"
669
+ expect(result.model).toBe("sonnet");
670
+ expect(result.env?.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe("org/model/version");
671
+ });
638
672
  });
639
673
 
640
674
  describe("resolveProviderEnv", () => {
@@ -704,6 +738,58 @@ describe("resolveProviderEnv", () => {
704
738
  );
705
739
  expect(result).not.toHaveProperty("ANTHROPIC_AUTH_TOKEN");
706
740
  });
741
+
742
+ test("env always sets ANTHROPIC_API_KEY to empty string", () => {
743
+ const result = resolveProviderEnv(
744
+ "openrouter",
745
+ "openai/gpt-5.3",
746
+ {
747
+ openrouter: {
748
+ type: "gateway",
749
+ baseUrl: "https://openrouter.ai/api/v1",
750
+ authTokenEnv: "OPENROUTER_API_KEY",
751
+ },
752
+ },
753
+ { OPENROUTER_API_KEY: "my-token" },
754
+ );
755
+ expect(result).not.toBeNull();
756
+ expect(result?.ANTHROPIC_API_KEY).toBe("");
757
+ });
758
+
759
+ test("handles authTokenEnv pointing to undefined env var", () => {
760
+ const result = resolveProviderEnv(
761
+ "openrouter",
762
+ "openai/gpt-5.3",
763
+ {
764
+ openrouter: {
765
+ type: "gateway",
766
+ baseUrl: "https://openrouter.ai/api/v1",
767
+ authTokenEnv: "MISSING_VAR",
768
+ },
769
+ },
770
+ {},
771
+ );
772
+ expect(result).not.toBeNull();
773
+ expect(result).not.toHaveProperty("ANTHROPIC_AUTH_TOKEN");
774
+ });
775
+
776
+ test("handles authTokenEnv field being undefined", () => {
777
+ const result = resolveProviderEnv(
778
+ "mygw",
779
+ "some-model",
780
+ {
781
+ mygw: {
782
+ type: "gateway",
783
+ baseUrl: "https://mygw.example.com",
784
+ },
785
+ },
786
+ {},
787
+ );
788
+ expect(result).not.toBeNull();
789
+ expect(result?.ANTHROPIC_BASE_URL).toBe("https://mygw.example.com");
790
+ expect(result?.ANTHROPIC_API_KEY).toBe("");
791
+ expect(result).not.toHaveProperty("ANTHROPIC_AUTH_TOKEN");
792
+ });
707
793
  });
708
794
 
709
795
  describe("manifest validation accepts arbitrary model strings", () => {
@@ -25,7 +25,7 @@ Read your assignment. Execute immediately.
25
25
  function makeConfig(overrides?: Partial<OverlayConfig>): OverlayConfig {
26
26
  return {
27
27
  agentName: "test-builder",
28
- beadId: "overstory-abc",
28
+ taskId: "overstory-abc",
29
29
  specPath: ".overstory/specs/overstory-abc.md",
30
30
  branchName: "agent/test-builder/overstory-abc",
31
31
  worktreePath: "/tmp/test-project/.overstory/worktrees/test-builder",
@@ -48,8 +48,8 @@ describe("generateOverlay", () => {
48
48
  expect(output).toContain("my-scout");
49
49
  });
50
50
 
51
- test("output contains bead ID", async () => {
52
- const config = makeConfig({ beadId: "overstory-xyz" });
51
+ test("output contains task ID", async () => {
52
+ const config = makeConfig({ taskId: "overstory-xyz" });
53
53
  const output = await generateOverlay(config);
54
54
 
55
55
  expect(output).toContain("overstory-xyz");
@@ -261,7 +261,7 @@ describe("generateOverlay", () => {
261
261
  const config = makeConfig({
262
262
  capability: "scout",
263
263
  agentName: "recon-1",
264
- beadId: "overstory-task1",
264
+ taskId: "overstory-task1",
265
265
  parentAgent: "lead-alpha",
266
266
  });
267
267
  const output = await generateOverlay(config);
@@ -419,7 +419,7 @@ describe("generateOverlay", () => {
419
419
  });
420
420
 
421
421
  test("default trackerCli renders as bd in quality gates", async () => {
422
- const config = makeConfig({ capability: "builder", beadId: "overstory-task1" });
422
+ const config = makeConfig({ capability: "builder", taskId: "overstory-task1" });
423
423
  const output = await generateOverlay(config);
424
424
 
425
425
  expect(output).toContain("bd close overstory-task1");
@@ -429,7 +429,7 @@ describe("generateOverlay", () => {
429
429
  const config = makeConfig({
430
430
  capability: "builder",
431
431
  trackerCli: "sd",
432
- beadId: "overstory-test1",
432
+ taskId: "overstory-test1",
433
433
  });
434
434
  const output = await generateOverlay(config);
435
435
 
@@ -451,7 +451,7 @@ describe("generateOverlay", () => {
451
451
  const config = makeConfig({
452
452
  capability: "scout",
453
453
  trackerCli: "sd",
454
- beadId: "overstory-test2",
454
+ taskId: "overstory-test2",
455
455
  });
456
456
  const output = await generateOverlay(config);
457
457
 
@@ -482,7 +482,7 @@ describe("generateOverlay", () => {
482
482
  });
483
483
 
484
484
  test("defaults backward-compatible: no trackerCli/trackerName produces bd/beads", async () => {
485
- const config = makeConfig({ capability: "builder", beadId: "overstory-back" });
485
+ const config = makeConfig({ capability: "builder", taskId: "overstory-back" });
486
486
  const output = await generateOverlay(config);
487
487
 
488
488
  expect(output).toContain("bd close overstory-back");
@@ -521,7 +521,7 @@ describe("writeOverlay", () => {
521
521
  const outputPath = join(worktreePath, ".claude", "CLAUDE.md");
522
522
  const content = await Bun.file(outputPath).text();
523
523
  expect(content).toContain("file-writer-test");
524
- expect(content).toContain(config.beadId);
524
+ expect(content).toContain(config.taskId);
525
525
  expect(content).toContain(config.branchName);
526
526
  });
527
527
 
@@ -91,7 +91,7 @@ function formatQualityGates(config: OverlayConfig): string {
91
91
  "Before reporting completion:",
92
92
  "",
93
93
  `1. **Record mulch learnings:** \`mulch record <domain> --type <convention|pattern|reference> --description "..."\` — capture reusable knowledge from your work`,
94
- `2. **Close issue:** \`${config.trackerCli ?? "bd"} close ${config.beadId} --reason "summary of findings"\``,
94
+ `2. **Close issue:** \`${config.trackerCli ?? "bd"} close ${config.taskId} --reason "summary of findings"\``,
95
95
  `3. **Send results:** \`overstory mail send --to ${config.parentAgent ?? "coordinator"} --subject "done" --body "Summary" --type result --agent ${config.agentName}\``,
96
96
  "",
97
97
  "You are a read-only agent. Do NOT commit, modify files, or run quality gates.",
@@ -113,8 +113,8 @@ function formatQualityGates(config: OverlayConfig): string {
113
113
  ...gateLines,
114
114
  `${gateLines.length + 1}. **Commit:** all changes committed to your branch (${config.branchName})`,
115
115
  `${gateLines.length + 2}. **Record mulch learnings:** \`mulch record <domain> --type <convention|pattern|failure|decision> --description "..." --outcome-status success --outcome-agent ${config.agentName}\` — capture insights from your work`,
116
- `${gateLines.length + 3}. **Signal completion:** send \`worker_done\` mail to ${config.parentAgent ?? "coordinator"}: \`overstory mail send --to ${config.parentAgent ?? "coordinator"} --subject "Worker done: ${config.beadId}" --body "Quality gates passed." --type worker_done --agent ${config.agentName}\``,
117
- `${gateLines.length + 4}. **Close issue:** \`${config.trackerCli ?? "bd"} close ${config.beadId} --reason "summary of changes"\``,
116
+ `${gateLines.length + 3}. **Signal completion:** send \`worker_done\` mail to ${config.parentAgent ?? "coordinator"}: \`overstory mail send --to ${config.parentAgent ?? "coordinator"} --subject "Worker done: ${config.taskId}" --body "Quality gates passed." --type worker_done --agent ${config.agentName}\``,
117
+ `${gateLines.length + 4}. **Close issue:** \`${config.trackerCli ?? "bd"} close ${config.taskId} --reason "summary of changes"\``,
118
118
  "",
119
119
  "Do NOT push to the canonical branch. Your work will be merged by the",
120
120
  "coordinator via `overstory merge`.",
@@ -205,7 +205,7 @@ export async function generateOverlay(config: OverlayConfig): Promise<string> {
205
205
 
206
206
  const replacements: Record<string, string> = {
207
207
  "{{AGENT_NAME}}": config.agentName,
208
- "{{BEAD_ID}}": config.beadId,
208
+ "{{BEAD_ID}}": config.taskId,
209
209
  "{{SPEC_PATH}}": config.specPath ?? "No spec file provided",
210
210
  "{{BRANCH_NAME}}": config.branchName,
211
211
  "{{WORKTREE_PATH}}": config.worktreePath,
@@ -97,7 +97,7 @@ describe("discoverAgents", () => {
97
97
  capability: "builder",
98
98
  worktreePath: join(tempDir, ".overstory", "worktrees", "builder-test"),
99
99
  branchName: "overstory/builder-test/task-123",
100
- beadId: "task-123",
100
+ taskId: "task-123",
101
101
  tmuxSession: "overstory-test-builder",
102
102
  state: "working",
103
103
  pid: 12345,
@@ -129,7 +129,7 @@ describe("discoverAgents", () => {
129
129
  capability: "builder",
130
130
  worktreePath: join(tempDir, ".overstory", "worktrees", "builder-test"),
131
131
  branchName: "overstory/builder-test/task-123",
132
- beadId: "task-123",
132
+ taskId: "task-123",
133
133
  tmuxSession: "overstory-test-builder",
134
134
  state: "working",
135
135
  pid: 12345,
@@ -148,7 +148,7 @@ describe("discoverAgents", () => {
148
148
  capability: "scout",
149
149
  worktreePath: join(tempDir, ".overstory", "worktrees", "scout-test"),
150
150
  branchName: "overstory/scout-test/task-456",
151
- beadId: "task-456",
151
+ taskId: "task-456",
152
152
  tmuxSession: "overstory-test-scout",
153
153
  state: "working",
154
154
  pid: 12346,
@@ -180,7 +180,7 @@ describe("discoverAgents", () => {
180
180
  capability: "builder",
181
181
  worktreePath: join(tempDir, ".overstory", "worktrees", "builder-working"),
182
182
  branchName: "overstory/builder-working/task-123",
183
- beadId: "task-123",
183
+ taskId: "task-123",
184
184
  tmuxSession: "overstory-test-working",
185
185
  state: "working",
186
186
  pid: 12345,
@@ -199,7 +199,7 @@ describe("discoverAgents", () => {
199
199
  capability: "builder",
200
200
  worktreePath: join(tempDir, ".overstory", "worktrees", "builder-completed"),
201
201
  branchName: "overstory/builder-completed/task-456",
202
- beadId: "task-456",
202
+ taskId: "task-456",
203
203
  tmuxSession: "overstory-test-completed",
204
204
  state: "completed",
205
205
  pid: null,
@@ -300,18 +300,18 @@ logging:
300
300
 
301
301
  it("should show help with --help flag", async () => {
302
302
  await agentsCommand(["--help"]);
303
- expect(stdoutBuffer).toContain("overstory agents");
303
+ expect(stdoutBuffer).toContain("agents");
304
304
  expect(stdoutBuffer).toContain("discover");
305
305
  });
306
306
 
307
307
  it("should show help with no subcommand", async () => {
308
308
  await agentsCommand([]);
309
- expect(stdoutBuffer).toContain("overstory agents");
309
+ expect(stdoutBuffer).toContain("agents");
310
310
  expect(stdoutBuffer).toContain("discover");
311
311
  });
312
312
 
313
313
  it("should error on unknown subcommand", async () => {
314
- await expect(agentsCommand(["unknown"])).rejects.toThrow("Unknown subcommand");
314
+ await expect(agentsCommand(["unknown"])).rejects.toThrow("unknown command");
315
315
  });
316
316
 
317
317
  afterEach(async () => {
@@ -5,26 +5,12 @@
5
5
  */
6
6
 
7
7
  import { join } from "node:path";
8
+ import { Command } from "commander";
8
9
  import { loadConfig } from "../config.ts";
9
10
  import { ValidationError } from "../errors.ts";
10
11
  import { openSessionStore } from "../sessions/compat.ts";
11
12
  import { type AgentSession, SUPPORTED_CAPABILITIES } from "../types.ts";
12
13
 
13
- /**
14
- * Parse a named flag value from args.
15
- */
16
- function getFlag(args: string[], flag: string): string | undefined {
17
- const idx = args.indexOf(flag);
18
- if (idx === -1 || idx + 1 >= args.length) {
19
- return undefined;
20
- }
21
- return args[idx + 1];
22
- }
23
-
24
- function hasFlag(args: string[], flag: string): boolean {
25
- return args.includes(flag);
26
- }
27
-
28
14
  /**
29
15
  * Discovered agent information including file scope.
30
16
  */
@@ -32,7 +18,7 @@ export interface DiscoveredAgent {
32
18
  agentName: string;
33
19
  capability: string;
34
20
  state: string;
35
- beadId: string;
21
+ taskId: string;
36
22
  branchName: string;
37
23
  parentAgent: string | null;
38
24
  depth: number;
@@ -131,7 +117,7 @@ export async function discoverAgents(
131
117
  agentName: session.agentName,
132
118
  capability: session.capability,
133
119
  state: session.state,
134
- beadId: session.beadId,
120
+ taskId: session.taskId,
135
121
  branchName: session.branchName,
136
122
  parentAgent: session.parentAgent,
137
123
  depth: session.depth,
@@ -180,7 +166,7 @@ function printAgents(agents: DiscoveredAgent[]): void {
180
166
  for (const agent of agents) {
181
167
  const icon = getStateIcon(agent.state);
182
168
  w(` ${icon} ${agent.agentName} [${agent.capability}]\n`);
183
- w(` State: ${agent.state} | Task: ${agent.beadId}\n`);
169
+ w(` State: ${agent.state} | Task: ${agent.taskId}\n`);
184
170
  w(` Branch: ${agent.branchName}\n`);
185
171
  w(` Parent: ${agent.parentAgent ?? "none"} | Depth: ${agent.depth}\n`);
186
172
 
@@ -194,94 +180,79 @@ function printAgents(agents: DiscoveredAgent[]): void {
194
180
  }
195
181
  }
196
182
 
197
- const DISCOVER_HELP = `overstory agents discover — Find active agents by capability
198
-
199
- Usage: overstory agents discover [--capability <type>] [--all] [--json]
200
-
201
- Options:
202
- --capability <type> Filter by capability (builder, scout, reviewer, lead, merger, coordinator, supervisor)
203
- --all Include completed and zombie agents (default: active only)
204
- --json Output as JSON
205
- --help, -h Show this help`;
206
-
207
- const AGENTS_HELP = `overstory agents — Discover and query agents
208
-
209
- Usage: overstory agents <subcommand> [options]
210
-
211
- Subcommands:
212
- discover Find active agents by capability
213
-
214
- Options:
215
- --json Output as JSON
216
- --help, -h Show this help
217
-
218
- Run 'overstory agents <subcommand> --help' for subcommand-specific help.`;
219
-
220
183
  /**
221
- * Handle the 'discover' subcommand.
184
+ * Create the Commander command for `overstory agents`.
222
185
  */
223
- async function discoverCommand(args: string[]): Promise<void> {
224
- if (hasFlag(args, "--help") || hasFlag(args, "-h")) {
225
- process.stdout.write(`${DISCOVER_HELP}\n`);
226
- return;
227
- }
186
+ export function createAgentsCommand(): Command {
187
+ const cmd = new Command("agents").description("Discover and query agents");
188
+
189
+ cmd
190
+ .command("discover")
191
+ .description("Find active agents by capability")
192
+ .option(
193
+ "--capability <type>",
194
+ "Filter by capability (builder, scout, reviewer, lead, merger, coordinator, supervisor)",
195
+ )
196
+ .option("--all", "Include completed and zombie agents (default: active only)")
197
+ .option("--json", "Output as JSON")
198
+ .action(async (opts: { capability?: string; all?: boolean; json?: boolean }) => {
199
+ const capability = opts.capability;
200
+
201
+ // Validate capability if provided
202
+ if (capability && !SUPPORTED_CAPABILITIES.includes(capability as never)) {
203
+ throw new ValidationError(
204
+ `Invalid capability: ${capability}. Must be one of: ${SUPPORTED_CAPABILITIES.join(", ")}`,
205
+ {
206
+ field: "capability",
207
+ value: capability,
208
+ },
209
+ );
210
+ }
228
211
 
229
- const json = hasFlag(args, "--json");
230
- const includeAll = hasFlag(args, "--all");
231
- const capability = getFlag(args, "--capability");
232
-
233
- // Validate capability if provided
234
- if (capability && !SUPPORTED_CAPABILITIES.includes(capability as never)) {
235
- throw new ValidationError(
236
- `Invalid capability: ${capability}. Must be one of: ${SUPPORTED_CAPABILITIES.join(", ")}`,
237
- {
238
- field: "capability",
239
- value: capability,
240
- },
241
- );
242
- }
212
+ const cwd = process.cwd();
213
+ const config = await loadConfig(cwd);
214
+ const root = config.project.root;
243
215
 
244
- const cwd = process.cwd();
245
- const config = await loadConfig(cwd);
246
- const root = config.project.root;
216
+ const agents = await discoverAgents(root, {
217
+ capability,
218
+ includeAll: opts.all ?? false,
219
+ });
247
220
 
248
- const agents = await discoverAgents(root, { capability, includeAll });
221
+ if (opts.json) {
222
+ process.stdout.write(`${JSON.stringify(agents, null, "\t")}\n`);
223
+ } else {
224
+ printAgents(agents);
225
+ }
226
+ });
249
227
 
250
- if (json) {
251
- process.stdout.write(`${JSON.stringify(agents, null, "\t")}\n`);
252
- } else {
253
- printAgents(agents);
254
- }
228
+ return cmd;
255
229
  }
256
230
 
257
231
  /**
258
232
  * Entry point for `overstory agents <subcommand>`.
259
233
  */
260
234
  export async function agentsCommand(args: string[]): Promise<void> {
261
- if (hasFlag(args, "--help") || hasFlag(args, "-h")) {
262
- process.stdout.write(`${AGENTS_HELP}\n`);
263
- return;
264
- }
235
+ const cmd = createAgentsCommand();
236
+ cmd.exitOverride();
265
237
 
266
- // Extract subcommand: first arg that is not a flag
267
- const subcommand = args.find((arg) => !arg.startsWith("-"));
268
-
269
- if (!subcommand) {
270
- process.stdout.write(`${AGENTS_HELP}\n`);
238
+ if (args.length === 0) {
239
+ process.stdout.write(cmd.helpInformation());
271
240
  return;
272
241
  }
273
242
 
274
- // Remove the subcommand from args before passing to handler
275
- const subArgs = args.filter((arg) => arg !== subcommand);
276
-
277
- switch (subcommand) {
278
- case "discover":
279
- await discoverCommand(subArgs);
280
- break;
281
- default:
282
- throw new ValidationError(`Unknown subcommand: ${subcommand}`, {
283
- field: "subcommand",
284
- value: subcommand,
285
- });
243
+ try {
244
+ await cmd.parseAsync(args, { from: "user" });
245
+ } catch (err: unknown) {
246
+ if (err && typeof err === "object" && "code" in err) {
247
+ const code = (err as { code: string }).code;
248
+ if (code === "commander.helpDisplayed" || code === "commander.version") {
249
+ return;
250
+ }
251
+ if (code === "commander.unknownCommand") {
252
+ const message = err instanceof Error ? err.message : String(err);
253
+ throw new ValidationError(message, { field: "subcommand" });
254
+ }
255
+ }
256
+ throw err;
286
257
  }
287
258
  }