@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
@@ -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";
@@ -13,6 +14,7 @@ import { createMergeQueue } from "../merge/queue.ts";
13
14
  import { createMetricsStore } from "../metrics/store.ts";
14
15
  import { openSessionStore } from "../sessions/compat.ts";
15
16
  import type { AgentSession } from "../types.ts";
17
+ import { evaluateHealth } from "../watchdog/health.ts";
16
18
  import { listWorktrees } from "../worktree/manager.ts";
17
19
  import { listSessions } from "../worktree/tmux.ts";
18
20
 
@@ -64,21 +66,6 @@ export async function getCachedTmuxSessions(
64
66
  }
65
67
  }
66
68
 
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
69
  /**
83
70
  * Format a duration in ms to a human-readable string.
84
71
  */
@@ -150,23 +137,23 @@ export async function gatherStatus(
150
137
 
151
138
  const tmuxSessions = await getCachedTmuxSessions();
152
139
 
153
- // Reconcile agent states: if tmux session is dead but agent state
154
- // indicates it should be alive, mark it as zombie
140
+ // Reconcile agent states using the same health evaluation as the
141
+ // dashboard and watchdog. This handles:
142
+ // 1. tmux dead -> zombie (regardless of recorded state)
143
+ // 2. persistent capabilities (coordinator, monitor) booting -> working when tmux alive
144
+ // 3. time-based stale/zombie detection for non-persistent agents
155
145
  const tmuxSessionNames = new Set(tmuxSessions.map((s) => s.name));
146
+ const healthThresholds = { staleMs: 300_000, zombieMs: 600_000 };
156
147
  for (const session of sessions) {
157
- if (
158
- session.state === "booting" ||
159
- session.state === "working" ||
160
- session.state === "stalled"
161
- ) {
162
- const tmuxAlive = tmuxSessionNames.has(session.tmuxSession);
163
- if (!tmuxAlive) {
164
- try {
165
- store.updateState(session.agentName, "zombie");
166
- session.state = "zombie";
167
- } catch {
168
- // Best effort: don't fail status display if update fails
169
- }
148
+ if (session.state === "completed") continue;
149
+ const tmuxAlive = tmuxSessionNames.has(session.tmuxSession);
150
+ const check = evaluateHealth(session, tmuxAlive, healthThresholds);
151
+ if (check.state !== session.state) {
152
+ try {
153
+ store.updateState(session.agentName, check.state);
154
+ session.state = check.state;
155
+ } catch {
156
+ // Best effort: don't fail status display if update fails
170
157
  }
171
158
  }
172
159
  }
@@ -287,7 +274,7 @@ export function printStatus(data: StatusData): void {
287
274
  const tmuxAlive = tmuxSessionNames.has(agent.tmuxSession);
288
275
  const aliveMarker = tmuxAlive ? "●" : "○";
289
276
  w(` ${aliveMarker} ${agent.agentName} [${agent.capability}] `);
290
- w(`${agent.state} | ${agent.beadId} | ${duration}\n`);
277
+ w(`${agent.state} | ${agent.taskId} | ${duration}\n`);
291
278
 
292
279
  const detail = data.verboseDetails?.[agent.agentName];
293
280
  if (detail) {
@@ -323,33 +310,21 @@ export function printStatus(data: StatusData): void {
323
310
  w(`📈 Sessions recorded: ${data.recentMetricsCount}\n`);
324
311
  }
325
312
 
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
- }
313
+ interface StatusOpts {
314
+ json?: boolean;
315
+ watch?: boolean;
316
+ verbose?: boolean;
317
+ all?: boolean;
318
+ interval?: string;
319
+ agent?: string;
320
+ }
347
321
 
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");
322
+ async function executeStatus(opts: StatusOpts): Promise<void> {
323
+ const json = opts.json ?? false;
324
+ const watch = opts.watch ?? false;
325
+ const verbose = opts.verbose ?? false;
326
+ const all = opts.all ?? false;
327
+ const intervalStr = opts.interval;
353
328
  const interval = intervalStr ? Number.parseInt(intervalStr, 10) : 3000;
354
329
 
355
330
  if (Number.isNaN(interval) || interval < 500) {
@@ -359,7 +334,7 @@ export async function statusCommand(args: string[]): Promise<void> {
359
334
  });
360
335
  }
361
336
 
362
- const agentName = getFlag(args, "--agent") ?? "orchestrator";
337
+ const agentName = opts.agent ?? "orchestrator";
363
338
 
364
339
  const cwd = process.cwd();
365
340
  const config = await loadConfig(cwd);
@@ -396,3 +371,33 @@ export async function statusCommand(args: string[]): Promise<void> {
396
371
  }
397
372
  }
398
373
  }
374
+
375
+ export function createStatusCommand(): Command {
376
+ return new Command("status")
377
+ .description("Show all active agents and project state")
378
+ .option("--json", "Output as JSON")
379
+ .option("--verbose", "Show extra detail per agent (worktree, logs, mail timestamps)")
380
+ .option("--agent <name>", "Show unread mail for this agent (default: orchestrator)")
381
+ .option("--all", "Show sessions from all runs (default: current run only)")
382
+ .option("--watch", "(deprecated) Use 'overstory dashboard' for live monitoring")
383
+ .option("--interval <ms>", "Poll interval for --watch in milliseconds (default: 3000)")
384
+ .action(async (opts: StatusOpts) => {
385
+ await executeStatus(opts);
386
+ });
387
+ }
388
+
389
+ export async function statusCommand(args: string[]): Promise<void> {
390
+ const cmd = createStatusCommand();
391
+ cmd.exitOverride();
392
+ try {
393
+ await cmd.parseAsync(args, { from: "user" });
394
+ } catch (err: unknown) {
395
+ if (err && typeof err === "object" && "code" in err) {
396
+ const code = (err as { code: string }).code;
397
+ if (code === "commander.helpDisplayed" || code === "commander.version") {
398
+ return;
399
+ }
400
+ }
401
+ throw err;
402
+ }
403
+ }
@@ -137,7 +137,7 @@ function makeAgentSession(overrides: Partial<AgentSession> = {}): AgentSession {
137
137
  capability: "builder",
138
138
  worktreePath: join(tempDir, ".overstory", "worktrees", "my-builder"),
139
139
  branchName: "overstory/my-builder/bead-123",
140
- beadId: "bead-123",
140
+ taskId: "bead-123",
141
141
  tmuxSession: "overstory-test-project-my-builder",
142
142
  state: "working",
143
143
  pid: 99999,
@@ -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
 
@@ -11,10 +11,10 @@ import { buildSupervisorBeacon, supervisorCommand } from "./supervisor.ts";
11
11
  */
12
12
 
13
13
  describe("buildSupervisorBeacon", () => {
14
- test("contains agent name and beadId from opts", () => {
14
+ test("contains agent name and taskId from opts", () => {
15
15
  const beacon = buildSupervisorBeacon({
16
16
  name: "supervisor-1",
17
- beadId: "task-abc123",
17
+ taskId: "task-abc123",
18
18
  depth: 1,
19
19
  parent: "coordinator",
20
20
  });
@@ -26,7 +26,7 @@ describe("buildSupervisorBeacon", () => {
26
26
  test("contains [OVERSTORY] prefix", () => {
27
27
  const beacon = buildSupervisorBeacon({
28
28
  name: "supervisor-1",
29
- beadId: "task-1",
29
+ taskId: "task-1",
30
30
  depth: 1,
31
31
  parent: "coordinator",
32
32
  });
@@ -37,7 +37,7 @@ describe("buildSupervisorBeacon", () => {
37
37
  test("contains (supervisor) designation", () => {
38
38
  const beacon = buildSupervisorBeacon({
39
39
  name: "supervisor-1",
40
- beadId: "task-1",
40
+ taskId: "task-1",
41
41
  depth: 1,
42
42
  parent: "coordinator",
43
43
  });
@@ -48,7 +48,7 @@ describe("buildSupervisorBeacon", () => {
48
48
  test("contains depth and parent info from opts", () => {
49
49
  const beacon = buildSupervisorBeacon({
50
50
  name: "supervisor-1",
51
- beadId: "task-1",
51
+ taskId: "task-1",
52
52
  depth: 2,
53
53
  parent: "lead-cli",
54
54
  });
@@ -60,7 +60,7 @@ describe("buildSupervisorBeacon", () => {
60
60
  test("contains startup instructions", () => {
61
61
  const beacon = buildSupervisorBeacon({
62
62
  name: "supervisor-1",
63
- beadId: "task-1",
63
+ taskId: "task-1",
64
64
  depth: 1,
65
65
  parent: "coordinator",
66
66
  });
@@ -71,7 +71,7 @@ describe("buildSupervisorBeacon", () => {
71
71
  // Should include mail check with agent name
72
72
  expect(beacon).toContain("overstory mail check --agent supervisor-1");
73
73
 
74
- // Should include bd show with beadId
74
+ // Should include bd show with taskId
75
75
  expect(beacon).toContain("bd show task-1");
76
76
  });
77
77
 
@@ -79,13 +79,13 @@ describe("buildSupervisorBeacon", () => {
79
79
  const before = new Date();
80
80
  const beacon = buildSupervisorBeacon({
81
81
  name: "supervisor-1",
82
- beadId: "task-1",
82
+ taskId: "task-1",
83
83
  depth: 1,
84
84
  parent: "coordinator",
85
85
  });
86
86
  const after = new Date();
87
87
 
88
- // Extract timestamp from beacon (format: [OVERSTORY] {name} (supervisor) {timestamp} task:{beadId})
88
+ // Extract timestamp from beacon (format: [OVERSTORY] {name} (supervisor) {timestamp} task:{taskId})
89
89
  const timestampMatch = beacon.match(/\(supervisor\)\s+(\S+)\s+task:/);
90
90
  expect(timestampMatch).toBeTruthy();
91
91
 
@@ -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
  }