@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
@@ -19,26 +19,17 @@ import { createMergeResolver } from "../merge/resolver.ts";
19
19
  import { createMulchClient } from "../mulch/client.ts";
20
20
  import type { MergeEntry, MergeResult } from "../types.ts";
21
21
 
22
- /**
23
- * Parse a named flag value from an args array.
24
- * Returns the value after the flag, or undefined if not present.
25
- */
26
- function getFlag(args: string[], flag: string): string | undefined {
27
- const idx = args.indexOf(flag);
28
- if (idx === -1 || idx + 1 >= args.length) {
29
- return undefined;
30
- }
31
- return args[idx + 1];
32
- }
33
-
34
- /** Check if a boolean flag is present in the args. */
35
- function hasFlag(args: string[], flag: string): boolean {
36
- return args.includes(flag);
22
+ export interface MergeOptions {
23
+ branch?: string;
24
+ all?: boolean;
25
+ into?: string;
26
+ dryRun?: boolean;
27
+ json?: boolean;
37
28
  }
38
29
 
39
30
  /**
40
31
  * Extract agent name from a branch following the overstory naming convention.
41
- * Pattern: overstory/{agentName}/{beadId}
32
+ * Pattern: overstory/{agentName}/{taskId}
42
33
  * Falls back to "unknown" if the pattern does not match.
43
34
  */
44
35
  function parseAgentName(branchName: string): string {
@@ -50,8 +41,8 @@ function parseAgentName(branchName: string): string {
50
41
  }
51
42
 
52
43
  /**
53
- * Extract bead ID from a branch following the overstory naming convention.
54
- * Pattern: overstory/{agentName}/{beadId}
44
+ * Extract task ID from a branch following the overstory naming convention.
45
+ * Pattern: overstory/{agentName}/{taskId}
55
46
  * Falls back to "unknown" if the pattern does not match.
56
47
  */
57
48
  function parseBeadId(branchName: string): string {
@@ -98,7 +89,7 @@ function formatResult(result: MergeResult): string {
98
89
  const statusIcon = result.success ? "Merged" : "Failed";
99
90
  const lines: string[] = [
100
91
  `Merging branch: ${result.entry.branchName}`,
101
- ` Agent: ${result.entry.agentName} | Task: ${result.entry.beadId}`,
92
+ ` Agent: ${result.entry.agentName} | Task: ${result.entry.taskId}`,
102
93
  ` Files: ${result.entry.filesModified.length} modified`,
103
94
  ` Result: ${statusIcon} (tier: ${result.tier})`,
104
95
  ];
@@ -118,7 +109,7 @@ function formatResult(result: MergeResult): string {
118
109
  function formatDryRun(entry: MergeEntry): string {
119
110
  const lines: string[] = [
120
111
  `[dry-run] Branch: ${entry.branchName}`,
121
- ` Agent: ${entry.agentName} | Task: ${entry.beadId}`,
112
+ ` Agent: ${entry.agentName} | Task: ${entry.taskId}`,
122
113
  ` Status: ${entry.status}`,
123
114
  ` Files: ${entry.filesModified.length} modified`,
124
115
  ];
@@ -135,35 +126,14 @@ function formatDryRun(entry: MergeEntry): string {
135
126
  /**
136
127
  * Entry point for `overstory merge [flags]`.
137
128
  *
138
- * Flags:
139
- * --branch <name> Merge a specific branch
140
- * --all Merge all pending branches in the queue
141
- * --dry-run Check for conflicts without actually merging
142
- * --json Output results as JSON
129
+ * @param opts - Command options
143
130
  */
144
- const MERGE_HELP = `overstory merge Merge agent branches into canonical
145
-
146
- Usage: overstory merge --branch <name> | --all [--into <branch>] [--dry-run] [--json]
147
-
148
- Options:
149
- --branch <name> Merge a specific branch
150
- --all Merge all pending branches in the queue
151
- --into <branch> Target branch to merge into (default: config canonicalBranch)
152
- --dry-run Check for conflicts without actually merging
153
- --json Output results as JSON
154
- --help, -h Show this help`;
155
-
156
- export async function mergeCommand(args: string[]): Promise<void> {
157
- if (args.includes("--help") || args.includes("-h")) {
158
- process.stdout.write(`${MERGE_HELP}\n`);
159
- return;
160
- }
161
-
162
- const branchName = getFlag(args, "--branch");
163
- const all = hasFlag(args, "--all");
164
- const into = getFlag(args, "--into");
165
- const dryRun = hasFlag(args, "--dry-run");
166
- const json = hasFlag(args, "--json");
131
+ export async function mergeCommand(opts: MergeOptions): Promise<void> {
132
+ const branchName = opts.branch;
133
+ const all = opts.all ?? false;
134
+ const into = opts.into;
135
+ const dryRun = opts.dryRun ?? false;
136
+ const json = opts.json ?? false;
167
137
 
168
138
  if (!branchName && !all) {
169
139
  throw new ValidationError("Either --branch <name> or --all is required for overstory merge", {
@@ -206,7 +176,7 @@ export async function mergeCommand(args: string[]): Promise<void> {
206
176
  /**
207
177
  * Handle merging a specific branch.
208
178
  * If the branch is not in the queue, creates a new entry by detecting
209
- * agent name, bead ID, and modified files from git.
179
+ * agent name, task ID, and modified files from git.
210
180
  */
211
181
  async function handleBranch(
212
182
  branchName: string,
@@ -241,12 +211,12 @@ async function handleBranch(
241
211
  }
242
212
 
243
213
  const agentName = parseAgentName(branchName);
244
- const beadId = parseBeadId(branchName);
214
+ const taskId = parseBeadId(branchName);
245
215
  const filesModified = await detectModifiedFiles(repoRoot, canonicalBranch, branchName);
246
216
 
247
217
  entry = queue.enqueue({
248
218
  branchName,
249
- beadId,
219
+ taskId,
250
220
  agentName,
251
221
  filesModified,
252
222
  });
@@ -54,7 +54,7 @@ describe("metricsCommand", () => {
54
54
  function makeSession(overrides: Partial<SessionMetrics> = {}): SessionMetrics {
55
55
  return {
56
56
  agentName: "test-agent",
57
- beadId: "bead-001",
57
+ taskId: "bead-001",
58
58
  capability: "builder",
59
59
  startedAt: new Date(Date.now() - 120_000).toISOString(),
60
60
  completedAt: new Date().toISOString(),
@@ -77,7 +77,7 @@ describe("metricsCommand", () => {
77
77
  await metricsCommand(["--help"]);
78
78
  const out = output();
79
79
 
80
- expect(out).toContain("overstory metrics");
80
+ expect(out).toContain("metrics");
81
81
  expect(out).toContain("--last <n>");
82
82
  expect(out).toContain("--json");
83
83
  expect(out).toContain("--help");
@@ -87,7 +87,7 @@ describe("metricsCommand", () => {
87
87
  await metricsCommand(["-h"]);
88
88
  const out = output();
89
89
 
90
- expect(out).toContain("overstory metrics");
90
+ expect(out).toContain("metrics");
91
91
  expect(out).toContain("--last <n>");
92
92
  });
93
93
 
@@ -177,7 +177,7 @@ describe("metricsCommand", () => {
177
177
  store.recordSession(
178
178
  makeSession({
179
179
  agentName: "test-builder",
180
- beadId: "bead-123",
180
+ taskId: "bead-123",
181
181
  capability: "builder",
182
182
  }),
183
183
  );
@@ -190,7 +190,7 @@ describe("metricsCommand", () => {
190
190
  const parsed = JSON.parse(out.trim()) as { sessions: SessionMetrics[] };
191
191
  expect(parsed.sessions).toHaveLength(1);
192
192
  expect(parsed.sessions[0]?.agentName).toBe("test-builder");
193
- expect(parsed.sessions[0]?.beadId).toBe("bead-123");
193
+ expect(parsed.sessions[0]?.taskId).toBe("bead-123");
194
194
  expect(parsed.sessions[0]?.capability).toBe("builder");
195
195
  });
196
196
 
@@ -203,7 +203,7 @@ describe("metricsCommand", () => {
203
203
  store.recordSession(
204
204
  makeSession({
205
205
  agentName: `agent-${i}`,
206
- beadId: `bead-${i}`,
206
+ taskId: `bead-${i}`,
207
207
  startedAt: new Date(Date.now() - (5 - i) * 1000).toISOString(),
208
208
  }),
209
209
  );
@@ -227,7 +227,7 @@ describe("metricsCommand", () => {
227
227
  store.recordSession(
228
228
  makeSession({
229
229
  agentName: `agent-${i}`,
230
- beadId: `bead-${i}`,
230
+ taskId: `bead-${i}`,
231
231
  }),
232
232
  );
233
233
  }
@@ -363,7 +363,7 @@ describe("formatDuration helper", () => {
363
363
  function makeSession(durationMs: number): SessionMetrics {
364
364
  return {
365
365
  agentName: "test-agent",
366
- beadId: "bead-001",
366
+ taskId: "bead-001",
367
367
  capability: "builder",
368
368
  startedAt: new Date(Date.now() - durationMs).toISOString(),
369
369
  completedAt: new Date().toISOString(),
@@ -6,22 +6,13 @@
6
6
  */
7
7
 
8
8
  import { join } from "node:path";
9
+ import { Command } from "commander";
9
10
  import { loadConfig } from "../config.ts";
10
11
  import { createMetricsStore } from "../metrics/store.ts";
11
12
 
12
- /**
13
- * Parse a named flag value from args.
14
- */
15
- function getFlag(args: string[], flag: string): string | undefined {
16
- const idx = args.indexOf(flag);
17
- if (idx === -1 || idx + 1 >= args.length) {
18
- return undefined;
19
- }
20
- return args[idx + 1];
21
- }
22
-
23
- function hasFlag(args: string[], flag: string): boolean {
24
- return args.includes(flag);
13
+ interface MetricsOpts {
14
+ last?: string;
15
+ json?: boolean;
25
16
  }
26
17
 
27
18
  /**
@@ -39,27 +30,9 @@ function formatDuration(ms: number): string {
39
30
  return `${hours}h ${remainMin}m`;
40
31
  }
41
32
 
42
- /**
43
- * Entry point for `overstory metrics [--last <n>] [--json]`.
44
- */
45
- const METRICS_HELP = `overstory metrics — Show session metrics
46
-
47
- Usage: overstory metrics [--last <n>] [--json]
48
-
49
- Options:
50
- --last <n> Number of recent sessions to show (default: 20)
51
- --json Output as JSON
52
- --help, -h Show this help`;
53
-
54
- export async function metricsCommand(args: string[]): Promise<void> {
55
- if (args.includes("--help") || args.includes("-h")) {
56
- process.stdout.write(`${METRICS_HELP}\n`);
57
- return;
58
- }
59
-
60
- const lastStr = getFlag(args, "--last");
61
- const limit = lastStr ? Number.parseInt(lastStr, 10) : 20;
62
- const json = hasFlag(args, "--json");
33
+ async function executeMetrics(opts: MetricsOpts): Promise<void> {
34
+ const limit = opts.last ? Number.parseInt(opts.last, 10) : 20;
35
+ const json = opts.json ?? false;
63
36
 
64
37
  const cwd = process.cwd();
65
38
  const config = await loadConfig(cwd);
@@ -134,10 +107,36 @@ export async function metricsCommand(args: string[]): Promise<void> {
134
107
  const status = s.completedAt ? "done" : "running";
135
108
  const duration = formatDuration(s.durationMs);
136
109
  process.stdout.write(
137
- ` ${s.agentName} [${s.capability}] ${s.beadId} | ${status} | ${duration}\n`,
110
+ ` ${s.agentName} [${s.capability}] ${s.taskId} | ${status} | ${duration}\n`,
138
111
  );
139
112
  }
140
113
  } finally {
141
114
  store.close();
142
115
  }
143
116
  }
117
+
118
+ export function createMetricsCommand(): Command {
119
+ return new Command("metrics")
120
+ .description("Show session metrics")
121
+ .option("--last <n>", "Number of recent sessions to show (default: 20)")
122
+ .option("--json", "Output as JSON")
123
+ .action(async (opts: MetricsOpts) => {
124
+ await executeMetrics(opts);
125
+ });
126
+ }
127
+
128
+ export async function metricsCommand(args: string[]): Promise<void> {
129
+ const cmd = createMetricsCommand();
130
+ cmd.exitOverride();
131
+ try {
132
+ await cmd.parseAsync(args, { from: "user" });
133
+ } catch (err: unknown) {
134
+ if (err && typeof err === "object" && "code" in err) {
135
+ const code = (err as { code: string }).code;
136
+ if (code === "commander.helpDisplayed" || code === "commander.version") {
137
+ return;
138
+ }
139
+ }
140
+ throw err;
141
+ }
142
+ }
@@ -89,7 +89,7 @@ describe("monitorCommand", () => {
89
89
  test("--help prints help text containing 'overstory monitor'", async () => {
90
90
  await monitorCommand(["--help"]);
91
91
  const output = stdoutWrites.join("");
92
- expect(output).toContain("overstory monitor");
92
+ expect(output).toContain("monitor");
93
93
  });
94
94
 
95
95
  test("--help prints help text containing 'start'", async () => {
@@ -113,13 +113,13 @@ describe("monitorCommand", () => {
113
113
  test("-h prints help text", async () => {
114
114
  await monitorCommand(["-h"]);
115
115
  const output = stdoutWrites.join("");
116
- expect(output).toContain("overstory monitor");
116
+ expect(output).toContain("monitor");
117
117
  });
118
118
 
119
119
  test("empty args [] shows help (same as --help)", async () => {
120
120
  await monitorCommand([]);
121
121
  const output = stdoutWrites.join("");
122
- expect(output).toContain("overstory monitor");
122
+ expect(output).toContain("monitor");
123
123
  });
124
124
 
125
125
  test("unknown subcommand 'restart' throws ValidationError", async () => {
@@ -15,6 +15,7 @@
15
15
 
16
16
  import { mkdir } from "node:fs/promises";
17
17
  import { join } from "node:path";
18
+ import { Command } from "commander";
18
19
  import { deployHooks } from "../agents/hooks-deployer.ts";
19
20
  import { createIdentity, loadIdentity } from "../agents/identity.ts";
20
21
  import { createManifestLoader, resolveModel } from "../agents/manifest.ts";
@@ -50,15 +51,6 @@ export function buildMonitorBeacon(): string {
50
51
  return parts.join(" — ");
51
52
  }
52
53
 
53
- /**
54
- * Determine whether to auto-attach to the tmux session after starting.
55
- */
56
- function resolveAttach(args: string[], isTTY: boolean): boolean {
57
- if (args.includes("--attach")) return true;
58
- if (args.includes("--no-attach")) return false;
59
- return isTTY;
60
- }
61
-
62
54
  /**
63
55
  * Start the monitor agent.
64
56
  *
@@ -70,9 +62,8 @@ function resolveAttach(args: string[], isTTY: boolean): boolean {
70
62
  * 6. Send startup beacon
71
63
  * 7. Record session in SessionStore (sessions.db)
72
64
  */
73
- async function startMonitor(args: string[]): Promise<void> {
74
- const json = args.includes("--json");
75
- const shouldAttach = resolveAttach(args, !!process.stdout.isTTY);
65
+ async function startMonitor(opts: { json: boolean; attach: boolean }): Promise<void> {
66
+ const { json, attach: shouldAttach } = opts;
76
67
 
77
68
  if (isRunningAsRoot()) {
78
69
  throw new AgentError(
@@ -118,8 +109,6 @@ async function startMonitor(args: string[]): Promise<void> {
118
109
  }
119
110
 
120
111
  // Deploy monitor-specific hooks to the project root's .claude/ directory.
121
- // The monitor gets the same structural enforcement as other non-implementation
122
- // agents (Write/Edit/NotebookEdit blocked, dangerous bash commands blocked).
123
112
  await deployHooks(projectRoot, MONITOR_NAME, "monitor");
124
113
 
125
114
  // Create monitor identity if first run
@@ -146,7 +135,6 @@ async function startMonitor(args: string[]): Promise<void> {
146
135
  const { model, env } = resolveModel(config, manifest, "monitor", "sonnet");
147
136
 
148
137
  // Spawn tmux session at project root with Claude Code (interactive mode).
149
- // Inject the monitor base definition via --append-system-prompt.
150
138
  const agentDefPath = join(projectRoot, ".overstory", "agent-defs", "monitor.md");
151
139
  const agentDefFile = Bun.file(agentDefPath);
152
140
  let claudeCmd = `claude --model ${model} --dangerously-skip-permissions`;
@@ -168,7 +156,7 @@ async function startMonitor(args: string[]): Promise<void> {
168
156
  capability: "monitor",
169
157
  worktreePath: projectRoot, // Monitor uses project root, not a worktree
170
158
  branchName: config.project.canonicalBranch, // Operates on canonical branch
171
- beadId: "", // No specific bead assignment
159
+ taskId: "", // No specific bead assignment
172
160
  tmuxSession,
173
161
  state: "booting",
174
162
  pid,
@@ -226,8 +214,8 @@ async function startMonitor(args: string[]): Promise<void> {
226
214
  * 2. Kill the tmux session (with process tree cleanup)
227
215
  * 3. Mark session as completed in SessionStore
228
216
  */
229
- async function stopMonitor(args: string[]): Promise<void> {
230
- const json = args.includes("--json");
217
+ async function stopMonitor(opts: { json: boolean }): Promise<void> {
218
+ const { json } = opts;
231
219
  const cwd = process.cwd();
232
220
  const config = await loadConfig(cwd);
233
221
  const projectRoot = config.project.root;
@@ -273,8 +261,8 @@ async function stopMonitor(args: string[]): Promise<void> {
273
261
  *
274
262
  * Checks session registry and tmux liveness to report actual state.
275
263
  */
276
- async function statusMonitor(args: string[]): Promise<void> {
277
- const json = args.includes("--json");
264
+ async function statusMonitor(opts: { json: boolean }): Promise<void> {
265
+ const { json } = opts;
278
266
  const cwd = process.cwd();
279
267
  const config = await loadConfig(cwd);
280
268
  const projectRoot = config.project.root;
@@ -301,8 +289,6 @@ async function statusMonitor(args: string[]): Promise<void> {
301
289
  const alive = await isSessionAlive(session.tmuxSession);
302
290
 
303
291
  // Reconcile state: if session says active but tmux is dead, update.
304
- // We already filtered out completed/zombie states above, so if tmux is dead
305
- // this session needs to be marked as zombie.
306
292
  if (!alive) {
307
293
  store.updateState(MONITOR_NAME, "zombie");
308
294
  store.updateLastActivity(MONITOR_NAME);
@@ -335,56 +321,65 @@ async function statusMonitor(args: string[]): Promise<void> {
335
321
  }
336
322
  }
337
323
 
338
- const MONITOR_HELP = `overstory monitor — Manage the persistent Tier 2 monitor agent
339
-
340
- Usage: overstory monitor <subcommand> [flags]
341
-
342
- Subcommands:
343
- start Start the monitor (spawns Claude Code at project root)
344
- stop Stop the monitor (kills tmux session)
345
- status Show monitor state
324
+ export function createMonitorCommand(): Command {
325
+ const cmd = new Command("monitor").description("Manage the persistent Tier 2 monitor agent");
326
+
327
+ cmd
328
+ .command("start")
329
+ .description("Start the monitor (spawns Claude Code at project root)")
330
+ .option("--attach", "Always attach to tmux session after start")
331
+ .option("--no-attach", "Never attach to tmux session after start")
332
+ .option("--json", "Output as JSON")
333
+ .action(async (opts: { attach?: boolean; json?: boolean }) => {
334
+ // opts.attach = true if --attach, false if --no-attach, undefined if neither
335
+ const shouldAttach = opts.attach !== undefined ? opts.attach : !!process.stdout.isTTY;
336
+ await startMonitor({ json: opts.json ?? false, attach: shouldAttach });
337
+ });
346
338
 
347
- Start options:
348
- --attach Always attach to tmux session after start
349
- --no-attach Never attach to tmux session after start
350
- Default: attach when running in an interactive TTY
339
+ cmd
340
+ .command("stop")
341
+ .description("Stop the monitor (kills tmux session)")
342
+ .option("--json", "Output as JSON")
343
+ .action(async (opts: { json?: boolean }) => {
344
+ await stopMonitor({ json: opts.json ?? false });
345
+ });
351
346
 
352
- General options:
353
- --json Output as JSON
354
- --help, -h Show this help
347
+ cmd
348
+ .command("status")
349
+ .description("Show monitor state")
350
+ .option("--json", "Output as JSON")
351
+ .action(async (opts: { json?: boolean }) => {
352
+ await statusMonitor({ json: opts.json ?? false });
353
+ });
355
354
 
356
- The monitor agent (Tier 2) continuously patrols the agent fleet by:
357
- - Checking agent health via overstory status
358
- - Sending progressive nudges to stalled agents
359
- - Escalating unresponsive agents to the coordinator
360
- - Producing periodic health summaries`;
355
+ return cmd;
356
+ }
361
357
 
362
358
  /**
363
359
  * Entry point for `overstory monitor <subcommand>`.
364
360
  */
365
361
  export async function monitorCommand(args: string[]): Promise<void> {
366
- if (args.includes("--help") || args.includes("-h") || args.length === 0) {
367
- process.stdout.write(`${MONITOR_HELP}\n`);
362
+ const cmd = createMonitorCommand();
363
+ cmd.exitOverride();
364
+
365
+ if (args.length === 0) {
366
+ process.stdout.write(cmd.helpInformation());
368
367
  return;
369
368
  }
370
369
 
371
- const subcommand = args[0];
372
- const subArgs = args.slice(1);
373
-
374
- switch (subcommand) {
375
- case "start":
376
- await startMonitor(subArgs);
377
- break;
378
- case "stop":
379
- await stopMonitor(subArgs);
380
- break;
381
- case "status":
382
- await statusMonitor(subArgs);
383
- break;
384
- default:
385
- throw new ValidationError(
386
- `Unknown monitor subcommand: ${subcommand}. Run 'overstory monitor --help' for usage.`,
387
- { field: "subcommand", value: subcommand },
388
- );
370
+ try {
371
+ await cmd.parseAsync(args, { from: "user" });
372
+ } catch (err: unknown) {
373
+ if (err && typeof err === "object" && "code" in err) {
374
+ const code = (err as { code: string }).code;
375
+ if (code === "commander.helpDisplayed" || code === "commander.version") {
376
+ return;
377
+ }
378
+ if (code === "commander.unknownCommand") {
379
+ const message = err instanceof Error ? err.message : String(err);
380
+ throw new ValidationError(message, { field: "subcommand" });
381
+ }
382
+ }
383
+ throw err;
389
384
  }
390
385
  }
@@ -46,7 +46,7 @@ function makeSession(overrides: Partial<AgentSession> = {}): AgentSession {
46
46
  capability: "builder",
47
47
  worktreePath: "/tmp/wt",
48
48
  branchName: "overstory/test-agent/task-1",
49
- beadId: "task-1",
49
+ taskId: "task-1",
50
50
  tmuxSession: "overstory-test-agent",
51
51
  state: "working",
52
52
  pid: 12345,