@os-eco/overstory-cli 0.6.1

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 (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +381 -0
  3. package/agents/builder.md +137 -0
  4. package/agents/coordinator.md +263 -0
  5. package/agents/lead.md +301 -0
  6. package/agents/merger.md +160 -0
  7. package/agents/monitor.md +214 -0
  8. package/agents/reviewer.md +140 -0
  9. package/agents/scout.md +119 -0
  10. package/agents/supervisor.md +423 -0
  11. package/package.json +47 -0
  12. package/src/agents/checkpoint.test.ts +88 -0
  13. package/src/agents/checkpoint.ts +101 -0
  14. package/src/agents/hooks-deployer.test.ts +2040 -0
  15. package/src/agents/hooks-deployer.ts +607 -0
  16. package/src/agents/identity.test.ts +603 -0
  17. package/src/agents/identity.ts +384 -0
  18. package/src/agents/lifecycle.test.ts +196 -0
  19. package/src/agents/lifecycle.ts +183 -0
  20. package/src/agents/manifest.test.ts +746 -0
  21. package/src/agents/manifest.ts +354 -0
  22. package/src/agents/overlay.test.ts +676 -0
  23. package/src/agents/overlay.ts +308 -0
  24. package/src/beads/client.test.ts +217 -0
  25. package/src/beads/client.ts +202 -0
  26. package/src/beads/molecules.test.ts +338 -0
  27. package/src/beads/molecules.ts +198 -0
  28. package/src/commands/agents.test.ts +322 -0
  29. package/src/commands/agents.ts +287 -0
  30. package/src/commands/clean.test.ts +670 -0
  31. package/src/commands/clean.ts +618 -0
  32. package/src/commands/completions.test.ts +342 -0
  33. package/src/commands/completions.ts +887 -0
  34. package/src/commands/coordinator.test.ts +1530 -0
  35. package/src/commands/coordinator.ts +733 -0
  36. package/src/commands/costs.test.ts +1119 -0
  37. package/src/commands/costs.ts +564 -0
  38. package/src/commands/dashboard.test.ts +308 -0
  39. package/src/commands/dashboard.ts +838 -0
  40. package/src/commands/doctor.test.ts +294 -0
  41. package/src/commands/doctor.ts +213 -0
  42. package/src/commands/errors.test.ts +647 -0
  43. package/src/commands/errors.ts +248 -0
  44. package/src/commands/feed.test.ts +578 -0
  45. package/src/commands/feed.ts +361 -0
  46. package/src/commands/group.test.ts +262 -0
  47. package/src/commands/group.ts +511 -0
  48. package/src/commands/hooks.test.ts +458 -0
  49. package/src/commands/hooks.ts +253 -0
  50. package/src/commands/init.test.ts +347 -0
  51. package/src/commands/init.ts +650 -0
  52. package/src/commands/inspect.test.ts +670 -0
  53. package/src/commands/inspect.ts +431 -0
  54. package/src/commands/log.test.ts +1454 -0
  55. package/src/commands/log.ts +724 -0
  56. package/src/commands/logs.test.ts +379 -0
  57. package/src/commands/logs.ts +546 -0
  58. package/src/commands/mail.test.ts +1270 -0
  59. package/src/commands/mail.ts +771 -0
  60. package/src/commands/merge.test.ts +670 -0
  61. package/src/commands/merge.ts +355 -0
  62. package/src/commands/metrics.test.ts +444 -0
  63. package/src/commands/metrics.ts +143 -0
  64. package/src/commands/monitor.test.ts +191 -0
  65. package/src/commands/monitor.ts +390 -0
  66. package/src/commands/nudge.test.ts +230 -0
  67. package/src/commands/nudge.ts +372 -0
  68. package/src/commands/prime.test.ts +470 -0
  69. package/src/commands/prime.ts +381 -0
  70. package/src/commands/replay.test.ts +741 -0
  71. package/src/commands/replay.ts +360 -0
  72. package/src/commands/run.test.ts +431 -0
  73. package/src/commands/run.ts +351 -0
  74. package/src/commands/sling.test.ts +657 -0
  75. package/src/commands/sling.ts +661 -0
  76. package/src/commands/spec.test.ts +203 -0
  77. package/src/commands/spec.ts +168 -0
  78. package/src/commands/status.test.ts +430 -0
  79. package/src/commands/status.ts +398 -0
  80. package/src/commands/stop.test.ts +420 -0
  81. package/src/commands/stop.ts +151 -0
  82. package/src/commands/supervisor.test.ts +187 -0
  83. package/src/commands/supervisor.ts +535 -0
  84. package/src/commands/trace.test.ts +745 -0
  85. package/src/commands/trace.ts +325 -0
  86. package/src/commands/watch.test.ts +145 -0
  87. package/src/commands/watch.ts +247 -0
  88. package/src/commands/worktree.test.ts +786 -0
  89. package/src/commands/worktree.ts +311 -0
  90. package/src/config.test.ts +822 -0
  91. package/src/config.ts +829 -0
  92. package/src/doctor/agents.test.ts +454 -0
  93. package/src/doctor/agents.ts +396 -0
  94. package/src/doctor/config-check.test.ts +190 -0
  95. package/src/doctor/config-check.ts +183 -0
  96. package/src/doctor/consistency.test.ts +651 -0
  97. package/src/doctor/consistency.ts +294 -0
  98. package/src/doctor/databases.test.ts +290 -0
  99. package/src/doctor/databases.ts +218 -0
  100. package/src/doctor/dependencies.test.ts +184 -0
  101. package/src/doctor/dependencies.ts +175 -0
  102. package/src/doctor/logs.test.ts +251 -0
  103. package/src/doctor/logs.ts +295 -0
  104. package/src/doctor/merge-queue.test.ts +216 -0
  105. package/src/doctor/merge-queue.ts +144 -0
  106. package/src/doctor/structure.test.ts +291 -0
  107. package/src/doctor/structure.ts +198 -0
  108. package/src/doctor/types.ts +37 -0
  109. package/src/doctor/version.test.ts +136 -0
  110. package/src/doctor/version.ts +129 -0
  111. package/src/e2e/init-sling-lifecycle.test.ts +277 -0
  112. package/src/errors.ts +217 -0
  113. package/src/events/store.test.ts +660 -0
  114. package/src/events/store.ts +369 -0
  115. package/src/events/tool-filter.test.ts +330 -0
  116. package/src/events/tool-filter.ts +126 -0
  117. package/src/index.ts +316 -0
  118. package/src/insights/analyzer.test.ts +466 -0
  119. package/src/insights/analyzer.ts +203 -0
  120. package/src/logging/color.test.ts +142 -0
  121. package/src/logging/color.ts +71 -0
  122. package/src/logging/logger.test.ts +813 -0
  123. package/src/logging/logger.ts +266 -0
  124. package/src/logging/reporter.test.ts +259 -0
  125. package/src/logging/reporter.ts +109 -0
  126. package/src/logging/sanitizer.test.ts +190 -0
  127. package/src/logging/sanitizer.ts +57 -0
  128. package/src/mail/broadcast.test.ts +203 -0
  129. package/src/mail/broadcast.ts +92 -0
  130. package/src/mail/client.test.ts +773 -0
  131. package/src/mail/client.ts +223 -0
  132. package/src/mail/store.test.ts +705 -0
  133. package/src/mail/store.ts +387 -0
  134. package/src/merge/queue.test.ts +359 -0
  135. package/src/merge/queue.ts +231 -0
  136. package/src/merge/resolver.test.ts +1345 -0
  137. package/src/merge/resolver.ts +645 -0
  138. package/src/metrics/store.test.ts +667 -0
  139. package/src/metrics/store.ts +445 -0
  140. package/src/metrics/summary.test.ts +398 -0
  141. package/src/metrics/summary.ts +178 -0
  142. package/src/metrics/transcript.test.ts +356 -0
  143. package/src/metrics/transcript.ts +175 -0
  144. package/src/mulch/client.test.ts +671 -0
  145. package/src/mulch/client.ts +332 -0
  146. package/src/sessions/compat.test.ts +280 -0
  147. package/src/sessions/compat.ts +104 -0
  148. package/src/sessions/store.test.ts +873 -0
  149. package/src/sessions/store.ts +494 -0
  150. package/src/test-helpers.test.ts +124 -0
  151. package/src/test-helpers.ts +126 -0
  152. package/src/tracker/beads.ts +56 -0
  153. package/src/tracker/factory.test.ts +80 -0
  154. package/src/tracker/factory.ts +64 -0
  155. package/src/tracker/seeds.ts +182 -0
  156. package/src/tracker/types.ts +52 -0
  157. package/src/types.ts +724 -0
  158. package/src/watchdog/daemon.test.ts +1975 -0
  159. package/src/watchdog/daemon.ts +671 -0
  160. package/src/watchdog/health.test.ts +431 -0
  161. package/src/watchdog/health.ts +264 -0
  162. package/src/watchdog/triage.test.ts +164 -0
  163. package/src/watchdog/triage.ts +179 -0
  164. package/src/worktree/manager.test.ts +439 -0
  165. package/src/worktree/manager.ts +198 -0
  166. package/src/worktree/tmux.test.ts +1009 -0
  167. package/src/worktree/tmux.ts +509 -0
  168. package/templates/CLAUDE.md.tmpl +89 -0
  169. package/templates/hooks.json.tmpl +105 -0
  170. package/templates/overlay.md.tmpl +81 -0
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Beads molecule management helpers.
3
+ *
4
+ * Wraps `bd mol` commands via Bun.spawn for multi-step workflow prototypes.
5
+ * Molecules are templates with ordered steps. "Pouring" a prototype creates
6
+ * actual issues with dependencies pre-wired.
7
+ *
8
+ * Zero runtime dependencies — only Bun built-in APIs.
9
+ */
10
+
11
+ import { AgentError } from "../errors.ts";
12
+
13
+ // === Types ===
14
+
15
+ export interface MoleculeStep {
16
+ title: string;
17
+ type?: string;
18
+ }
19
+
20
+ export interface MoleculePrototype {
21
+ id: string;
22
+ name: string;
23
+ stepCount: number;
24
+ }
25
+
26
+ export interface ConvoyStatus {
27
+ total: number;
28
+ completed: number;
29
+ inProgress: number;
30
+ blocked: number;
31
+ }
32
+
33
+ // === Internal helpers ===
34
+
35
+ /**
36
+ * Run a shell command and capture its output.
37
+ */
38
+ async function runCommand(
39
+ cmd: string[],
40
+ cwd: string,
41
+ ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
42
+ const proc = Bun.spawn(cmd, {
43
+ cwd,
44
+ stdout: "pipe",
45
+ stderr: "pipe",
46
+ });
47
+ const stdout = await new Response(proc.stdout).text();
48
+ const stderr = await new Response(proc.stderr).text();
49
+ const exitCode = await proc.exited;
50
+ return { stdout, stderr, exitCode };
51
+ }
52
+
53
+ /**
54
+ * Run a `bd` subcommand and throw on failure.
55
+ */
56
+ async function runBd(
57
+ args: string[],
58
+ cwd: string,
59
+ context: string,
60
+ ): Promise<{ stdout: string; stderr: string }> {
61
+ const { stdout, stderr, exitCode } = await runCommand(["bd", ...args], cwd);
62
+ if (exitCode !== 0) {
63
+ throw new AgentError(`bd ${context} failed (exit ${exitCode}): ${stderr.trim()}`);
64
+ }
65
+ return { stdout, stderr };
66
+ }
67
+
68
+ /**
69
+ * Parse JSON output from a bd command.
70
+ * Handles the case where output may be empty or malformed.
71
+ */
72
+ function parseJsonOutput<T>(stdout: string, context: string): T {
73
+ const trimmed = stdout.trim();
74
+ if (trimmed === "") {
75
+ throw new AgentError(`Empty output from bd ${context}`);
76
+ }
77
+ try {
78
+ return JSON.parse(trimmed) as T;
79
+ } catch {
80
+ throw new AgentError(
81
+ `Failed to parse JSON output from bd ${context}: ${trimmed.slice(0, 200)}`,
82
+ );
83
+ }
84
+ }
85
+
86
+ // === Public API ===
87
+
88
+ /**
89
+ * Create a molecule prototype with ordered steps.
90
+ *
91
+ * First creates the prototype via `bd mol create`, then adds each step
92
+ * in order via `bd mol step add`. Returns the prototype ID.
93
+ *
94
+ * @param cwd - Working directory where bd commands should run
95
+ * @param options - Prototype name and ordered steps
96
+ * @returns The molecule prototype ID
97
+ */
98
+ export async function createMoleculePrototype(
99
+ cwd: string,
100
+ options: { name: string; steps: MoleculeStep[] },
101
+ ): Promise<string> {
102
+ const { stdout } = await runBd(
103
+ ["mol", "create", "--name", options.name, "--json"],
104
+ cwd,
105
+ "mol create",
106
+ );
107
+ const result = parseJsonOutput<{ id: string }>(stdout, "mol create");
108
+ const molId = result.id;
109
+
110
+ for (const step of options.steps) {
111
+ const stepArgs = [
112
+ "mol",
113
+ "step",
114
+ "add",
115
+ molId,
116
+ "--title",
117
+ step.title,
118
+ "--type",
119
+ step.type ?? "task",
120
+ "--json",
121
+ ];
122
+ await runBd(stepArgs, cwd, `mol step add (${step.title})`);
123
+ }
124
+
125
+ return molId;
126
+ }
127
+
128
+ /**
129
+ * Pour (instantiate) a molecule prototype into actual issues.
130
+ *
131
+ * Creates issues from the prototype with dependencies pre-wired.
132
+ * Optionally applies a prefix to all created issue titles.
133
+ *
134
+ * @param cwd - Working directory where bd commands should run
135
+ * @param options - Prototype ID and optional title prefix
136
+ * @returns Array of created issue IDs
137
+ */
138
+ export async function pourMolecule(
139
+ cwd: string,
140
+ options: { prototypeId: string; prefix?: string },
141
+ ): Promise<string[]> {
142
+ const args = ["mol", "pour", options.prototypeId, "--json"];
143
+ if (options.prefix !== undefined) {
144
+ args.push("--prefix", options.prefix);
145
+ }
146
+ const { stdout } = await runBd(args, cwd, "mol pour");
147
+ const result = parseJsonOutput<{ ids: string[] }>(stdout, "mol pour");
148
+ return result.ids;
149
+ }
150
+
151
+ /**
152
+ * List all molecule prototypes.
153
+ *
154
+ * @param cwd - Working directory where bd commands should run
155
+ * @returns Array of prototype summaries
156
+ */
157
+ export async function listPrototypes(cwd: string): Promise<MoleculePrototype[]> {
158
+ const { stdout } = await runBd(["mol", "list", "--json"], cwd, "mol list");
159
+ const result = parseJsonOutput<Array<{ id: string; name: string; stepCount: number }>>(
160
+ stdout,
161
+ "mol list",
162
+ );
163
+ return result.map((entry) => ({
164
+ id: entry.id,
165
+ name: entry.name,
166
+ stepCount: entry.stepCount,
167
+ }));
168
+ }
169
+
170
+ /**
171
+ * Get the convoy status for a molecule prototype instance.
172
+ *
173
+ * Returns counts of total, completed, in-progress, and blocked issues
174
+ * that were poured from this prototype.
175
+ *
176
+ * @param cwd - Working directory where bd commands should run
177
+ * @param prototypeId - The prototype ID to check status for
178
+ * @returns Status counts for the convoy
179
+ */
180
+ export async function getConvoyStatus(cwd: string, prototypeId: string): Promise<ConvoyStatus> {
181
+ const { stdout } = await runBd(
182
+ ["mol", "status", prototypeId, "--json"],
183
+ cwd,
184
+ `mol status ${prototypeId}`,
185
+ );
186
+ const result = parseJsonOutput<{
187
+ total: number;
188
+ completed: number;
189
+ inProgress: number;
190
+ blocked: number;
191
+ }>(stdout, `mol status ${prototypeId}`);
192
+ return {
193
+ total: result.total,
194
+ completed: result.completed,
195
+ inProgress: result.inProgress,
196
+ blocked: result.blocked,
197
+ };
198
+ }
@@ -0,0 +1,322 @@
1
+ /**
2
+ * Tests for the agents command.
3
+ */
4
+
5
+ import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
6
+ import { mkdtemp, rm } from "node:fs/promises";
7
+ import { tmpdir } from "node:os";
8
+ import { join } from "node:path";
9
+ import { createSessionStore } from "../sessions/store.ts";
10
+ import type { AgentSession } from "../types.ts";
11
+ import { agentsCommand, discoverAgents, extractFileScope } from "./agents.ts";
12
+
13
+ describe("extractFileScope", () => {
14
+ let tempDir: string;
15
+
16
+ beforeEach(async () => {
17
+ tempDir = await mkdtemp(join(tmpdir(), "overstory-test-"));
18
+ });
19
+
20
+ it("should return empty array when overlay doesn't exist", async () => {
21
+ const scope = await extractFileScope(tempDir);
22
+ expect(scope).toEqual([]);
23
+ });
24
+
25
+ it("should return empty array when 'No file scope restrictions'", async () => {
26
+ const overlayPath = join(tempDir, ".claude", "CLAUDE.md");
27
+ const content = `# Agent Overlay
28
+
29
+ ## File Scope (exclusive ownership)
30
+
31
+ No file scope restrictions. You may modify any file in the worktree.
32
+
33
+ ## Expertise
34
+
35
+ Some expertise here.
36
+ `;
37
+ await Bun.write(overlayPath, content);
38
+ const scope = await extractFileScope(tempDir);
39
+ expect(scope).toEqual([]);
40
+ });
41
+
42
+ it("should extract file paths from valid overlay", async () => {
43
+ const overlayPath = join(tempDir, ".claude", "CLAUDE.md");
44
+ const content = `# Agent Overlay
45
+
46
+ ## File Scope (exclusive ownership)
47
+
48
+ These files are yours to modify:
49
+
50
+ - \`src/commands/agents.ts\`
51
+ - \`src/commands/agents.test.ts\`
52
+ - \`src/index.ts\`
53
+
54
+ ## Expertise
55
+
56
+ Some expertise here.
57
+ `;
58
+ await Bun.write(overlayPath, content);
59
+ const scope = await extractFileScope(tempDir);
60
+ expect(scope).toEqual([
61
+ "src/commands/agents.ts",
62
+ "src/commands/agents.test.ts",
63
+ "src/index.ts",
64
+ ]);
65
+ });
66
+
67
+ afterEach(async () => {
68
+ await rm(tempDir, { recursive: true, force: true });
69
+ });
70
+ });
71
+
72
+ describe("discoverAgents", () => {
73
+ let tempDir: string;
74
+ let dbPath: string;
75
+
76
+ beforeEach(async () => {
77
+ tempDir = await mkdtemp(join(tmpdir(), "overstory-test-"));
78
+ const overstoryDir = join(tempDir, ".overstory");
79
+ await Bun.write(join(overstoryDir, ".gitkeep"), "");
80
+ dbPath = join(overstoryDir, "sessions.db");
81
+ });
82
+
83
+ it("should return empty when no sessions", async () => {
84
+ const store = createSessionStore(dbPath);
85
+ store.close();
86
+
87
+ const agents = await discoverAgents(tempDir);
88
+ expect(agents).toEqual([]);
89
+ });
90
+
91
+ it("should return active agents", async () => {
92
+ const store = createSessionStore(dbPath);
93
+
94
+ const session: AgentSession = {
95
+ id: "session-1",
96
+ agentName: "builder-test",
97
+ capability: "builder",
98
+ worktreePath: join(tempDir, ".overstory", "worktrees", "builder-test"),
99
+ branchName: "overstory/builder-test/task-123",
100
+ beadId: "task-123",
101
+ tmuxSession: "overstory-test-builder",
102
+ state: "working",
103
+ pid: 12345,
104
+ parentAgent: null,
105
+ depth: 0,
106
+ runId: "run-1",
107
+ startedAt: "2024-01-01T00:00:00Z",
108
+ lastActivity: "2024-01-01T00:01:00Z",
109
+ escalationLevel: 0,
110
+ stalledSince: null,
111
+ };
112
+
113
+ store.upsert(session);
114
+ store.close();
115
+
116
+ const agents = await discoverAgents(tempDir);
117
+ expect(agents).toHaveLength(1);
118
+ expect(agents[0]?.agentName).toBe("builder-test");
119
+ expect(agents[0]?.capability).toBe("builder");
120
+ expect(agents[0]?.state).toBe("working");
121
+ });
122
+
123
+ it("should filter by capability", async () => {
124
+ const store = createSessionStore(dbPath);
125
+
126
+ const builder: AgentSession = {
127
+ id: "session-1",
128
+ agentName: "builder-test",
129
+ capability: "builder",
130
+ worktreePath: join(tempDir, ".overstory", "worktrees", "builder-test"),
131
+ branchName: "overstory/builder-test/task-123",
132
+ beadId: "task-123",
133
+ tmuxSession: "overstory-test-builder",
134
+ state: "working",
135
+ pid: 12345,
136
+ parentAgent: null,
137
+ depth: 0,
138
+ runId: "run-1",
139
+ startedAt: "2024-01-01T00:00:00Z",
140
+ lastActivity: "2024-01-01T00:01:00Z",
141
+ escalationLevel: 0,
142
+ stalledSince: null,
143
+ };
144
+
145
+ const scout: AgentSession = {
146
+ id: "session-2",
147
+ agentName: "scout-test",
148
+ capability: "scout",
149
+ worktreePath: join(tempDir, ".overstory", "worktrees", "scout-test"),
150
+ branchName: "overstory/scout-test/task-456",
151
+ beadId: "task-456",
152
+ tmuxSession: "overstory-test-scout",
153
+ state: "working",
154
+ pid: 12346,
155
+ parentAgent: null,
156
+ depth: 0,
157
+ runId: "run-1",
158
+ startedAt: "2024-01-01T00:00:00Z",
159
+ lastActivity: "2024-01-01T00:01:00Z",
160
+ escalationLevel: 0,
161
+ stalledSince: null,
162
+ };
163
+
164
+ store.upsert(builder);
165
+ store.upsert(scout);
166
+ store.close();
167
+
168
+ const agents = await discoverAgents(tempDir, { capability: "builder" });
169
+ expect(agents).toHaveLength(1);
170
+ expect(agents[0]?.agentName).toBe("builder-test");
171
+ expect(agents[0]?.capability).toBe("builder");
172
+ });
173
+
174
+ it("should includeAll returns completed agents too", async () => {
175
+ const store = createSessionStore(dbPath);
176
+
177
+ const working: AgentSession = {
178
+ id: "session-1",
179
+ agentName: "builder-working",
180
+ capability: "builder",
181
+ worktreePath: join(tempDir, ".overstory", "worktrees", "builder-working"),
182
+ branchName: "overstory/builder-working/task-123",
183
+ beadId: "task-123",
184
+ tmuxSession: "overstory-test-working",
185
+ state: "working",
186
+ pid: 12345,
187
+ parentAgent: null,
188
+ depth: 0,
189
+ runId: "run-1",
190
+ startedAt: "2024-01-01T00:00:00Z",
191
+ lastActivity: "2024-01-01T00:01:00Z",
192
+ escalationLevel: 0,
193
+ stalledSince: null,
194
+ };
195
+
196
+ const completed: AgentSession = {
197
+ id: "session-2",
198
+ agentName: "builder-completed",
199
+ capability: "builder",
200
+ worktreePath: join(tempDir, ".overstory", "worktrees", "builder-completed"),
201
+ branchName: "overstory/builder-completed/task-456",
202
+ beadId: "task-456",
203
+ tmuxSession: "overstory-test-completed",
204
+ state: "completed",
205
+ pid: null,
206
+ parentAgent: null,
207
+ depth: 0,
208
+ runId: "run-1",
209
+ startedAt: "2024-01-01T00:00:00Z",
210
+ lastActivity: "2024-01-01T00:02:00Z",
211
+ escalationLevel: 0,
212
+ stalledSince: null,
213
+ };
214
+
215
+ store.upsert(working);
216
+ store.upsert(completed);
217
+ store.close();
218
+
219
+ // Without includeAll, only active agents
220
+ const activeAgents = await discoverAgents(tempDir);
221
+ expect(activeAgents).toHaveLength(1);
222
+ expect(activeAgents[0]?.agentName).toBe("builder-working");
223
+
224
+ // With includeAll, both working and completed
225
+ const allAgents = await discoverAgents(tempDir, { includeAll: true });
226
+ expect(allAgents).toHaveLength(2);
227
+ const names = allAgents.map((a) => a.agentName);
228
+ expect(names).toContain("builder-working");
229
+ expect(names).toContain("builder-completed");
230
+ });
231
+
232
+ afterEach(async () => {
233
+ await rm(tempDir, { recursive: true, force: true });
234
+ });
235
+ });
236
+
237
+ describe("agentsCommand", () => {
238
+ let tempDir: string;
239
+ let originalCwd: string;
240
+ let originalStdoutWrite: typeof process.stdout.write;
241
+ let stdoutBuffer: string;
242
+
243
+ beforeEach(async () => {
244
+ tempDir = await mkdtemp(join(tmpdir(), "overstory-test-"));
245
+ const overstoryDir = join(tempDir, ".overstory");
246
+
247
+ // Create config.yaml
248
+ const configContent = `project:
249
+ name: test-project
250
+ root: ${tempDir}
251
+ canonicalBranch: main
252
+ agents:
253
+ manifestPath: .overstory/agent-manifest.json
254
+ baseDir: agents
255
+ maxConcurrent: 5
256
+ staggerDelayMs: 100
257
+ maxDepth: 2
258
+ worktrees:
259
+ baseDir: .overstory/worktrees
260
+ beads:
261
+ enabled: true
262
+ mulch:
263
+ enabled: true
264
+ domains: []
265
+ primeFormat: markdown
266
+ merge:
267
+ aiResolveEnabled: false
268
+ reimagineEnabled: false
269
+ watchdog:
270
+ tier0Enabled: false
271
+ tier0IntervalMs: 30000
272
+ tier1Enabled: false
273
+ tier2Enabled: false
274
+ staleThresholdMs: 300000
275
+ zombieThresholdMs: 600000
276
+ nudgeIntervalMs: 60000
277
+ logging:
278
+ verbose: false
279
+ redactSecrets: true
280
+ `;
281
+ await Bun.write(join(overstoryDir, "config.yaml"), configContent);
282
+
283
+ // Create sessions.db
284
+ const dbPath = join(overstoryDir, "sessions.db");
285
+ const store = createSessionStore(dbPath);
286
+ store.close();
287
+
288
+ // Mock stdout.write
289
+ stdoutBuffer = "";
290
+ originalStdoutWrite = process.stdout.write;
291
+ process.stdout.write = mock((chunk: unknown) => {
292
+ stdoutBuffer += String(chunk);
293
+ return true;
294
+ });
295
+
296
+ // Change to temp dir
297
+ originalCwd = process.cwd();
298
+ process.chdir(tempDir);
299
+ });
300
+
301
+ it("should show help with --help flag", async () => {
302
+ await agentsCommand(["--help"]);
303
+ expect(stdoutBuffer).toContain("overstory agents");
304
+ expect(stdoutBuffer).toContain("discover");
305
+ });
306
+
307
+ it("should show help with no subcommand", async () => {
308
+ await agentsCommand([]);
309
+ expect(stdoutBuffer).toContain("overstory agents");
310
+ expect(stdoutBuffer).toContain("discover");
311
+ });
312
+
313
+ it("should error on unknown subcommand", async () => {
314
+ await expect(agentsCommand(["unknown"])).rejects.toThrow("Unknown subcommand");
315
+ });
316
+
317
+ afterEach(async () => {
318
+ process.stdout.write = originalStdoutWrite;
319
+ process.chdir(originalCwd);
320
+ await rm(tempDir, { recursive: true, force: true });
321
+ });
322
+ });