@katyella/legio 0.1.0

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 (219) hide show
  1. package/CHANGELOG.md +422 -0
  2. package/LICENSE +21 -0
  3. package/README.md +555 -0
  4. package/agents/builder.md +141 -0
  5. package/agents/coordinator.md +351 -0
  6. package/agents/cto.md +196 -0
  7. package/agents/gateway.md +276 -0
  8. package/agents/lead.md +281 -0
  9. package/agents/merger.md +156 -0
  10. package/agents/monitor.md +212 -0
  11. package/agents/reviewer.md +142 -0
  12. package/agents/scout.md +131 -0
  13. package/agents/supervisor.md +416 -0
  14. package/bin/legio.mjs +38 -0
  15. package/package.json +77 -0
  16. package/src/agents/checkpoint.test.ts +88 -0
  17. package/src/agents/checkpoint.ts +102 -0
  18. package/src/agents/hooks-deployer.test.ts +1820 -0
  19. package/src/agents/hooks-deployer.ts +574 -0
  20. package/src/agents/identity.test.ts +614 -0
  21. package/src/agents/identity.ts +385 -0
  22. package/src/agents/lifecycle.test.ts +202 -0
  23. package/src/agents/lifecycle.ts +184 -0
  24. package/src/agents/manifest.test.ts +558 -0
  25. package/src/agents/manifest.ts +297 -0
  26. package/src/agents/overlay.test.ts +592 -0
  27. package/src/agents/overlay.ts +316 -0
  28. package/src/beads/client.test.ts +210 -0
  29. package/src/beads/client.ts +227 -0
  30. package/src/beads/molecules.test.ts +320 -0
  31. package/src/beads/molecules.ts +209 -0
  32. package/src/commands/agents.test.ts +325 -0
  33. package/src/commands/agents.ts +286 -0
  34. package/src/commands/clean.test.ts +730 -0
  35. package/src/commands/clean.ts +653 -0
  36. package/src/commands/completions.test.ts +346 -0
  37. package/src/commands/completions.ts +950 -0
  38. package/src/commands/coordinator.test.ts +1524 -0
  39. package/src/commands/coordinator.ts +880 -0
  40. package/src/commands/costs.test.ts +1015 -0
  41. package/src/commands/costs.ts +473 -0
  42. package/src/commands/dashboard.test.ts +94 -0
  43. package/src/commands/dashboard.ts +607 -0
  44. package/src/commands/doctor.test.ts +295 -0
  45. package/src/commands/doctor.ts +213 -0
  46. package/src/commands/down.test.ts +308 -0
  47. package/src/commands/down.ts +124 -0
  48. package/src/commands/errors.test.ts +648 -0
  49. package/src/commands/errors.ts +255 -0
  50. package/src/commands/feed.test.ts +579 -0
  51. package/src/commands/feed.ts +368 -0
  52. package/src/commands/gateway.test.ts +698 -0
  53. package/src/commands/gateway.ts +419 -0
  54. package/src/commands/group.test.ts +262 -0
  55. package/src/commands/group.ts +539 -0
  56. package/src/commands/hooks.test.ts +292 -0
  57. package/src/commands/hooks.ts +210 -0
  58. package/src/commands/init.test.ts +211 -0
  59. package/src/commands/init.ts +622 -0
  60. package/src/commands/inspect.test.ts +670 -0
  61. package/src/commands/inspect.ts +455 -0
  62. package/src/commands/log.test.ts +1556 -0
  63. package/src/commands/log.ts +752 -0
  64. package/src/commands/logs.test.ts +379 -0
  65. package/src/commands/logs.ts +544 -0
  66. package/src/commands/mail.test.ts +1726 -0
  67. package/src/commands/mail.ts +926 -0
  68. package/src/commands/merge.test.ts +676 -0
  69. package/src/commands/merge.ts +374 -0
  70. package/src/commands/metrics.test.ts +444 -0
  71. package/src/commands/metrics.ts +150 -0
  72. package/src/commands/monitor.test.ts +151 -0
  73. package/src/commands/monitor.ts +394 -0
  74. package/src/commands/nudge.test.ts +230 -0
  75. package/src/commands/nudge.ts +373 -0
  76. package/src/commands/prime.test.ts +467 -0
  77. package/src/commands/prime.ts +386 -0
  78. package/src/commands/replay.test.ts +742 -0
  79. package/src/commands/replay.ts +367 -0
  80. package/src/commands/run.test.ts +443 -0
  81. package/src/commands/run.ts +365 -0
  82. package/src/commands/server.test.ts +626 -0
  83. package/src/commands/server.ts +298 -0
  84. package/src/commands/sling.test.ts +810 -0
  85. package/src/commands/sling.ts +700 -0
  86. package/src/commands/spec.test.ts +206 -0
  87. package/src/commands/spec.ts +171 -0
  88. package/src/commands/status.test.ts +276 -0
  89. package/src/commands/status.ts +339 -0
  90. package/src/commands/stop.test.ts +357 -0
  91. package/src/commands/stop.ts +119 -0
  92. package/src/commands/supervisor.test.ts +186 -0
  93. package/src/commands/supervisor.ts +544 -0
  94. package/src/commands/trace.test.ts +746 -0
  95. package/src/commands/trace.ts +332 -0
  96. package/src/commands/up.test.ts +597 -0
  97. package/src/commands/up.ts +275 -0
  98. package/src/commands/watch.test.ts +152 -0
  99. package/src/commands/watch.ts +238 -0
  100. package/src/commands/worktree.test.ts +648 -0
  101. package/src/commands/worktree.ts +266 -0
  102. package/src/config.test.ts +496 -0
  103. package/src/config.ts +616 -0
  104. package/src/doctor/agents.test.ts +448 -0
  105. package/src/doctor/agents.ts +396 -0
  106. package/src/doctor/config-check.test.ts +184 -0
  107. package/src/doctor/config-check.ts +185 -0
  108. package/src/doctor/consistency.test.ts +645 -0
  109. package/src/doctor/consistency.ts +294 -0
  110. package/src/doctor/databases.test.ts +284 -0
  111. package/src/doctor/databases.ts +211 -0
  112. package/src/doctor/dependencies.test.ts +150 -0
  113. package/src/doctor/dependencies.ts +179 -0
  114. package/src/doctor/logs.test.ts +244 -0
  115. package/src/doctor/logs.ts +295 -0
  116. package/src/doctor/merge-queue.test.ts +210 -0
  117. package/src/doctor/merge-queue.ts +144 -0
  118. package/src/doctor/structure.test.ts +285 -0
  119. package/src/doctor/structure.ts +195 -0
  120. package/src/doctor/types.ts +37 -0
  121. package/src/doctor/version.test.ts +130 -0
  122. package/src/doctor/version.ts +131 -0
  123. package/src/e2e/chat-flow.test.ts +346 -0
  124. package/src/e2e/init-sling-lifecycle.test.ts +288 -0
  125. package/src/errors.test.ts +21 -0
  126. package/src/errors.ts +246 -0
  127. package/src/events/store.test.ts +660 -0
  128. package/src/events/store.ts +344 -0
  129. package/src/events/tool-filter.test.ts +330 -0
  130. package/src/events/tool-filter.ts +126 -0
  131. package/src/global-setup.ts +14 -0
  132. package/src/index.ts +339 -0
  133. package/src/insights/analyzer.test.ts +466 -0
  134. package/src/insights/analyzer.ts +203 -0
  135. package/src/logging/color.test.ts +118 -0
  136. package/src/logging/color.ts +71 -0
  137. package/src/logging/logger.test.ts +812 -0
  138. package/src/logging/logger.ts +266 -0
  139. package/src/logging/reporter.test.ts +258 -0
  140. package/src/logging/reporter.ts +109 -0
  141. package/src/logging/sanitizer.test.ts +190 -0
  142. package/src/logging/sanitizer.ts +57 -0
  143. package/src/mail/broadcast.test.ts +203 -0
  144. package/src/mail/broadcast.ts +92 -0
  145. package/src/mail/client.test.ts +873 -0
  146. package/src/mail/client.ts +236 -0
  147. package/src/mail/store.test.ts +815 -0
  148. package/src/mail/store.ts +402 -0
  149. package/src/merge/queue.test.ts +449 -0
  150. package/src/merge/queue.ts +262 -0
  151. package/src/merge/resolver.test.ts +1453 -0
  152. package/src/merge/resolver.ts +759 -0
  153. package/src/metrics/store.test.ts +1167 -0
  154. package/src/metrics/store.ts +511 -0
  155. package/src/metrics/summary.test.ts +397 -0
  156. package/src/metrics/summary.ts +178 -0
  157. package/src/metrics/transcript.test.ts +643 -0
  158. package/src/metrics/transcript.ts +351 -0
  159. package/src/mulch/client.test.ts +547 -0
  160. package/src/mulch/client.ts +416 -0
  161. package/src/server/audit-store.test.ts +384 -0
  162. package/src/server/audit-store.ts +257 -0
  163. package/src/server/headless.test.ts +180 -0
  164. package/src/server/headless.ts +151 -0
  165. package/src/server/index.test.ts +241 -0
  166. package/src/server/index.ts +317 -0
  167. package/src/server/public/app.js +187 -0
  168. package/src/server/public/apple-touch-icon.png +0 -0
  169. package/src/server/public/components/agent-badge.js +37 -0
  170. package/src/server/public/components/data-table.js +114 -0
  171. package/src/server/public/components/gateway-chat.js +256 -0
  172. package/src/server/public/components/issue-card.js +96 -0
  173. package/src/server/public/components/layout.js +88 -0
  174. package/src/server/public/components/message-bubble.js +120 -0
  175. package/src/server/public/components/stat-card.js +26 -0
  176. package/src/server/public/components/terminal-panel.js +140 -0
  177. package/src/server/public/favicon-16.png +0 -0
  178. package/src/server/public/favicon-32.png +0 -0
  179. package/src/server/public/favicon.ico +0 -0
  180. package/src/server/public/favicon.png +0 -0
  181. package/src/server/public/index.html +64 -0
  182. package/src/server/public/lib/api.js +35 -0
  183. package/src/server/public/lib/markdown.js +8 -0
  184. package/src/server/public/lib/preact-setup.js +8 -0
  185. package/src/server/public/lib/state.js +99 -0
  186. package/src/server/public/lib/utils.js +309 -0
  187. package/src/server/public/lib/ws.js +79 -0
  188. package/src/server/public/views/chat.js +983 -0
  189. package/src/server/public/views/costs.js +692 -0
  190. package/src/server/public/views/dashboard.js +781 -0
  191. package/src/server/public/views/gateway-chat.js +622 -0
  192. package/src/server/public/views/inspect.js +399 -0
  193. package/src/server/public/views/issues.js +470 -0
  194. package/src/server/public/views/setup.js +94 -0
  195. package/src/server/public/views/task-detail.js +422 -0
  196. package/src/server/routes.test.ts +3816 -0
  197. package/src/server/routes.ts +1964 -0
  198. package/src/server/websocket.test.ts +288 -0
  199. package/src/server/websocket.ts +196 -0
  200. package/src/sessions/compat.test.ts +109 -0
  201. package/src/sessions/compat.ts +17 -0
  202. package/src/sessions/store.test.ts +969 -0
  203. package/src/sessions/store.ts +480 -0
  204. package/src/test-helpers.test.ts +97 -0
  205. package/src/test-helpers.ts +143 -0
  206. package/src/types.ts +708 -0
  207. package/src/watchdog/daemon.test.ts +1233 -0
  208. package/src/watchdog/daemon.ts +533 -0
  209. package/src/watchdog/health.test.ts +371 -0
  210. package/src/watchdog/health.ts +248 -0
  211. package/src/watchdog/triage.test.ts +162 -0
  212. package/src/watchdog/triage.ts +193 -0
  213. package/src/worktree/manager.test.ts +444 -0
  214. package/src/worktree/manager.ts +224 -0
  215. package/src/worktree/tmux.test.ts +1238 -0
  216. package/src/worktree/tmux.ts +644 -0
  217. package/templates/CLAUDE.md.tmpl +89 -0
  218. package/templates/hooks.json.tmpl +132 -0
  219. package/templates/overlay.md.tmpl +79 -0
@@ -0,0 +1,547 @@
1
+ /**
2
+ * Tests for mulch CLI client.
3
+ *
4
+ * Uses real mulch CLI when available (preferred).
5
+ * All tests are skipped if mulch is not installed.
6
+ */
7
+
8
+ import { spawnSync } from "node:child_process";
9
+ import { mkdtemp, rm } from "node:fs/promises";
10
+ import { tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
13
+ import { AgentError } from "../errors.ts";
14
+ import { createMulchClient, inferDomainsFromFiles } from "./client.ts";
15
+
16
+ // Check if mulch is available
17
+ let hasMulch = false;
18
+ try {
19
+ const result = spawnSync("which", ["mulch"], { stdio: "pipe" });
20
+ hasMulch = result.status === 0;
21
+ } catch {
22
+ hasMulch = false;
23
+ }
24
+
25
+ describe("createMulchClient", () => {
26
+ let tempDir: string;
27
+
28
+ beforeEach(async () => {
29
+ tempDir = await mkdtemp(join(tmpdir(), "mulch-test-"));
30
+ });
31
+
32
+ afterEach(async () => {
33
+ await rm(tempDir, { recursive: true, force: true });
34
+ });
35
+
36
+ /**
37
+ * Helper to initialize git repo in tempDir.
38
+ * Some mulch commands (diff, learn) require a git repository.
39
+ */
40
+ function initGit(): void {
41
+ spawnSync("git", ["init"], { cwd: tempDir, stdio: "pipe" });
42
+ spawnSync("git", ["config", "user.name", "Test User"], { cwd: tempDir, stdio: "pipe" });
43
+ spawnSync("git", ["config", "user.email", "test@example.com"], { cwd: tempDir, stdio: "pipe" });
44
+ }
45
+
46
+ /**
47
+ * Helper to initialize mulch in tempDir.
48
+ * Creates .mulch/ directory and initial structure.
49
+ */
50
+ function initMulch(): void {
51
+ if (!hasMulch) return;
52
+ spawnSync("mulch", ["init"], { cwd: tempDir, stdio: "pipe" });
53
+ }
54
+
55
+ describe("prime", () => {
56
+ test.skipIf(!hasMulch)("returns non-empty string", async () => {
57
+ initMulch();
58
+ const client = createMulchClient(tempDir);
59
+ const result = await client.prime();
60
+ expect(result).toBeTruthy();
61
+ expect(typeof result).toBe("string");
62
+ expect(result.length).toBeGreaterThan(0);
63
+ });
64
+
65
+ test.skipIf(!hasMulch)("passes domain args when provided", async () => {
66
+ initMulch();
67
+ // Add a domain first so we can prime it
68
+ spawnSync("mulch", ["add", "architecture"], { cwd: tempDir, stdio: "pipe" });
69
+
70
+ const client = createMulchClient(tempDir);
71
+ const result = await client.prime(["architecture"]);
72
+ expect(typeof result).toBe("string");
73
+ });
74
+
75
+ test.skipIf(!hasMulch)("passes --format flag", async () => {
76
+ initMulch();
77
+ const client = createMulchClient(tempDir);
78
+ const result = await client.prime([], "markdown");
79
+ expect(typeof result).toBe("string");
80
+ });
81
+
82
+ test.skipIf(!hasMulch)("passes both domains and format", async () => {
83
+ initMulch();
84
+ spawnSync("mulch", ["add", "architecture"], { cwd: tempDir, stdio: "pipe" });
85
+
86
+ const client = createMulchClient(tempDir);
87
+ const result = await client.prime(["architecture"], "xml");
88
+ expect(typeof result).toBe("string");
89
+ });
90
+
91
+ test.skipIf(!hasMulch)("passes --files flag", async () => {
92
+ initMulch();
93
+ const client = createMulchClient(tempDir);
94
+ const result = await client.prime([], "markdown", {
95
+ files: ["src/config.ts", "src/types.ts"],
96
+ });
97
+ expect(typeof result).toBe("string");
98
+ });
99
+
100
+ test.skipIf(!hasMulch)("passes --exclude-domain flag", async () => {
101
+ initMulch();
102
+ spawnSync("mulch", ["add", "architecture"], { cwd: tempDir, stdio: "pipe" });
103
+
104
+ const client = createMulchClient(tempDir);
105
+ const result = await client.prime([], "markdown", {
106
+ excludeDomain: ["architecture"],
107
+ });
108
+ expect(typeof result).toBe("string");
109
+ });
110
+
111
+ test.skipIf(!hasMulch)("passes both --files and --exclude-domain", async () => {
112
+ initMulch();
113
+ // Add a domain to exclude
114
+ spawnSync("mulch", ["add", "internal"], { cwd: tempDir, stdio: "pipe" });
115
+
116
+ const client = createMulchClient(tempDir);
117
+ const result = await client.prime([], "markdown", {
118
+ files: ["src/config.ts"],
119
+ excludeDomain: ["internal"],
120
+ });
121
+ expect(typeof result).toBe("string");
122
+ });
123
+ });
124
+
125
+ describe("status", () => {
126
+ test.skipIf(!hasMulch)("returns MulchStatus shape", async () => {
127
+ initMulch();
128
+ const client = createMulchClient(tempDir);
129
+ const result = await client.status();
130
+ expect(result).toHaveProperty("domains");
131
+ expect(Array.isArray(result.domains)).toBe(true);
132
+ });
133
+
134
+ test.skipIf(!hasMulch)("with no domains returns empty array", async () => {
135
+ initMulch();
136
+ const client = createMulchClient(tempDir);
137
+ const result = await client.status();
138
+ expect(result.domains).toEqual([]);
139
+ });
140
+
141
+ test.skipIf(!hasMulch)("includes domain data when domains exist", async () => {
142
+ initMulch();
143
+ // Add a domain
144
+ spawnSync("mulch", ["add", "architecture"], { cwd: tempDir, stdio: "pipe" });
145
+
146
+ const client = createMulchClient(tempDir);
147
+ const result = await client.status();
148
+ expect(result.domains.length).toBeGreaterThan(0);
149
+ // Just verify we got an array with entries, don't check specific structure
150
+ // as mulch CLI output format may vary
151
+ });
152
+ });
153
+
154
+ describe("record", () => {
155
+ test.skipIf(!hasMulch)("with required args succeeds", async () => {
156
+ initMulch();
157
+ // Add domain first
158
+ spawnSync("mulch", ["add", "architecture"], { cwd: tempDir, stdio: "pipe" });
159
+
160
+ const client = createMulchClient(tempDir);
161
+ await expect(
162
+ client.record("architecture", {
163
+ type: "convention",
164
+ description: "test convention",
165
+ }),
166
+ ).resolves.toBeUndefined();
167
+ });
168
+
169
+ test.skipIf(!hasMulch)("with optional args succeeds", async () => {
170
+ initMulch();
171
+ spawnSync("mulch", ["add", "architecture"], { cwd: tempDir, stdio: "pipe" });
172
+
173
+ const client = createMulchClient(tempDir);
174
+ await expect(
175
+ client.record("architecture", {
176
+ type: "pattern",
177
+ name: "test-pattern",
178
+ description: "test description",
179
+ title: "Test Pattern",
180
+ rationale: "testing all options",
181
+ tags: ["testing", "example"],
182
+ }),
183
+ ).resolves.toBeUndefined();
184
+ });
185
+
186
+ test.skipIf(!hasMulch)("with multiple tags", async () => {
187
+ initMulch();
188
+ spawnSync("mulch", ["add", "typescript"], { cwd: tempDir, stdio: "pipe" });
189
+
190
+ const client = createMulchClient(tempDir);
191
+ await expect(
192
+ client.record("typescript", {
193
+ type: "convention",
194
+ description: "multi-tag test",
195
+ tags: ["tag1", "tag2", "tag3"],
196
+ }),
197
+ ).resolves.toBeUndefined();
198
+ });
199
+
200
+ test.skipIf(!hasMulch)("with --stdin flag passes flag to CLI", async () => {
201
+ initMulch();
202
+ spawnSync("mulch", ["add", "testing"], { cwd: tempDir, stdio: "pipe" });
203
+
204
+ const client = createMulchClient(tempDir);
205
+ // --stdin expects JSON input, which we're not providing, so this will fail
206
+ // but we're testing that the flag is passed correctly
207
+ await expect(
208
+ client.record("testing", {
209
+ type: "convention",
210
+ description: "stdin test",
211
+ stdin: true,
212
+ }),
213
+ ).rejects.toThrow(AgentError);
214
+ });
215
+
216
+ test.skipIf(!hasMulch)("with --evidence-bead flag passes flag to CLI", async () => {
217
+ initMulch();
218
+ spawnSync("mulch", ["add", "testing"], { cwd: tempDir, stdio: "pipe" });
219
+
220
+ const client = createMulchClient(tempDir);
221
+ // The flag is passed correctly, but may fail if the bead ID is invalid
222
+ // or if other required fields are missing. This test documents that the
223
+ // flag is properly passed to the CLI.
224
+ try {
225
+ await client.record("testing", {
226
+ type: "decision",
227
+ description: "bead evidence test",
228
+ evidenceBead: "beads-abc123",
229
+ });
230
+ // If it succeeds, great!
231
+ expect(true).toBe(true);
232
+ } catch (error) {
233
+ // If it fails, verify it's an AgentError (not a type error or similar)
234
+ // which proves the command was executed with the flag
235
+ expect(error).toBeInstanceOf(AgentError);
236
+ }
237
+ });
238
+ });
239
+
240
+ describe("query", () => {
241
+ test.skipIf(!hasMulch)("passes domain arg when provided", async () => {
242
+ initMulch();
243
+ spawnSync("mulch", ["add", "architecture"], { cwd: tempDir, stdio: "pipe" });
244
+
245
+ const client = createMulchClient(tempDir);
246
+ const result = await client.query("architecture");
247
+ expect(typeof result).toBe("string");
248
+ });
249
+
250
+ test.skipIf(!hasMulch)("query without domain requires --all flag", async () => {
251
+ initMulch();
252
+ const client = createMulchClient(tempDir);
253
+ // Current implementation doesn't pass --all, so this will fail
254
+ // This documents the current behavior
255
+ await expect(client.query()).rejects.toThrow(AgentError);
256
+ });
257
+ });
258
+
259
+ describe("search", () => {
260
+ test.skipIf(!hasMulch)("returns string output", async () => {
261
+ initMulch();
262
+ const client = createMulchClient(tempDir);
263
+ const result = await client.search("test");
264
+ expect(typeof result).toBe("string");
265
+ });
266
+
267
+ test.skipIf(!hasMulch)("searches across domains", async () => {
268
+ initMulch();
269
+ // Add a domain and record
270
+ spawnSync("mulch", ["add", "testing"], { cwd: tempDir, stdio: "pipe" });
271
+
272
+ const client = createMulchClient(tempDir);
273
+ await client.record("testing", {
274
+ type: "convention",
275
+ description: "searchable keyword here",
276
+ });
277
+
278
+ const result = await client.search("searchable");
279
+ expect(typeof result).toBe("string");
280
+ });
281
+ });
282
+
283
+ describe("diff", () => {
284
+ test.skipIf(!hasMulch)("shows expertise changes", async () => {
285
+ initGit();
286
+ initMulch();
287
+ const client = createMulchClient(tempDir);
288
+ const result = await client.diff();
289
+ expect(result).toHaveProperty("success");
290
+ expect(result).toHaveProperty("command");
291
+ expect(result).toHaveProperty("domains");
292
+ expect(Array.isArray(result.domains)).toBe(true);
293
+ });
294
+
295
+ test.skipIf(!hasMulch)("passes --since flag", async () => {
296
+ initGit();
297
+ initMulch();
298
+ const client = createMulchClient(tempDir);
299
+ const result = await client.diff({ since: "HEAD~5" });
300
+ expect(result).toHaveProperty("success");
301
+ expect(result).toHaveProperty("since");
302
+ });
303
+ });
304
+
305
+ describe("learn", () => {
306
+ test.skipIf(!hasMulch)("suggests domains for learnings", async () => {
307
+ initGit();
308
+ initMulch();
309
+ const client = createMulchClient(tempDir);
310
+ const result = await client.learn();
311
+ expect(result).toHaveProperty("success");
312
+ expect(result).toHaveProperty("command");
313
+ expect(result).toHaveProperty("changedFiles");
314
+ expect(Array.isArray(result.changedFiles)).toBe(true);
315
+ });
316
+
317
+ test.skipIf(!hasMulch)("passes --since flag", async () => {
318
+ initGit();
319
+ initMulch();
320
+ const client = createMulchClient(tempDir);
321
+ const result = await client.learn({ since: "HEAD~3" });
322
+ expect(result).toHaveProperty("success");
323
+ expect(result).toHaveProperty("changedFiles");
324
+ });
325
+ });
326
+
327
+ describe("prune", () => {
328
+ test.skipIf(!hasMulch)("prunes records", async () => {
329
+ initMulch();
330
+ const client = createMulchClient(tempDir);
331
+ const result = await client.prune();
332
+ expect(result).toHaveProperty("success");
333
+ expect(result).toHaveProperty("command");
334
+ expect(result).toHaveProperty("totalPruned");
335
+ });
336
+
337
+ test.skipIf(!hasMulch)("supports --dry-run flag", async () => {
338
+ initMulch();
339
+ const client = createMulchClient(tempDir);
340
+ const result = await client.prune({ dryRun: true });
341
+ expect(result).toHaveProperty("success");
342
+ expect(result).toHaveProperty("dryRun");
343
+ expect(result.dryRun).toBe(true);
344
+ });
345
+ });
346
+
347
+ describe("doctor", () => {
348
+ test.skipIf(!hasMulch)("runs health checks", async () => {
349
+ initMulch();
350
+ const client = createMulchClient(tempDir);
351
+ const result = await client.doctor();
352
+ expect(result).toHaveProperty("success");
353
+ expect(result).toHaveProperty("command");
354
+ expect(result).toHaveProperty("checks");
355
+ expect(Array.isArray(result.checks)).toBe(true);
356
+ });
357
+
358
+ test.skipIf(!hasMulch)("passes --fix flag", async () => {
359
+ initMulch();
360
+ const client = createMulchClient(tempDir);
361
+ const result = await client.doctor({ fix: true });
362
+ expect(result).toHaveProperty("success");
363
+ expect(result).toHaveProperty("checks");
364
+ });
365
+ });
366
+
367
+ describe("ready", () => {
368
+ test.skipIf(!hasMulch)("shows recently updated records", async () => {
369
+ initMulch();
370
+ const client = createMulchClient(tempDir);
371
+ const result = await client.ready();
372
+ expect(result).toHaveProperty("success");
373
+ expect(result).toHaveProperty("command");
374
+ expect(result).toHaveProperty("entries");
375
+ expect(Array.isArray(result.entries)).toBe(true);
376
+ });
377
+
378
+ test.skipIf(!hasMulch)("passes --limit flag", async () => {
379
+ initMulch();
380
+ const client = createMulchClient(tempDir);
381
+ const result = await client.ready({ limit: 5 });
382
+ expect(result).toHaveProperty("success");
383
+ expect(result).toHaveProperty("count");
384
+ });
385
+
386
+ test.skipIf(!hasMulch)("passes --domain flag", async () => {
387
+ initMulch();
388
+ spawnSync("mulch", ["add", "testing"], { cwd: tempDir, stdio: "pipe" });
389
+
390
+ const client = createMulchClient(tempDir);
391
+ const result = await client.ready({ domain: "testing" });
392
+ expect(result).toHaveProperty("success");
393
+ expect(result).toHaveProperty("entries");
394
+ });
395
+
396
+ test.skipIf(!hasMulch)("passes --since flag", async () => {
397
+ initMulch();
398
+ const client = createMulchClient(tempDir);
399
+ const result = await client.ready({ since: "7d" });
400
+ expect(result).toHaveProperty("success");
401
+ expect(result).toHaveProperty("entries");
402
+ });
403
+ });
404
+
405
+ describe("compact", () => {
406
+ test.skipIf(!hasMulch)("runs with --analyze flag", async () => {
407
+ initMulch();
408
+ const client = createMulchClient(tempDir);
409
+ const result = await client.compact(undefined, { analyze: true });
410
+ expect(result).toHaveProperty("success");
411
+ expect(result).toHaveProperty("command");
412
+ expect(result).toHaveProperty("action");
413
+ });
414
+
415
+ test.skipIf(!hasMulch)("compacts specific domain with --analyze", async () => {
416
+ initMulch();
417
+ spawnSync("mulch", ["add", "large"], { cwd: tempDir, stdio: "pipe" });
418
+
419
+ const client = createMulchClient(tempDir);
420
+ const result = await client.compact("large", { analyze: true });
421
+ expect(result).toHaveProperty("success");
422
+ expect(result).toHaveProperty("action");
423
+ });
424
+
425
+ test.skipIf(!hasMulch)("passes --auto with --dry-run flags", async () => {
426
+ initMulch();
427
+ const client = createMulchClient(tempDir);
428
+ const result = await client.compact(undefined, { auto: true, dryRun: true });
429
+ expect(result).toHaveProperty("success");
430
+ expect(result).toHaveProperty("command");
431
+ });
432
+
433
+ test.skipIf(!hasMulch)("passes multiple options", async () => {
434
+ initMulch();
435
+ const client = createMulchClient(tempDir);
436
+ const result = await client.compact(undefined, {
437
+ auto: true,
438
+ dryRun: true,
439
+ minGroup: 3,
440
+ maxRecords: 20,
441
+ });
442
+ expect(result).toHaveProperty("success");
443
+ expect(result).toHaveProperty("command");
444
+ });
445
+ });
446
+
447
+ describe("error handling", () => {
448
+ test.skipIf(!hasMulch)("throws AgentError when mulch command fails", async () => {
449
+ // Don't init mulch - operations will fail with "not initialized" error
450
+ const client = createMulchClient(tempDir);
451
+ await expect(client.status()).rejects.toThrow(AgentError);
452
+ });
453
+
454
+ test.skipIf(!hasMulch)("AgentError message contains exit code", async () => {
455
+ const client = createMulchClient(tempDir);
456
+ try {
457
+ await client.status();
458
+ expect.unreachable("Should have thrown AgentError");
459
+ } catch (error) {
460
+ expect(error).toBeInstanceOf(AgentError);
461
+ const agentError = error as AgentError;
462
+ expect(agentError.message).toContain("exit");
463
+ expect(agentError.message).toContain("status");
464
+ }
465
+ });
466
+
467
+ test.skipIf(!hasMulch)("record fails with descriptive error for missing domain", async () => {
468
+ initMulch();
469
+ const client = createMulchClient(tempDir);
470
+ // Try to record to a domain that doesn't exist
471
+ await expect(
472
+ client.record("nonexistent-domain", {
473
+ type: "convention",
474
+ description: "test",
475
+ }),
476
+ ).rejects.toThrow(AgentError);
477
+ });
478
+
479
+ test.skipIf(!hasMulch)("handles empty status output correctly", async () => {
480
+ initMulch();
481
+ const client = createMulchClient(tempDir);
482
+ const result = await client.status();
483
+ // With no domains, should have empty array (not throw)
484
+ expect(result).toHaveProperty("domains");
485
+ expect(result.domains).toEqual([]);
486
+ });
487
+ });
488
+ });
489
+
490
+ describe("inferDomainsFromFiles", () => {
491
+ test("returns empty array for empty file list", () => {
492
+ expect(inferDomainsFromFiles([])).toEqual([]);
493
+ });
494
+
495
+ test("maps src/commands/ files to cli domain", () => {
496
+ const result = inferDomainsFromFiles(["src/commands/init.ts"]);
497
+ expect(result).toContain("cli");
498
+ expect(result).not.toContain("server");
499
+ });
500
+
501
+ test("maps src/worktree/ files to both swarm and merge domains", () => {
502
+ const result = inferDomainsFromFiles(["src/worktree/manager.ts"]);
503
+ expect(result).toContain("swarm");
504
+ expect(result).toContain("merge");
505
+ });
506
+
507
+ test("maps test files to testing domain", () => {
508
+ const direct = inferDomainsFromFiles(["src/config.test.ts"]);
509
+ expect(direct).toContain("testing");
510
+
511
+ const nested = inferDomainsFromFiles(["src/mulch/client.test.ts"]);
512
+ expect(nested).toContain("testing");
513
+ });
514
+
515
+ test("deduplicates domains when multiple files match same domain", () => {
516
+ const result = inferDomainsFromFiles([
517
+ "src/commands/init.ts",
518
+ "src/commands/sling.ts",
519
+ "src/beads/client.ts",
520
+ ]);
521
+ const cliCount = result.filter((d) => d === "cli").length;
522
+ expect(cliCount).toBe(1);
523
+ });
524
+
525
+ test("accepts custom domainMap that overrides defaults", () => {
526
+ const customMap: Record<string, string[]> = {
527
+ "src/custom/**": ["myDomain"],
528
+ };
529
+ const result = inferDomainsFromFiles(["src/custom/foo.ts"], customMap);
530
+ expect(result).toEqual(["myDomain"]);
531
+ // Should NOT match DEFAULT_DOMAIN_MAP patterns
532
+ expect(result).not.toContain("cli");
533
+ });
534
+
535
+ test("returns empty array when no patterns match", () => {
536
+ const result = inferDomainsFromFiles(["some/random/file.txt"]);
537
+ expect(result).toEqual([]);
538
+ });
539
+
540
+ test("maps agents/ and templates/ to swarm domain", () => {
541
+ const agents = inferDomainsFromFiles(["agents/builder.md"]);
542
+ expect(agents).toContain("swarm");
543
+
544
+ const templates = inferDomainsFromFiles(["templates/overlay.md.tmpl"]);
545
+ expect(templates).toContain("swarm");
546
+ });
547
+ });