@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,671 @@
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 { afterEach, beforeEach, describe, expect, test } from "bun:test";
9
+ import { mkdtemp, rm } from "node:fs/promises";
10
+ import { tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+ import { AgentError } from "../errors.ts";
13
+ import { createMulchClient } from "./client.ts";
14
+
15
+ // Check if mulch is available
16
+ let hasMulch = false;
17
+ try {
18
+ const proc = Bun.spawn(["which", "mulch"], { stdout: "pipe", stderr: "pipe" });
19
+ const exitCode = await proc.exited;
20
+ hasMulch = exitCode === 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
+ async function initGit(): Promise<void> {
41
+ const initProc = Bun.spawn(["git", "init"], {
42
+ cwd: tempDir,
43
+ stdout: "pipe",
44
+ stderr: "pipe",
45
+ });
46
+ await initProc.exited;
47
+
48
+ const configNameProc = Bun.spawn(["git", "config", "user.name", "Test User"], {
49
+ cwd: tempDir,
50
+ stdout: "pipe",
51
+ stderr: "pipe",
52
+ });
53
+ await configNameProc.exited;
54
+
55
+ const configEmailProc = Bun.spawn(["git", "config", "user.email", "test@example.com"], {
56
+ cwd: tempDir,
57
+ stdout: "pipe",
58
+ stderr: "pipe",
59
+ });
60
+ await configEmailProc.exited;
61
+ }
62
+
63
+ /**
64
+ * Helper to initialize mulch in tempDir.
65
+ * Creates .mulch/ directory and initial structure.
66
+ */
67
+ async function initMulch(): Promise<void> {
68
+ if (!hasMulch) return;
69
+ const proc = Bun.spawn(["mulch", "init"], {
70
+ cwd: tempDir,
71
+ stdout: "pipe",
72
+ stderr: "pipe",
73
+ });
74
+ await proc.exited;
75
+ }
76
+
77
+ describe("prime", () => {
78
+ test.skipIf(!hasMulch)("returns non-empty string", async () => {
79
+ await initMulch();
80
+ const client = createMulchClient(tempDir);
81
+ const result = await client.prime();
82
+ expect(result).toBeTruthy();
83
+ expect(typeof result).toBe("string");
84
+ expect(result.length).toBeGreaterThan(0);
85
+ });
86
+
87
+ test.skipIf(!hasMulch)("passes domain args when provided", async () => {
88
+ await initMulch();
89
+ // Add a domain first so we can prime it
90
+ const addProc = Bun.spawn(["mulch", "add", "architecture"], {
91
+ cwd: tempDir,
92
+ stdout: "pipe",
93
+ stderr: "pipe",
94
+ });
95
+ await addProc.exited;
96
+
97
+ const client = createMulchClient(tempDir);
98
+ const result = await client.prime(["architecture"]);
99
+ expect(typeof result).toBe("string");
100
+ });
101
+
102
+ test.skipIf(!hasMulch)("passes --format flag", async () => {
103
+ await initMulch();
104
+ const client = createMulchClient(tempDir);
105
+ const result = await client.prime([], "markdown");
106
+ expect(typeof result).toBe("string");
107
+ });
108
+
109
+ test.skipIf(!hasMulch)("passes both domains and format", async () => {
110
+ await initMulch();
111
+ const addProc = Bun.spawn(["mulch", "add", "architecture"], {
112
+ cwd: tempDir,
113
+ stdout: "pipe",
114
+ stderr: "pipe",
115
+ });
116
+ await addProc.exited;
117
+
118
+ const client = createMulchClient(tempDir);
119
+ const result = await client.prime(["architecture"], "xml");
120
+ expect(typeof result).toBe("string");
121
+ });
122
+
123
+ test.skipIf(!hasMulch)("passes --files flag", async () => {
124
+ await initMulch();
125
+ const client = createMulchClient(tempDir);
126
+ const result = await client.prime([], "markdown", {
127
+ files: ["src/config.ts", "src/types.ts"],
128
+ });
129
+ expect(typeof result).toBe("string");
130
+ });
131
+
132
+ test.skipIf(!hasMulch)("passes --exclude-domain flag", async () => {
133
+ await initMulch();
134
+ const addProc = Bun.spawn(["mulch", "add", "architecture"], {
135
+ cwd: tempDir,
136
+ stdout: "pipe",
137
+ stderr: "pipe",
138
+ });
139
+ await addProc.exited;
140
+
141
+ const client = createMulchClient(tempDir);
142
+ const result = await client.prime([], "markdown", {
143
+ excludeDomain: ["architecture"],
144
+ });
145
+ expect(typeof result).toBe("string");
146
+ });
147
+
148
+ test.skipIf(!hasMulch)("passes both --files and --exclude-domain", async () => {
149
+ await initMulch();
150
+ // Add a domain to exclude
151
+ const addProc = Bun.spawn(["mulch", "add", "internal"], {
152
+ cwd: tempDir,
153
+ stdout: "pipe",
154
+ stderr: "pipe",
155
+ });
156
+ await addProc.exited;
157
+
158
+ const client = createMulchClient(tempDir);
159
+ const result = await client.prime([], "markdown", {
160
+ files: ["src/config.ts"],
161
+ excludeDomain: ["internal"],
162
+ });
163
+ expect(typeof result).toBe("string");
164
+ });
165
+ });
166
+
167
+ describe("status", () => {
168
+ test.skipIf(!hasMulch)("returns MulchStatus shape", async () => {
169
+ await initMulch();
170
+ const client = createMulchClient(tempDir);
171
+ const result = await client.status();
172
+ expect(result).toHaveProperty("domains");
173
+ expect(Array.isArray(result.domains)).toBe(true);
174
+ });
175
+
176
+ test.skipIf(!hasMulch)("with no domains returns empty array", async () => {
177
+ await initMulch();
178
+ const client = createMulchClient(tempDir);
179
+ const result = await client.status();
180
+ expect(result.domains).toEqual([]);
181
+ });
182
+
183
+ test.skipIf(!hasMulch)("includes domain data when domains exist", async () => {
184
+ await initMulch();
185
+ // Add a domain
186
+ const addProc = Bun.spawn(["mulch", "add", "architecture"], {
187
+ cwd: tempDir,
188
+ stdout: "pipe",
189
+ stderr: "pipe",
190
+ });
191
+ await addProc.exited;
192
+
193
+ const client = createMulchClient(tempDir);
194
+ const result = await client.status();
195
+ expect(result.domains.length).toBeGreaterThan(0);
196
+ // Just verify we got an array with entries, don't check specific structure
197
+ // as mulch CLI output format may vary
198
+ });
199
+ });
200
+
201
+ describe("record", () => {
202
+ test.skipIf(!hasMulch)("with required args succeeds", async () => {
203
+ await initMulch();
204
+ // Add domain first
205
+ const addProc = Bun.spawn(["mulch", "add", "architecture"], {
206
+ cwd: tempDir,
207
+ stdout: "pipe",
208
+ stderr: "pipe",
209
+ });
210
+ await addProc.exited;
211
+
212
+ const client = createMulchClient(tempDir);
213
+ await expect(
214
+ client.record("architecture", {
215
+ type: "convention",
216
+ description: "test convention",
217
+ }),
218
+ ).resolves.toBeUndefined();
219
+ });
220
+
221
+ test.skipIf(!hasMulch)("with optional args succeeds", async () => {
222
+ await initMulch();
223
+ const addProc = Bun.spawn(["mulch", "add", "architecture"], {
224
+ cwd: tempDir,
225
+ stdout: "pipe",
226
+ stderr: "pipe",
227
+ });
228
+ await addProc.exited;
229
+
230
+ const client = createMulchClient(tempDir);
231
+ await expect(
232
+ client.record("architecture", {
233
+ type: "pattern",
234
+ name: "test-pattern",
235
+ description: "test description",
236
+ title: "Test Pattern",
237
+ rationale: "testing all options",
238
+ tags: ["testing", "example"],
239
+ }),
240
+ ).resolves.toBeUndefined();
241
+ });
242
+
243
+ test.skipIf(!hasMulch)("with multiple tags", async () => {
244
+ await initMulch();
245
+ const addProc = Bun.spawn(["mulch", "add", "typescript"], {
246
+ cwd: tempDir,
247
+ stdout: "pipe",
248
+ stderr: "pipe",
249
+ });
250
+ await addProc.exited;
251
+
252
+ const client = createMulchClient(tempDir);
253
+ await expect(
254
+ client.record("typescript", {
255
+ type: "convention",
256
+ description: "multi-tag test",
257
+ tags: ["tag1", "tag2", "tag3"],
258
+ }),
259
+ ).resolves.toBeUndefined();
260
+ });
261
+
262
+ test.skipIf(!hasMulch)("with --stdin flag passes flag to CLI", async () => {
263
+ await initMulch();
264
+ const addProc = Bun.spawn(["mulch", "add", "testing"], {
265
+ cwd: tempDir,
266
+ stdout: "pipe",
267
+ stderr: "pipe",
268
+ });
269
+ await addProc.exited;
270
+
271
+ const client = createMulchClient(tempDir);
272
+ // --stdin expects JSON input, which we're not providing, so this will fail
273
+ // but we're testing that the flag is passed correctly
274
+ await expect(
275
+ client.record("testing", {
276
+ type: "convention",
277
+ description: "stdin test",
278
+ stdin: true,
279
+ }),
280
+ ).rejects.toThrow(AgentError);
281
+ });
282
+
283
+ test.skipIf(!hasMulch)("with outcome flags passes them to CLI", async () => {
284
+ await initMulch();
285
+ const addProc = Bun.spawn(["mulch", "add", "testing"], {
286
+ cwd: tempDir,
287
+ stdout: "pipe",
288
+ stderr: "pipe",
289
+ });
290
+ await addProc.exited;
291
+
292
+ const client = createMulchClient(tempDir);
293
+ // May succeed or fail depending on mulch version, but verifies flags are passed
294
+ try {
295
+ await client.record("testing", {
296
+ type: "convention",
297
+ description: "outcome test",
298
+ outcomeStatus: "success",
299
+ outcomeDuration: 42,
300
+ outcomeTestResults: "15 passed",
301
+ outcomeAgent: "test-agent",
302
+ });
303
+ expect(true).toBe(true);
304
+ } catch (error) {
305
+ expect(error).toBeInstanceOf(AgentError);
306
+ }
307
+ });
308
+
309
+ test.skipIf(!hasMulch)("with outcomeStatus: failure passes flag to CLI", async () => {
310
+ await initMulch();
311
+ const addProc = Bun.spawn(["mulch", "add", "testing"], {
312
+ cwd: tempDir,
313
+ stdout: "pipe",
314
+ stderr: "pipe",
315
+ });
316
+ await addProc.exited;
317
+
318
+ const client = createMulchClient(tempDir);
319
+ try {
320
+ await client.record("testing", {
321
+ type: "failure",
322
+ description: "failure outcome test",
323
+ outcomeStatus: "failure",
324
+ });
325
+ expect(true).toBe(true);
326
+ } catch (error) {
327
+ expect(error).toBeInstanceOf(AgentError);
328
+ }
329
+ });
330
+
331
+ test.skipIf(!hasMulch)("with outcomeDuration: 0 passes zero value to CLI", async () => {
332
+ await initMulch();
333
+ const addProc = Bun.spawn(["mulch", "add", "testing"], {
334
+ cwd: tempDir,
335
+ stdout: "pipe",
336
+ stderr: "pipe",
337
+ });
338
+ await addProc.exited;
339
+
340
+ const client = createMulchClient(tempDir);
341
+ try {
342
+ await client.record("testing", {
343
+ type: "convention",
344
+ description: "zero duration test",
345
+ outcomeDuration: 0,
346
+ });
347
+ expect(true).toBe(true);
348
+ } catch (error) {
349
+ expect(error).toBeInstanceOf(AgentError);
350
+ }
351
+ });
352
+
353
+ test.skipIf(!hasMulch)("with --evidence-bead flag passes flag to CLI", async () => {
354
+ await initMulch();
355
+ const addProc = Bun.spawn(["mulch", "add", "testing"], {
356
+ cwd: tempDir,
357
+ stdout: "pipe",
358
+ stderr: "pipe",
359
+ });
360
+ await addProc.exited;
361
+
362
+ const client = createMulchClient(tempDir);
363
+ // The flag is passed correctly, but may fail if the bead ID is invalid
364
+ // or if other required fields are missing. This test documents that the
365
+ // flag is properly passed to the CLI.
366
+ try {
367
+ await client.record("testing", {
368
+ type: "decision",
369
+ description: "bead evidence test",
370
+ evidenceBead: "beads-abc123",
371
+ });
372
+ // If it succeeds, great!
373
+ expect(true).toBe(true);
374
+ } catch (error) {
375
+ // If it fails, verify it's an AgentError (not a type error or similar)
376
+ // which proves the command was executed with the flag
377
+ expect(error).toBeInstanceOf(AgentError);
378
+ }
379
+ });
380
+ });
381
+
382
+ describe("query", () => {
383
+ test.skipIf(!hasMulch)("passes domain arg when provided", async () => {
384
+ await initMulch();
385
+ const addProc = Bun.spawn(["mulch", "add", "architecture"], {
386
+ cwd: tempDir,
387
+ stdout: "pipe",
388
+ stderr: "pipe",
389
+ });
390
+ await addProc.exited;
391
+
392
+ const client = createMulchClient(tempDir);
393
+ const result = await client.query("architecture");
394
+ expect(typeof result).toBe("string");
395
+ });
396
+
397
+ test.skipIf(!hasMulch)("query without domain requires --all flag", async () => {
398
+ await initMulch();
399
+ const client = createMulchClient(tempDir);
400
+ // Current implementation doesn't pass --all, so this will fail
401
+ // This documents the current behavior
402
+ await expect(client.query()).rejects.toThrow(AgentError);
403
+ });
404
+ });
405
+
406
+ describe("search", () => {
407
+ test.skipIf(!hasMulch)("returns string output", async () => {
408
+ await initMulch();
409
+ const client = createMulchClient(tempDir);
410
+ const result = await client.search("test");
411
+ expect(typeof result).toBe("string");
412
+ });
413
+
414
+ test.skipIf(!hasMulch)("searches across domains", async () => {
415
+ await initMulch();
416
+ // Add a domain and record
417
+ const addProc = Bun.spawn(["mulch", "add", "testing"], {
418
+ cwd: tempDir,
419
+ stdout: "pipe",
420
+ stderr: "pipe",
421
+ });
422
+ await addProc.exited;
423
+
424
+ const client = createMulchClient(tempDir);
425
+ await client.record("testing", {
426
+ type: "convention",
427
+ description: "searchable keyword here",
428
+ });
429
+
430
+ const result = await client.search("searchable");
431
+ expect(typeof result).toBe("string");
432
+ });
433
+
434
+ test.skipIf(!hasMulch)("passes --file flag when provided", async () => {
435
+ await initMulch();
436
+ const client = createMulchClient(tempDir);
437
+ const result = await client.search("test", { file: "src/config.ts" });
438
+ expect(typeof result).toBe("string");
439
+ });
440
+
441
+ test.skipIf(!hasMulch)("passes --sort-by-score flag when provided", async () => {
442
+ await initMulch();
443
+ const client = createMulchClient(tempDir);
444
+ const result = await client.search("test", { sortByScore: true });
445
+ expect(typeof result).toBe("string");
446
+ });
447
+
448
+ test.skipIf(!hasMulch)("passes both --file and --sort-by-score flags", async () => {
449
+ await initMulch();
450
+ const client = createMulchClient(tempDir);
451
+ const result = await client.search("test", { file: "src/config.ts", sortByScore: true });
452
+ expect(typeof result).toBe("string");
453
+ });
454
+ });
455
+
456
+ describe("diff", () => {
457
+ test.skipIf(!hasMulch)("shows expertise changes", async () => {
458
+ await initGit();
459
+ await initMulch();
460
+ const client = createMulchClient(tempDir);
461
+ const result = await client.diff();
462
+ expect(result).toHaveProperty("success");
463
+ expect(result).toHaveProperty("command");
464
+ expect(result).toHaveProperty("domains");
465
+ expect(Array.isArray(result.domains)).toBe(true);
466
+ });
467
+
468
+ test.skipIf(!hasMulch)("passes --since flag", async () => {
469
+ await initGit();
470
+ await initMulch();
471
+ const client = createMulchClient(tempDir);
472
+ const result = await client.diff({ since: "HEAD~5" });
473
+ expect(result).toHaveProperty("success");
474
+ expect(result).toHaveProperty("since");
475
+ });
476
+ });
477
+
478
+ describe("learn", () => {
479
+ test.skipIf(!hasMulch)("suggests domains for learnings", async () => {
480
+ await initGit();
481
+ await initMulch();
482
+ const client = createMulchClient(tempDir);
483
+ const result = await client.learn();
484
+ expect(result).toHaveProperty("success");
485
+ expect(result).toHaveProperty("command");
486
+ expect(result).toHaveProperty("changedFiles");
487
+ expect(Array.isArray(result.changedFiles)).toBe(true);
488
+ });
489
+
490
+ test.skipIf(!hasMulch)("passes --since flag", async () => {
491
+ await initGit();
492
+ await initMulch();
493
+ const client = createMulchClient(tempDir);
494
+ const result = await client.learn({ since: "HEAD~3" });
495
+ expect(result).toHaveProperty("success");
496
+ expect(result).toHaveProperty("changedFiles");
497
+ });
498
+ });
499
+
500
+ describe("prune", () => {
501
+ test.skipIf(!hasMulch)("prunes records", async () => {
502
+ await initMulch();
503
+ const client = createMulchClient(tempDir);
504
+ const result = await client.prune();
505
+ expect(result).toHaveProperty("success");
506
+ expect(result).toHaveProperty("command");
507
+ expect(result).toHaveProperty("totalPruned");
508
+ });
509
+
510
+ test.skipIf(!hasMulch)("supports --dry-run flag", async () => {
511
+ await initMulch();
512
+ const client = createMulchClient(tempDir);
513
+ const result = await client.prune({ dryRun: true });
514
+ expect(result).toHaveProperty("success");
515
+ expect(result).toHaveProperty("dryRun");
516
+ expect(result.dryRun).toBe(true);
517
+ });
518
+ });
519
+
520
+ describe("doctor", () => {
521
+ test.skipIf(!hasMulch)("runs health checks", async () => {
522
+ await initMulch();
523
+ const client = createMulchClient(tempDir);
524
+ const result = await client.doctor();
525
+ expect(result).toHaveProperty("success");
526
+ expect(result).toHaveProperty("command");
527
+ expect(result).toHaveProperty("checks");
528
+ expect(Array.isArray(result.checks)).toBe(true);
529
+ });
530
+
531
+ test.skipIf(!hasMulch)("passes --fix flag", async () => {
532
+ await initMulch();
533
+ const client = createMulchClient(tempDir);
534
+ const result = await client.doctor({ fix: true });
535
+ expect(result).toHaveProperty("success");
536
+ expect(result).toHaveProperty("checks");
537
+ });
538
+ });
539
+
540
+ describe("ready", () => {
541
+ test.skipIf(!hasMulch)("shows recently updated records", async () => {
542
+ await initMulch();
543
+ const client = createMulchClient(tempDir);
544
+ const result = await client.ready();
545
+ expect(result).toHaveProperty("success");
546
+ expect(result).toHaveProperty("command");
547
+ expect(result).toHaveProperty("entries");
548
+ expect(Array.isArray(result.entries)).toBe(true);
549
+ });
550
+
551
+ test.skipIf(!hasMulch)("passes --limit flag", async () => {
552
+ await initMulch();
553
+ const client = createMulchClient(tempDir);
554
+ const result = await client.ready({ limit: 5 });
555
+ expect(result).toHaveProperty("success");
556
+ expect(result).toHaveProperty("count");
557
+ });
558
+
559
+ test.skipIf(!hasMulch)("passes --domain flag", async () => {
560
+ await initMulch();
561
+ const addProc = Bun.spawn(["mulch", "add", "testing"], {
562
+ cwd: tempDir,
563
+ stdout: "pipe",
564
+ stderr: "pipe",
565
+ });
566
+ await addProc.exited;
567
+
568
+ const client = createMulchClient(tempDir);
569
+ const result = await client.ready({ domain: "testing" });
570
+ expect(result).toHaveProperty("success");
571
+ expect(result).toHaveProperty("entries");
572
+ });
573
+
574
+ test.skipIf(!hasMulch)("passes --since flag", async () => {
575
+ await initMulch();
576
+ const client = createMulchClient(tempDir);
577
+ const result = await client.ready({ since: "7d" });
578
+ expect(result).toHaveProperty("success");
579
+ expect(result).toHaveProperty("entries");
580
+ });
581
+ });
582
+
583
+ describe("compact", () => {
584
+ test.skipIf(!hasMulch)("runs with --analyze flag", async () => {
585
+ await initMulch();
586
+ const client = createMulchClient(tempDir);
587
+ const result = await client.compact(undefined, { analyze: true });
588
+ expect(result).toHaveProperty("success");
589
+ expect(result).toHaveProperty("command");
590
+ expect(result).toHaveProperty("action");
591
+ });
592
+
593
+ test.skipIf(!hasMulch)("compacts specific domain with --analyze", async () => {
594
+ await initMulch();
595
+ const addProc = Bun.spawn(["mulch", "add", "large"], {
596
+ cwd: tempDir,
597
+ stdout: "pipe",
598
+ stderr: "pipe",
599
+ });
600
+ await addProc.exited;
601
+
602
+ const client = createMulchClient(tempDir);
603
+ const result = await client.compact("large", { analyze: true });
604
+ expect(result).toHaveProperty("success");
605
+ expect(result).toHaveProperty("action");
606
+ });
607
+
608
+ test.skipIf(!hasMulch)("passes --auto with --dry-run flags", async () => {
609
+ await initMulch();
610
+ const client = createMulchClient(tempDir);
611
+ const result = await client.compact(undefined, { auto: true, dryRun: true });
612
+ expect(result).toHaveProperty("success");
613
+ expect(result).toHaveProperty("command");
614
+ });
615
+
616
+ test.skipIf(!hasMulch)("passes multiple options", async () => {
617
+ await initMulch();
618
+ const client = createMulchClient(tempDir);
619
+ const result = await client.compact(undefined, {
620
+ auto: true,
621
+ dryRun: true,
622
+ minGroup: 3,
623
+ maxRecords: 20,
624
+ });
625
+ expect(result).toHaveProperty("success");
626
+ expect(result).toHaveProperty("command");
627
+ });
628
+ });
629
+
630
+ describe("error handling", () => {
631
+ test.skipIf(!hasMulch)("throws AgentError when mulch command fails", async () => {
632
+ // Don't init mulch - operations will fail with "not initialized" error
633
+ const client = createMulchClient(tempDir);
634
+ await expect(client.status()).rejects.toThrow(AgentError);
635
+ });
636
+
637
+ test.skipIf(!hasMulch)("AgentError message contains exit code", async () => {
638
+ const client = createMulchClient(tempDir);
639
+ try {
640
+ await client.status();
641
+ expect.unreachable("Should have thrown AgentError");
642
+ } catch (error) {
643
+ expect(error).toBeInstanceOf(AgentError);
644
+ const agentError = error as AgentError;
645
+ expect(agentError.message).toContain("exit");
646
+ expect(agentError.message).toContain("status");
647
+ }
648
+ });
649
+
650
+ test.skipIf(!hasMulch)("record fails with descriptive error for missing domain", async () => {
651
+ await initMulch();
652
+ const client = createMulchClient(tempDir);
653
+ // Try to record to a domain that doesn't exist
654
+ await expect(
655
+ client.record("nonexistent-domain", {
656
+ type: "convention",
657
+ description: "test",
658
+ }),
659
+ ).rejects.toThrow(AgentError);
660
+ });
661
+
662
+ test.skipIf(!hasMulch)("handles empty status output correctly", async () => {
663
+ await initMulch();
664
+ const client = createMulchClient(tempDir);
665
+ const result = await client.status();
666
+ // With no domains, should have empty array (not throw)
667
+ expect(result).toHaveProperty("domains");
668
+ expect(result.domains).toEqual([]);
669
+ });
670
+ });
671
+ });