@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
@@ -12,42 +12,9 @@ import { mkdir } from "node:fs/promises";
12
12
  import { join } from "node:path";
13
13
  import { ValidationError } from "../errors.ts";
14
14
 
15
- /** Boolean flags that do NOT consume the next arg. */
16
- const BOOLEAN_FLAGS = new Set(["--help", "-h"]);
17
-
18
- /**
19
- * Parse a named flag value from args.
20
- */
21
- function getFlag(args: string[], flag: string): string | undefined {
22
- const idx = args.indexOf(flag);
23
- if (idx === -1 || idx + 1 >= args.length) {
24
- return undefined;
25
- }
26
- return args[idx + 1];
27
- }
28
-
29
- /**
30
- * Extract positional arguments, skipping flag-value pairs.
31
- */
32
- function getPositionalArgs(args: string[]): string[] {
33
- const positional: string[] = [];
34
- let i = 0;
35
- while (i < args.length) {
36
- const arg = args[i];
37
- if (arg?.startsWith("-")) {
38
- if (BOOLEAN_FLAGS.has(arg)) {
39
- i += 1;
40
- } else {
41
- i += 2;
42
- }
43
- } else {
44
- if (arg !== undefined) {
45
- positional.push(arg);
46
- }
47
- i += 1;
48
- }
49
- }
50
- return positional;
15
+ export interface SpecWriteOptions {
16
+ body?: string;
17
+ agent?: string;
51
18
  }
52
19
 
53
20
  /**
@@ -62,23 +29,6 @@ async function readStdin(): Promise<string> {
62
29
  return await new Response(Bun.stdin.stream()).text();
63
30
  }
64
31
 
65
- const SPEC_HELP = `overstory spec -- Manage task specifications
66
-
67
- Usage: overstory spec <subcommand> [args...]
68
-
69
- Subcommands:
70
- write <bead-id> Write a spec file to .overstory/specs/<bead-id>.md
71
-
72
- Options for 'write':
73
- --body <content> Spec content (or pipe via stdin)
74
- --agent <name> Agent writing the spec (for attribution)
75
- --help, -h Show this help
76
-
77
- Examples:
78
- overstory spec write task-abc --body "# Spec\\nDetails here..."
79
- echo "# Spec" | overstory spec write task-abc
80
- overstory spec write task-abc --body "..." --agent scout-1`;
81
-
82
32
  /**
83
33
  * Write a spec file to .overstory/specs/<bead-id>.md.
84
34
  *
@@ -112,57 +62,38 @@ export async function writeSpec(
112
62
  }
113
63
 
114
64
  /**
115
- * Entry point for `overstory spec <subcommand>`.
65
+ * Entry point for `overstory spec write <bead-id> [flags]`.
66
+ *
67
+ * @param beadId - The bead/task ID for the spec file
68
+ * @param opts - Command options
116
69
  */
117
- export async function specCommand(args: string[]): Promise<void> {
118
- if (args.includes("--help") || args.includes("-h") || args.length === 0) {
119
- process.stdout.write(`${SPEC_HELP}\n`);
120
- return;
70
+ export async function specWriteCommand(beadId: string, opts: SpecWriteOptions): Promise<void> {
71
+ if (!beadId || beadId.trim().length === 0) {
72
+ throw new ValidationError(
73
+ "Bead ID is required: overstory spec write <bead-id> --body <content>",
74
+ { field: "beadId" },
75
+ );
121
76
  }
122
77
 
123
- const subcommand = args[0];
124
- const subArgs = args.slice(1);
125
-
126
- switch (subcommand) {
127
- case "write": {
128
- const positional = getPositionalArgs(subArgs);
129
- const beadId = positional[0];
130
- if (!beadId || beadId.trim().length === 0) {
131
- throw new ValidationError(
132
- "Bead ID is required: overstory spec write <bead-id> --body <content>",
133
- { field: "beadId" },
134
- );
135
- }
136
-
137
- const agent = getFlag(subArgs, "--agent");
138
- let body = getFlag(subArgs, "--body");
78
+ let body = opts.body;
139
79
 
140
- // If no --body flag, try reading from stdin
141
- if (body === undefined) {
142
- const stdinContent = await readStdin();
143
- if (stdinContent.trim().length > 0) {
144
- body = stdinContent;
145
- }
146
- }
147
-
148
- if (body === undefined || body.trim().length === 0) {
149
- throw new ValidationError("Spec body is required: use --body <content> or pipe via stdin", {
150
- field: "body",
151
- });
152
- }
153
-
154
- const { resolveProjectRoot } = await import("../config.ts");
155
- const projectRoot = await resolveProjectRoot(process.cwd());
156
-
157
- const specPath = await writeSpec(projectRoot, beadId, body, agent);
158
- process.stdout.write(`${specPath}\n`);
159
- break;
80
+ // If no --body flag, try reading from stdin
81
+ if (body === undefined) {
82
+ const stdinContent = await readStdin();
83
+ if (stdinContent.trim().length > 0) {
84
+ body = stdinContent;
160
85
  }
86
+ }
161
87
 
162
- default:
163
- throw new ValidationError(
164
- `Unknown spec subcommand: ${subcommand}. Run 'overstory spec --help' for usage.`,
165
- { field: "subcommand", value: subcommand },
166
- );
88
+ if (body === undefined || body.trim().length === 0) {
89
+ throw new ValidationError("Spec body is required: use --body <content> or pipe via stdin", {
90
+ field: "body",
91
+ });
167
92
  }
93
+
94
+ const { resolveProjectRoot } = await import("../config.ts");
95
+ const projectRoot = await resolveProjectRoot(process.cwd());
96
+
97
+ const specPath = await writeSpec(projectRoot, beadId, body, opts.agent);
98
+ process.stdout.write(`${specPath}\n`);
168
99
  }
@@ -6,6 +6,7 @@
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 { ValidationError } from "../errors.ts";
11
12
  import { createMailStore } from "../mail/store.ts";
@@ -64,21 +65,6 @@ export async function getCachedTmuxSessions(
64
65
  }
65
66
  }
66
67
 
67
- /**
68
- * Parse a named flag value from args.
69
- */
70
- function getFlag(args: string[], flag: string): string | undefined {
71
- const idx = args.indexOf(flag);
72
- if (idx === -1 || idx + 1 >= args.length) {
73
- return undefined;
74
- }
75
- return args[idx + 1];
76
- }
77
-
78
- function hasFlag(args: string[], flag: string): boolean {
79
- return args.includes(flag);
80
- }
81
-
82
68
  /**
83
69
  * Format a duration in ms to a human-readable string.
84
70
  */
@@ -323,33 +309,21 @@ export function printStatus(data: StatusData): void {
323
309
  w(`📈 Sessions recorded: ${data.recentMetricsCount}\n`);
324
310
  }
325
311
 
326
- /**
327
- * Entry point for `overstory status [--json] [--watch]`.
328
- */
329
- const STATUS_HELP = `overstory status — Show all active agents and project state
330
-
331
- Usage: overstory status [--json] [--verbose] [--agent <name>] [--all]
332
-
333
- Options:
334
- --json Output as JSON
335
- --verbose Show extra detail per agent (worktree, logs, mail timestamps)
336
- --agent <name> Show unread mail for this agent (default: orchestrator)
337
- --all Show sessions from all runs (default: current run only)
338
- --watch (deprecated) Use 'overstory dashboard' for live monitoring
339
- --interval <ms> Poll interval for --watch in milliseconds (default: 3000)
340
- --help, -h Show this help`;
341
-
342
- export async function statusCommand(args: string[]): Promise<void> {
343
- if (args.includes("--help") || args.includes("-h")) {
344
- process.stdout.write(`${STATUS_HELP}\n`);
345
- return;
346
- }
312
+ interface StatusOpts {
313
+ json?: boolean;
314
+ watch?: boolean;
315
+ verbose?: boolean;
316
+ all?: boolean;
317
+ interval?: string;
318
+ agent?: string;
319
+ }
347
320
 
348
- const json = hasFlag(args, "--json");
349
- const watch = hasFlag(args, "--watch");
350
- const verbose = hasFlag(args, "--verbose");
351
- const all = hasFlag(args, "--all");
352
- const intervalStr = getFlag(args, "--interval");
321
+ async function executeStatus(opts: StatusOpts): Promise<void> {
322
+ const json = opts.json ?? false;
323
+ const watch = opts.watch ?? false;
324
+ const verbose = opts.verbose ?? false;
325
+ const all = opts.all ?? false;
326
+ const intervalStr = opts.interval;
353
327
  const interval = intervalStr ? Number.parseInt(intervalStr, 10) : 3000;
354
328
 
355
329
  if (Number.isNaN(interval) || interval < 500) {
@@ -359,7 +333,7 @@ export async function statusCommand(args: string[]): Promise<void> {
359
333
  });
360
334
  }
361
335
 
362
- const agentName = getFlag(args, "--agent") ?? "orchestrator";
336
+ const agentName = opts.agent ?? "orchestrator";
363
337
 
364
338
  const cwd = process.cwd();
365
339
  const config = await loadConfig(cwd);
@@ -396,3 +370,33 @@ export async function statusCommand(args: string[]): Promise<void> {
396
370
  }
397
371
  }
398
372
  }
373
+
374
+ export function createStatusCommand(): Command {
375
+ return new Command("status")
376
+ .description("Show all active agents and project state")
377
+ .option("--json", "Output as JSON")
378
+ .option("--verbose", "Show extra detail per agent (worktree, logs, mail timestamps)")
379
+ .option("--agent <name>", "Show unread mail for this agent (default: orchestrator)")
380
+ .option("--all", "Show sessions from all runs (default: current run only)")
381
+ .option("--watch", "(deprecated) Use 'overstory dashboard' for live monitoring")
382
+ .option("--interval <ms>", "Poll interval for --watch in milliseconds (default: 3000)")
383
+ .action(async (opts: StatusOpts) => {
384
+ await executeStatus(opts);
385
+ });
386
+ }
387
+
388
+ export async function statusCommand(args: string[]): Promise<void> {
389
+ const cmd = createStatusCommand();
390
+ cmd.exitOverride();
391
+ try {
392
+ await cmd.parseAsync(args, { from: "user" });
393
+ } catch (err: unknown) {
394
+ if (err && typeof err === "object" && "code" in err) {
395
+ const code = (err as { code: string }).code;
396
+ if (code === "commander.helpDisplayed" || code === "commander.version") {
397
+ return;
398
+ }
399
+ }
400
+ throw err;
401
+ }
402
+ }
@@ -207,37 +207,15 @@ function makeDeps(
207
207
 
208
208
  // --- Tests ---
209
209
 
210
- describe("stopCommand help", () => {
211
- test("--help outputs help text", async () => {
212
- const output = await captureStdout(() => stopCommand(["--help"]));
213
- expect(output).toContain("overstory stop");
214
- expect(output).toContain("<agent-name>");
215
- expect(output).toContain("--force");
216
- expect(output).toContain("--clean-worktree");
217
- expect(output).toContain("--json");
218
- });
219
-
220
- test("-h outputs help text", async () => {
221
- const output = await captureStdout(() => stopCommand(["-h"]));
222
- expect(output).toContain("overstory stop");
223
- expect(output).toContain("<agent-name>");
224
- });
225
- });
226
-
227
210
  describe("stopCommand validation", () => {
228
- test("throws ValidationError when no agent name provided", async () => {
211
+ test("throws ValidationError when agent name is empty string", async () => {
229
212
  const { deps } = makeDeps();
230
- await expect(stopCommand([], deps)).rejects.toThrow(ValidationError);
231
- });
232
-
233
- test("throws ValidationError when only flags are provided (no agent name)", async () => {
234
- const { deps } = makeDeps();
235
- await expect(stopCommand(["--json"], deps)).rejects.toThrow(ValidationError);
213
+ await expect(stopCommand("", {}, deps)).rejects.toThrow(ValidationError);
236
214
  });
237
215
 
238
216
  test("throws AgentError when agent not found", async () => {
239
217
  const { deps } = makeDeps();
240
- await expect(stopCommand(["nonexistent-agent"], deps)).rejects.toThrow(AgentError);
218
+ await expect(stopCommand("nonexistent-agent", {}, deps)).rejects.toThrow(AgentError);
241
219
  });
242
220
 
243
221
  test("throws AgentError when agent is already completed", async () => {
@@ -245,8 +223,8 @@ describe("stopCommand validation", () => {
245
223
  saveSessionsToDb([session]);
246
224
 
247
225
  const { deps } = makeDeps();
248
- await expect(stopCommand(["my-builder"], deps)).rejects.toThrow(AgentError);
249
- await expect(stopCommand(["my-builder"], deps)).rejects.toThrow(/already completed/);
226
+ await expect(stopCommand("my-builder", {}, deps)).rejects.toThrow(AgentError);
227
+ await expect(stopCommand("my-builder", {}, deps)).rejects.toThrow(/already completed/);
250
228
  });
251
229
 
252
230
  test("throws AgentError when agent is already zombie", async () => {
@@ -254,8 +232,8 @@ describe("stopCommand validation", () => {
254
232
  saveSessionsToDb([session]);
255
233
 
256
234
  const { deps } = makeDeps();
257
- await expect(stopCommand(["my-builder"], deps)).rejects.toThrow(AgentError);
258
- await expect(stopCommand(["my-builder"], deps)).rejects.toThrow(/zombie/);
235
+ await expect(stopCommand("my-builder", {}, deps)).rejects.toThrow(AgentError);
236
+ await expect(stopCommand("my-builder", {}, deps)).rejects.toThrow(/zombie/);
259
237
  });
260
238
  });
261
239
 
@@ -265,7 +243,7 @@ describe("stopCommand stop behavior", () => {
265
243
  saveSessionsToDb([session]);
266
244
 
267
245
  const { deps, tmuxCalls } = makeDeps({ [session.tmuxSession]: true });
268
- const output = await captureStdout(() => stopCommand(["my-builder"], deps));
246
+ const output = await captureStdout(() => stopCommand("my-builder", {}, deps));
269
247
 
270
248
  expect(output).toContain(`Agent "my-builder" stopped`);
271
249
  expect(output).toContain(`Tmux session killed: ${session.tmuxSession}`);
@@ -284,7 +262,7 @@ describe("stopCommand stop behavior", () => {
284
262
  saveSessionsToDb([session]);
285
263
 
286
264
  const { deps, tmuxCalls } = makeDeps({ [session.tmuxSession]: true });
287
- await stopCommand(["my-builder"], deps);
265
+ await stopCommand("my-builder", {}, deps);
288
266
 
289
267
  expect(tmuxCalls.killSession).toHaveLength(1);
290
268
  const { store } = openSessionStore(overstoryDir);
@@ -298,7 +276,7 @@ describe("stopCommand stop behavior", () => {
298
276
  saveSessionsToDb([session]);
299
277
 
300
278
  const { deps, tmuxCalls } = makeDeps({ [session.tmuxSession]: true });
301
- await stopCommand(["my-builder"], deps);
279
+ await stopCommand("my-builder", {}, deps);
302
280
 
303
281
  expect(tmuxCalls.killSession).toHaveLength(1);
304
282
  const { store } = openSessionStore(overstoryDir);
@@ -313,7 +291,7 @@ describe("stopCommand stop behavior", () => {
313
291
 
314
292
  // tmux session is NOT alive
315
293
  const { deps, tmuxCalls } = makeDeps({ [session.tmuxSession]: false });
316
- const output = await captureStdout(() => stopCommand(["my-builder"], deps));
294
+ const output = await captureStdout(() => stopCommand("my-builder", {}, deps));
317
295
 
318
296
  expect(output).toContain("Tmux session was already dead");
319
297
  expect(tmuxCalls.killSession).toHaveLength(0);
@@ -332,7 +310,7 @@ describe("stopCommand --json output", () => {
332
310
  saveSessionsToDb([session]);
333
311
 
334
312
  const { deps } = makeDeps({ [session.tmuxSession]: true });
335
- const output = await captureStdout(() => stopCommand(["my-builder", "--json"], deps));
313
+ const output = await captureStdout(() => stopCommand("my-builder", { json: true }, deps));
336
314
 
337
315
  const parsed = JSON.parse(output.trim()) as Record<string, unknown>;
338
316
  expect(parsed.stopped).toBe(true);
@@ -350,7 +328,7 @@ describe("stopCommand --json output", () => {
350
328
 
351
329
  const { deps } = makeDeps({ [session.tmuxSession]: true });
352
330
  const output = await captureStdout(() =>
353
- stopCommand(["my-builder", "--json", "--force"], deps),
331
+ stopCommand("my-builder", { json: true, force: true }, deps),
354
332
  );
355
333
 
356
334
  const parsed = JSON.parse(output.trim()) as Record<string, unknown>;
@@ -364,7 +342,9 @@ describe("stopCommand --clean-worktree", () => {
364
342
  saveSessionsToDb([session]);
365
343
 
366
344
  const { deps, worktreeCalls } = makeDeps({ [session.tmuxSession]: true });
367
- const output = await captureStdout(() => stopCommand(["my-builder", "--clean-worktree"], deps));
345
+ const output = await captureStdout(() =>
346
+ stopCommand("my-builder", { cleanWorktree: true }, deps),
347
+ );
368
348
 
369
349
  expect(output).toContain(`Worktree removed: ${session.worktreePath}`);
370
350
  expect(worktreeCalls.remove).toHaveLength(1);
@@ -376,7 +356,9 @@ describe("stopCommand --clean-worktree", () => {
376
356
  saveSessionsToDb([session]);
377
357
 
378
358
  const { deps, worktreeCalls } = makeDeps({ [session.tmuxSession]: true });
379
- await captureStdout(() => stopCommand(["my-builder", "--clean-worktree", "--force"], deps));
359
+ await captureStdout(() =>
360
+ stopCommand("my-builder", { cleanWorktree: true, force: true }, deps),
361
+ );
380
362
 
381
363
  expect(worktreeCalls.remove).toHaveLength(1);
382
364
  expect(worktreeCalls.remove[0]?.options?.force).toBe(true);
@@ -389,7 +371,7 @@ describe("stopCommand --clean-worktree", () => {
389
371
 
390
372
  const { deps } = makeDeps({ [session.tmuxSession]: true }, { shouldFail: true });
391
373
  const { stderr, stdout } = await captureStderr(() =>
392
- stopCommand(["my-builder", "--clean-worktree"], deps),
374
+ stopCommand("my-builder", { cleanWorktree: true }, deps),
393
375
  );
394
376
 
395
377
  // Agent was still stopped
@@ -410,7 +392,7 @@ describe("stopCommand --clean-worktree", () => {
410
392
 
411
393
  const { deps } = makeDeps({ [session.tmuxSession]: true }, { shouldFail: true });
412
394
  const { stdout } = await captureStderr(() =>
413
- stopCommand(["my-builder", "--clean-worktree", "--json"], deps),
395
+ stopCommand("my-builder", { cleanWorktree: true, json: true }, deps),
414
396
  );
415
397
 
416
398
  const parsed = JSON.parse(stdout.trim()) as Record<string, unknown>;
@@ -15,6 +15,12 @@ import { openSessionStore } from "../sessions/compat.ts";
15
15
  import { removeWorktree } from "../worktree/manager.ts";
16
16
  import { isSessionAlive, killSession } from "../worktree/tmux.ts";
17
17
 
18
+ export interface StopOptions {
19
+ force?: boolean;
20
+ cleanWorktree?: boolean;
21
+ json?: boolean;
22
+ }
23
+
18
24
  /** Dependency injection for testing. Uses real implementations when omitted. */
19
25
  export interface StopDeps {
20
26
  _tmux?: {
@@ -30,50 +36,29 @@ export interface StopDeps {
30
36
  };
31
37
  }
32
38
 
33
- const STOP_HELP = `overstory stop — Terminate a running agent
34
-
35
- Usage: overstory stop <agent-name> [flags]
36
-
37
- Arguments:
38
- <agent-name> Name of the agent to stop
39
-
40
- Options:
41
- --force Force kill and force-delete branch when cleaning worktree
42
- --clean-worktree Remove the agent's worktree after stopping
43
- --json Output as JSON
44
- --help, -h Show this help
45
-
46
- Examples:
47
- overstory stop my-builder
48
- overstory stop my-builder --clean-worktree
49
- overstory stop my-builder --clean-worktree --force
50
- overstory stop my-builder --json`;
51
-
52
39
  /**
53
40
  * Entry point for `overstory stop <agent-name>`.
54
41
  *
55
- * @param args - CLI arguments after "stop"
42
+ * @param agentName - Name of the agent to stop
43
+ * @param opts - Command options
56
44
  * @param deps - Optional dependency injection for testing (tmux, worktree)
57
45
  */
58
- export async function stopCommand(args: string[], deps: StopDeps = {}): Promise<void> {
59
- if (args.includes("--help") || args.includes("-h")) {
60
- process.stdout.write(`${STOP_HELP}\n`);
61
- return;
62
- }
63
-
64
- const json = args.includes("--json");
65
- const force = args.includes("--force");
66
- const cleanWorktree = args.includes("--clean-worktree");
67
-
68
- // First non-flag arg is the agent name
69
- const agentName = args.find((a) => !a.startsWith("-"));
70
- if (!agentName) {
46
+ export async function stopCommand(
47
+ agentName: string,
48
+ opts: StopOptions,
49
+ deps: StopDeps = {},
50
+ ): Promise<void> {
51
+ if (!agentName || agentName.trim().length === 0) {
71
52
  throw new ValidationError("Missing required argument: <agent-name>", {
72
53
  field: "agentName",
73
54
  value: "",
74
55
  });
75
56
  }
76
57
 
58
+ const json = opts.json ?? false;
59
+ const force = opts.force ?? false;
60
+ const cleanWorktree = opts.cleanWorktree ?? false;
61
+
77
62
  const tmux = deps._tmux ?? { isSessionAlive, killSession };
78
63
  const worktree = deps._worktree ?? { remove: removeWorktree };
79
64
 
@@ -111,12 +111,10 @@ describe("supervisorCommand", () => {
111
111
 
112
112
  try {
113
113
  await supervisorCommand(["--help"]);
114
- expect(output).toContain("overstory supervisor");
114
+ expect(output).toContain("supervisor");
115
115
  expect(output).toContain("start");
116
116
  expect(output).toContain("stop");
117
117
  expect(output).toContain("status");
118
- expect(output).toContain("--task");
119
- expect(output).toContain("--name");
120
118
  } finally {
121
119
  process.stdout.write = originalWrite;
122
120
  }
@@ -132,7 +130,7 @@ describe("supervisorCommand", () => {
132
130
 
133
131
  try {
134
132
  await supervisorCommand(["-h"]);
135
- expect(output).toContain("overstory supervisor");
133
+ expect(output).toContain("supervisor");
136
134
  } finally {
137
135
  process.stdout.write = originalWrite;
138
136
  }
@@ -148,7 +146,7 @@ describe("supervisorCommand", () => {
148
146
 
149
147
  try {
150
148
  await supervisorCommand([]);
151
- expect(output).toContain("overstory supervisor");
149
+ expect(output).toContain("supervisor");
152
150
  } finally {
153
151
  process.stdout.write = originalWrite;
154
152
  }