@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,347 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { readdir } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { cleanupTempDir, createTempGitRepo, runGitInDir } from "../test-helpers.ts";
5
+ import { initCommand, OVERSTORY_GITIGNORE, OVERSTORY_README } from "./init.ts";
6
+
7
+ /**
8
+ * Tests for `overstory init` -- agent definition deployment.
9
+ *
10
+ * Uses real temp git repos. Suppresses stdout to keep test output clean.
11
+ * process.cwd() is saved/restored because initCommand uses it to find the project root.
12
+ */
13
+
14
+ const AGENT_DEF_FILES = [
15
+ "scout.md",
16
+ "builder.md",
17
+ "reviewer.md",
18
+ "lead.md",
19
+ "merger.md",
20
+ "supervisor.md",
21
+ "coordinator.md",
22
+ "monitor.md",
23
+ ];
24
+
25
+ /** Resolve the source agents directory (same logic as init.ts). */
26
+ const SOURCE_AGENTS_DIR = join(import.meta.dir, "..", "..", "agents");
27
+
28
+ describe("initCommand: agent-defs deployment", () => {
29
+ let tempDir: string;
30
+ let originalCwd: string;
31
+ let originalWrite: typeof process.stdout.write;
32
+
33
+ beforeEach(async () => {
34
+ tempDir = await createTempGitRepo();
35
+ originalCwd = process.cwd();
36
+ process.chdir(tempDir);
37
+
38
+ // Suppress stdout noise from initCommand
39
+ originalWrite = process.stdout.write;
40
+ process.stdout.write = (() => true) as typeof process.stdout.write;
41
+ });
42
+
43
+ afterEach(async () => {
44
+ process.chdir(originalCwd);
45
+ process.stdout.write = originalWrite;
46
+ await cleanupTempDir(tempDir);
47
+ });
48
+
49
+ test("creates .overstory/agent-defs/ with all 8 agent definition files", async () => {
50
+ await initCommand([]);
51
+
52
+ const agentDefsDir = join(tempDir, ".overstory", "agent-defs");
53
+ const files = await readdir(agentDefsDir);
54
+ const mdFiles = files.filter((f) => f.endsWith(".md")).sort();
55
+
56
+ expect(mdFiles).toEqual(AGENT_DEF_FILES.slice().sort());
57
+ });
58
+
59
+ test("copied files match source content", async () => {
60
+ await initCommand([]);
61
+
62
+ for (const fileName of AGENT_DEF_FILES) {
63
+ const sourcePath = join(SOURCE_AGENTS_DIR, fileName);
64
+ const targetPath = join(tempDir, ".overstory", "agent-defs", fileName);
65
+
66
+ const sourceContent = await Bun.file(sourcePath).text();
67
+ const targetContent = await Bun.file(targetPath).text();
68
+
69
+ expect(targetContent).toBe(sourceContent);
70
+ }
71
+ });
72
+
73
+ test("--force reinit overwrites existing agent def files", async () => {
74
+ // First init
75
+ await initCommand([]);
76
+
77
+ // Tamper with one of the deployed files
78
+ const tamperPath = join(tempDir, ".overstory", "agent-defs", "scout.md");
79
+ await Bun.write(tamperPath, "# tampered content\n");
80
+
81
+ // Verify tamper worked
82
+ const tampered = await Bun.file(tamperPath).text();
83
+ expect(tampered).toBe("# tampered content\n");
84
+
85
+ // Reinit with --force
86
+ await initCommand(["--force"]);
87
+
88
+ // Verify the file was overwritten with the original source
89
+ const sourceContent = await Bun.file(join(SOURCE_AGENTS_DIR, "scout.md")).text();
90
+ const restored = await Bun.file(tamperPath).text();
91
+ expect(restored).toBe(sourceContent);
92
+ });
93
+
94
+ test("Stop hook includes mulch learn command", async () => {
95
+ await initCommand([]);
96
+
97
+ const hooksPath = join(tempDir, ".overstory", "hooks.json");
98
+ const content = await Bun.file(hooksPath).text();
99
+ const parsed = JSON.parse(content);
100
+ const stopHooks = parsed.hooks.Stop[0].hooks;
101
+
102
+ expect(stopHooks.length).toBe(2);
103
+ expect(stopHooks[0].command).toContain("overstory log session-end");
104
+ expect(stopHooks[1].command).toBe("mulch learn");
105
+ });
106
+
107
+ test("PostToolUse hooks include Bash-matched mulch diff hook", async () => {
108
+ await initCommand([]);
109
+
110
+ const hooksPath = join(tempDir, ".overstory", "hooks.json");
111
+ const content = await Bun.file(hooksPath).text();
112
+ const parsed = JSON.parse(content);
113
+ const postToolUseHooks = parsed.hooks.PostToolUse;
114
+
115
+ // Should have the generic tool-end logger plus the new Bash-specific hook
116
+ expect(postToolUseHooks.length).toBe(2);
117
+
118
+ const bashHookEntry = postToolUseHooks[1];
119
+ expect(bashHookEntry.matcher).toBe("Bash");
120
+ expect(bashHookEntry.hooks.length).toBe(1);
121
+
122
+ const command = bashHookEntry.hooks[0].command;
123
+ expect(command).toContain("git commit");
124
+ expect(command).toContain("mulch diff HEAD~1");
125
+ });
126
+ });
127
+
128
+ describe("initCommand: .overstory/.gitignore", () => {
129
+ let tempDir: string;
130
+ let originalCwd: string;
131
+ let originalWrite: typeof process.stdout.write;
132
+
133
+ beforeEach(async () => {
134
+ tempDir = await createTempGitRepo();
135
+ originalCwd = process.cwd();
136
+ process.chdir(tempDir);
137
+
138
+ // Suppress stdout noise from initCommand
139
+ originalWrite = process.stdout.write;
140
+ process.stdout.write = (() => true) as typeof process.stdout.write;
141
+ });
142
+
143
+ afterEach(async () => {
144
+ process.chdir(originalCwd);
145
+ process.stdout.write = originalWrite;
146
+ await cleanupTempDir(tempDir);
147
+ });
148
+
149
+ test("creates .overstory/.gitignore with wildcard+whitelist model", async () => {
150
+ await initCommand([]);
151
+
152
+ const gitignorePath = join(tempDir, ".overstory", ".gitignore");
153
+ const content = await Bun.file(gitignorePath).text();
154
+
155
+ // Verify wildcard+whitelist pattern
156
+ expect(content).toContain("*\n");
157
+ expect(content).toContain("!.gitignore\n");
158
+ expect(content).toContain("!config.yaml\n");
159
+ expect(content).toContain("!agent-manifest.json\n");
160
+ expect(content).toContain("!hooks.json\n");
161
+ expect(content).toContain("!groups.json\n");
162
+ expect(content).toContain("!agent-defs/\n");
163
+
164
+ // Verify it matches the exported constant
165
+ expect(content).toBe(OVERSTORY_GITIGNORE);
166
+ });
167
+
168
+ test("gitignore is always written when init completes", async () => {
169
+ // Init should write gitignore
170
+ await initCommand([]);
171
+
172
+ const gitignorePath = join(tempDir, ".overstory", ".gitignore");
173
+ const content = await Bun.file(gitignorePath).text();
174
+
175
+ // Verify gitignore was written with correct content
176
+ expect(content).toBe(OVERSTORY_GITIGNORE);
177
+
178
+ // Verify the file exists
179
+ const exists = await Bun.file(gitignorePath).exists();
180
+ expect(exists).toBe(true);
181
+ });
182
+
183
+ test("--force reinit overwrites stale .overstory/.gitignore", async () => {
184
+ // First init
185
+ await initCommand([]);
186
+
187
+ const gitignorePath = join(tempDir, ".overstory", ".gitignore");
188
+
189
+ // Tamper with the gitignore file (simulate old deny-list format)
190
+ await Bun.write(gitignorePath, "# old format\nworktrees/\nlogs/\nmail.db\n");
191
+
192
+ // Verify tamper worked
193
+ const tampered = await Bun.file(gitignorePath).text();
194
+ expect(tampered).not.toContain("*\n");
195
+ expect(tampered).not.toContain("!.gitignore\n");
196
+
197
+ // Reinit with --force
198
+ await initCommand(["--force"]);
199
+
200
+ // Verify the file was overwritten with the new wildcard+whitelist format
201
+ const restored = await Bun.file(gitignorePath).text();
202
+ expect(restored).toBe(OVERSTORY_GITIGNORE);
203
+ expect(restored).toContain("*\n");
204
+ expect(restored).toContain("!.gitignore\n");
205
+ });
206
+
207
+ test("subsequent init without --force does not overwrite gitignore", async () => {
208
+ // First init
209
+ await initCommand([]);
210
+
211
+ const gitignorePath = join(tempDir, ".overstory", ".gitignore");
212
+
213
+ // Tamper with the gitignore file
214
+ await Bun.write(gitignorePath, "# custom content\n");
215
+
216
+ // Verify tamper worked
217
+ const tampered = await Bun.file(gitignorePath).text();
218
+ expect(tampered).toBe("# custom content\n");
219
+
220
+ // Second init without --force should return early (not overwrite)
221
+ await initCommand([]);
222
+
223
+ // Verify the file was NOT overwritten (early return prevented it)
224
+ const afterSecondInit = await Bun.file(gitignorePath).text();
225
+ expect(afterSecondInit).toBe("# custom content\n");
226
+ });
227
+ });
228
+
229
+ describe("initCommand: .overstory/README.md", () => {
230
+ let tempDir: string;
231
+ let originalCwd: string;
232
+ let originalWrite: typeof process.stdout.write;
233
+
234
+ beforeEach(async () => {
235
+ tempDir = await createTempGitRepo();
236
+ originalCwd = process.cwd();
237
+ process.chdir(tempDir);
238
+
239
+ // Suppress stdout noise from initCommand
240
+ originalWrite = process.stdout.write;
241
+ process.stdout.write = (() => true) as typeof process.stdout.write;
242
+ });
243
+
244
+ afterEach(async () => {
245
+ process.chdir(originalCwd);
246
+ process.stdout.write = originalWrite;
247
+ await cleanupTempDir(tempDir);
248
+ });
249
+
250
+ test("creates .overstory/README.md with expected content", async () => {
251
+ await initCommand([]);
252
+
253
+ const readmePath = join(tempDir, ".overstory", "README.md");
254
+ const exists = await Bun.file(readmePath).exists();
255
+ expect(exists).toBe(true);
256
+
257
+ const content = await Bun.file(readmePath).text();
258
+ expect(content).toBe(OVERSTORY_README);
259
+ });
260
+
261
+ test("README.md is whitelisted in gitignore", () => {
262
+ expect(OVERSTORY_GITIGNORE).toContain("!README.md\n");
263
+ });
264
+
265
+ test("--force reinit overwrites README.md", async () => {
266
+ // First init
267
+ await initCommand([]);
268
+
269
+ const readmePath = join(tempDir, ".overstory", "README.md");
270
+
271
+ // Tamper with the README
272
+ await Bun.write(readmePath, "# tampered\n");
273
+ const tampered = await Bun.file(readmePath).text();
274
+ expect(tampered).toBe("# tampered\n");
275
+
276
+ // Reinit with --force
277
+ await initCommand(["--force"]);
278
+
279
+ // Verify restored to canonical content
280
+ const restored = await Bun.file(readmePath).text();
281
+ expect(restored).toBe(OVERSTORY_README);
282
+ });
283
+
284
+ test("subsequent init without --force does not overwrite README.md", async () => {
285
+ // First init
286
+ await initCommand([]);
287
+
288
+ const readmePath = join(tempDir, ".overstory", "README.md");
289
+
290
+ // Tamper with the README
291
+ await Bun.write(readmePath, "# custom content\n");
292
+ const tampered = await Bun.file(readmePath).text();
293
+ expect(tampered).toBe("# custom content\n");
294
+
295
+ // Second init without --force returns early
296
+ await initCommand([]);
297
+
298
+ // Verify tampered content preserved (early return)
299
+ const afterSecondInit = await Bun.file(readmePath).text();
300
+ expect(afterSecondInit).toBe("# custom content\n");
301
+ });
302
+ });
303
+
304
+ describe("initCommand: canonical branch detection", () => {
305
+ let tempDir: string;
306
+ let originalCwd: string;
307
+ let originalWrite: typeof process.stdout.write;
308
+
309
+ beforeEach(async () => {
310
+ tempDir = await createTempGitRepo();
311
+ originalCwd = process.cwd();
312
+ // Remove origin remote so detectCanonicalBranch falls through to
313
+ // current-branch check (otherwise remote HEAD resolves to main regardless)
314
+ await runGitInDir(tempDir, ["remote", "remove", "origin"]);
315
+ process.chdir(tempDir);
316
+
317
+ // Suppress stdout noise from initCommand
318
+ originalWrite = process.stdout.write;
319
+ process.stdout.write = (() => true) as typeof process.stdout.write;
320
+ });
321
+
322
+ afterEach(async () => {
323
+ process.chdir(originalCwd);
324
+ process.stdout.write = originalWrite;
325
+ await cleanupTempDir(tempDir);
326
+ });
327
+
328
+ test("non-standard branch names are accepted as canonicalBranch", async () => {
329
+ // Switch to a non-standard branch name
330
+ await runGitInDir(tempDir, ["switch", "-c", "trunk"]);
331
+
332
+ await initCommand([]);
333
+
334
+ const configPath = join(tempDir, ".overstory", "config.yaml");
335
+ const content = await Bun.file(configPath).text();
336
+ expect(content).toContain("canonicalBranch: trunk");
337
+ });
338
+
339
+ test("standard branch names (main) still work as canonicalBranch", async () => {
340
+ // createTempGitRepo defaults to main branch
341
+ await initCommand([]);
342
+
343
+ const configPath = join(tempDir, ".overstory", "config.yaml");
344
+ const content = await Bun.file(configPath).text();
345
+ expect(content).toContain("canonicalBranch: main");
346
+ });
347
+ });