@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
package/README.md CHANGED
@@ -127,6 +127,7 @@ overstory sling <task-id> Spawn a worker agent
127
127
  --parent <agent-name> Parent (for hierarchy tracking)
128
128
  --depth <n> Current hierarchy depth
129
129
  --skip-scout Skip scout phase (passed to lead overlay)
130
+ --skip-task-check Skip task existence validation
130
131
  --json JSON output
131
132
 
132
133
  overstory stop <agent-name> Terminate a running agent
@@ -269,16 +270,16 @@ Global Flags:
269
270
  ## Tech Stack
270
271
 
271
272
  - **Runtime**: Bun (TypeScript directly, no build step)
272
- - **Dependencies**: Zero runtime dependencies only Bun built-in APIs
273
+ - **Dependencies**: Minimal runtime — `chalk` (color output), core I/O via Bun built-in APIs
273
274
  - **Database**: SQLite via `bun:sqlite` (WAL mode for concurrent access)
274
275
  - **Linting**: Biome (formatter + linter)
275
- - **Testing**: `bun test` (2087 tests across 75 files, colocated with source)
276
+ - **Testing**: `bun test` (2128 tests across 76 files, colocated with source)
276
277
  - **External CLIs**: `bd` (beads) or `sd` (seeds), `mulch`, `git`, `tmux` — invoked as subprocesses
277
278
 
278
279
  ## Development
279
280
 
280
281
  ```bash
281
- # Run tests (2087 tests across 75 files)
282
+ # Run tests (2128 tests across 76 files)
282
283
  bun test
283
284
 
284
285
  # Run a single test
@@ -307,14 +308,14 @@ Use the bump script to update both:
307
308
  bun run version:bump <major|minor|patch>
308
309
  ```
309
310
 
310
- Git tags are created automatically by GitHub Actions when a version bump is pushed to `main`.
311
+ Git tags, npm publishing, and GitHub releases are handled automatically by the `publish.yml` workflow when a version bump is pushed to `main`.
311
312
 
312
313
  ## Project Structure
313
314
 
314
315
  ```
315
316
  overstory/
316
317
  src/
317
- index.ts CLI entry point (command router)
318
+ index.ts CLI entry point (Commander.js program)
318
319
  types.ts Shared types and interfaces
319
320
  config.ts Config loader + validation
320
321
  errors.ts Custom error types
@@ -322,7 +323,7 @@ overstory/
322
323
  agents.ts Agent discovery and querying
323
324
  coordinator.ts Persistent orchestrator lifecycle
324
325
  supervisor.ts Team lead management
325
- dashboard.ts Live TUI dashboard (ANSI, zero deps)
326
+ dashboard.ts Live TUI dashboard (ANSI via Chalk)
326
327
  hooks.ts Orchestrator hooks management
327
328
  sling.ts Agent spawning
328
329
  group.ts Task group batch tracking
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@os-eco/overstory-cli",
3
- "version": "0.6.1",
3
+ "version": "0.6.4",
4
4
  "description": "Multi-agent orchestration for Claude Code — spawn worker agents in git worktrees via tmux, coordinate through SQLite mail, merge with tiered conflict resolution",
5
5
  "author": "Jaymin West",
6
6
  "license": "MIT",
@@ -21,10 +21,15 @@
21
21
  "developer-tools"
22
22
  ],
23
23
  "bin": {
24
- "overstory": "./src/index.ts"
24
+ "overstory": "./src/index.ts",
25
+ "ov": "./src/index.ts"
25
26
  },
26
27
  "main": "src/index.ts",
27
- "files": ["src", "agents", "templates"],
28
+ "files": [
29
+ "src",
30
+ "agents",
31
+ "templates"
32
+ ],
28
33
  "publishConfig": {
29
34
  "access": "public"
30
35
  },
@@ -38,7 +43,10 @@
38
43
  "typecheck": "tsc --noEmit",
39
44
  "version:bump": "bun scripts/version-bump.ts"
40
45
  },
41
- "dependencies": {},
46
+ "dependencies": {
47
+ "chalk": "^5.6.2",
48
+ "commander": "^14.0.3"
49
+ },
42
50
  "devDependencies": {
43
51
  "@types/bun": "latest",
44
52
  "typescript": "^5.9.0",
@@ -894,35 +894,37 @@ describe("isOverstoryHookEntry", () => {
894
894
  describe("getCapabilityGuards", () => {
895
895
  // 10 native team tool blocks apply to ALL capabilities
896
896
  const NATIVE_TEAM_TOOL_COUNT = 10;
897
+ // 3 interactive tool blocks (AskUserQuestion, EnterPlanMode, EnterWorktree) apply to ALL capabilities
898
+ const INTERACTIVE_TOOL_COUNT = 3;
897
899
 
898
- test("returns 14 guards for scout (10 team + 3 tool blocks + 1 bash file guard)", () => {
900
+ test("returns 17 guards for scout (10 team + 3 interactive + 3 tool blocks + 1 bash file guard)", () => {
899
901
  const guards = getCapabilityGuards("scout");
900
- expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + 4);
902
+ expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + INTERACTIVE_TOOL_COUNT + 4);
901
903
  });
902
904
 
903
- test("returns 14 guards for reviewer (10 team + 3 tool blocks + 1 bash file guard)", () => {
905
+ test("returns 17 guards for reviewer (10 team + 3 interactive + 3 tool blocks + 1 bash file guard)", () => {
904
906
  const guards = getCapabilityGuards("reviewer");
905
- expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + 4);
907
+ expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + INTERACTIVE_TOOL_COUNT + 4);
906
908
  });
907
909
 
908
- test("returns 14 guards for lead (10 team + 3 tool blocks + 1 bash file guard)", () => {
910
+ test("returns 17 guards for lead (10 team + 3 interactive + 3 tool blocks + 1 bash file guard)", () => {
909
911
  const guards = getCapabilityGuards("lead");
910
- expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + 4);
912
+ expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + INTERACTIVE_TOOL_COUNT + 4);
911
913
  });
912
914
 
913
- test("returns 11 guards for builder (10 team + 1 bash path boundary)", () => {
915
+ test("returns 14 guards for builder (10 team + 3 interactive + 1 bash path boundary)", () => {
914
916
  const guards = getCapabilityGuards("builder");
915
- expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + 1);
917
+ expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + INTERACTIVE_TOOL_COUNT + 1);
916
918
  });
917
919
 
918
- test("returns 11 guards for merger (10 team + 1 bash path boundary)", () => {
920
+ test("returns 14 guards for merger (10 team + 3 interactive + 1 bash path boundary)", () => {
919
921
  const guards = getCapabilityGuards("merger");
920
- expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + 1);
922
+ expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + INTERACTIVE_TOOL_COUNT + 1);
921
923
  });
922
924
 
923
- test("returns 10 guards for unknown capability (10 team tool blocks only)", () => {
925
+ test("returns 13 guards for unknown capability (10 team + 3 interactive tool blocks)", () => {
924
926
  const guards = getCapabilityGuards("unknown");
925
- expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT);
927
+ expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + INTERACTIVE_TOOL_COUNT);
926
928
  });
927
929
 
928
930
  test("builder gets Bash path boundary guard", () => {
@@ -1039,14 +1041,90 @@ describe("getCapabilityGuards", () => {
1039
1041
  expect(taskGuard?.hooks[0]?.command).toContain('[ -z "$OVERSTORY_AGENT_NAME" ] && exit 0;');
1040
1042
  });
1041
1043
 
1042
- test("coordinator gets 14 guards (10 team + 3 tool blocks + 1 bash file guard)", () => {
1044
+ test("coordinator gets 17 guards (10 team + 3 interactive + 3 tool blocks + 1 bash file guard)", () => {
1043
1045
  const guards = getCapabilityGuards("coordinator");
1044
- expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + 4);
1046
+ expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + INTERACTIVE_TOOL_COUNT + 4);
1045
1047
  });
1046
1048
 
1047
- test("supervisor gets 14 guards (10 team + 3 tool blocks + 1 bash file guard)", () => {
1049
+ test("supervisor gets 17 guards (10 team + 3 interactive + 3 tool blocks + 1 bash file guard)", () => {
1048
1050
  const guards = getCapabilityGuards("supervisor");
1049
- expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + 4);
1051
+ expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + INTERACTIVE_TOOL_COUNT + 4);
1052
+ });
1053
+
1054
+ test("all capabilities get AskUserQuestion blocked", () => {
1055
+ for (const cap of [
1056
+ "scout",
1057
+ "reviewer",
1058
+ "lead",
1059
+ "coordinator",
1060
+ "supervisor",
1061
+ "builder",
1062
+ "merger",
1063
+ "unknown",
1064
+ ]) {
1065
+ const guards = getCapabilityGuards(cap);
1066
+ const guard = guards.find((g) => g.matcher === "AskUserQuestion");
1067
+ expect(guard).toBeDefined();
1068
+ expect(guard?.hooks[0]?.command).toContain("human interaction");
1069
+ expect(guard?.hooks[0]?.command).toContain("overstory mail");
1070
+ }
1071
+ });
1072
+
1073
+ test("all capabilities get EnterPlanMode blocked", () => {
1074
+ for (const cap of [
1075
+ "scout",
1076
+ "reviewer",
1077
+ "lead",
1078
+ "coordinator",
1079
+ "supervisor",
1080
+ "builder",
1081
+ "merger",
1082
+ "unknown",
1083
+ ]) {
1084
+ const guards = getCapabilityGuards(cap);
1085
+ const guard = guards.find((g) => g.matcher === "EnterPlanMode");
1086
+ expect(guard).toBeDefined();
1087
+ expect(guard?.hooks[0]?.command).toContain("human interaction");
1088
+ expect(guard?.hooks[0]?.command).toContain("overstory mail");
1089
+ }
1090
+ });
1091
+
1092
+ test("all capabilities get EnterWorktree blocked", () => {
1093
+ for (const cap of [
1094
+ "scout",
1095
+ "reviewer",
1096
+ "lead",
1097
+ "coordinator",
1098
+ "supervisor",
1099
+ "builder",
1100
+ "merger",
1101
+ "unknown",
1102
+ ]) {
1103
+ const guards = getCapabilityGuards(cap);
1104
+ const guard = guards.find((g) => g.matcher === "EnterWorktree");
1105
+ expect(guard).toBeDefined();
1106
+ expect(guard?.hooks[0]?.command).toContain("human interaction");
1107
+ expect(guard?.hooks[0]?.command).toContain("overstory mail");
1108
+ }
1109
+ });
1110
+
1111
+ test("interactive guards include env var guard prefix", () => {
1112
+ const guards = getCapabilityGuards("builder");
1113
+ for (const tool of ["AskUserQuestion", "EnterPlanMode", "EnterWorktree"]) {
1114
+ const guard = guards.find((g) => g.matcher === tool);
1115
+ expect(guard).toBeDefined();
1116
+ expect(guard?.hooks[0]?.command).toContain('[ -z "$OVERSTORY_AGENT_NAME" ] && exit 0;');
1117
+ }
1118
+ });
1119
+
1120
+ test("interactive guard block reason mentions tool name", () => {
1121
+ const guards = getCapabilityGuards("scout");
1122
+ const askGuard = guards.find((g) => g.matcher === "AskUserQuestion");
1123
+ expect(askGuard?.hooks[0]?.command).toContain("AskUserQuestion");
1124
+ const planGuard = guards.find((g) => g.matcher === "EnterPlanMode");
1125
+ expect(planGuard?.hooks[0]?.command).toContain("EnterPlanMode");
1126
+ const worktreeGuard = guards.find((g) => g.matcher === "EnterWorktree");
1127
+ expect(worktreeGuard?.hooks[0]?.command).toContain("EnterWorktree");
1050
1128
  });
1051
1129
  });
1052
1130
 
@@ -46,6 +46,13 @@ const NATIVE_TEAM_TOOLS = [
46
46
  "TaskStop",
47
47
  ];
48
48
 
49
+ /**
50
+ * Tools that require human interaction and block indefinitely in non-interactive
51
+ * tmux sessions. Agents run non-interactively and must never call these tools.
52
+ * Use overstory mail (--type question) to escalate to the orchestrator instead.
53
+ */
54
+ const INTERACTIVE_TOOLS = ["AskUserQuestion", "EnterPlanMode", "EnterWorktree"];
55
+
49
56
  /** Tools that non-implementation agents must not use. */
50
57
  const WRITE_TOOLS = ["Write", "Edit", "NotebookEdit"];
51
58
 
@@ -445,6 +452,17 @@ export function getCapabilityGuards(capability: string): HookEntry[] {
445
452
  );
446
453
  guards.push(...teamToolGuards);
447
454
 
455
+ // Block interactive tools for ALL overstory agents.
456
+ // These tools require a human to respond and block indefinitely in tmux sessions.
457
+ // Agents must use overstory mail (--type question) to escalate instead.
458
+ const interactiveGuards = INTERACTIVE_TOOLS.map((tool) =>
459
+ blockGuard(
460
+ tool,
461
+ `${tool} requires human interaction -- agents run non-interactively. Use overstory mail (--type question) to escalate`,
462
+ ),
463
+ );
464
+ guards.push(...interactiveGuards);
465
+
448
466
  if (NON_IMPLEMENTATION_CAPABILITIES.has(capability)) {
449
467
  const toolGuards = WRITE_TOOLS.map((tool) =>
450
468
  blockGuard(tool, `${capability} agents cannot modify files — ${tool} is not allowed`),
@@ -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", () => {
@@ -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
  */
@@ -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
  }