@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,470 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdir, mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { cleanupTempDir, createTempGitRepo } from "../test-helpers.ts";
6
+ import type { AgentSession } from "../types.ts";
7
+ import { primeCommand } from "./prime.ts";
8
+
9
+ /**
10
+ * Tests for `overstory prime` command.
11
+ *
12
+ * Uses real filesystem (temp directories) and process.stdout spy to test
13
+ * the prime command end-to-end.
14
+ */
15
+
16
+ describe("primeCommand", () => {
17
+ let chunks: string[];
18
+ let originalWrite: typeof process.stdout.write;
19
+ let originalStderrWrite: typeof process.stderr.write;
20
+ let stderrChunks: string[];
21
+ let tempDir: string;
22
+ let originalCwd: string;
23
+
24
+ beforeEach(async () => {
25
+ // Spy on stdout
26
+ chunks = [];
27
+ originalWrite = process.stdout.write;
28
+ process.stdout.write = ((chunk: string) => {
29
+ chunks.push(chunk);
30
+ return true;
31
+ }) as typeof process.stdout.write;
32
+
33
+ // Spy on stderr
34
+ stderrChunks = [];
35
+ originalStderrWrite = process.stderr.write;
36
+ process.stderr.write = ((chunk: string) => {
37
+ stderrChunks.push(chunk);
38
+ return true;
39
+ }) as typeof process.stderr.write;
40
+
41
+ // Create temp dir with .overstory/config.yaml structure
42
+ tempDir = await mkdtemp(join(tmpdir(), "prime-test-"));
43
+ const overstoryDir = join(tempDir, ".overstory");
44
+ await Bun.write(
45
+ join(overstoryDir, "config.yaml"),
46
+ `project:\n name: test-project\n root: ${tempDir}\n canonicalBranch: main\nmulch:\n enabled: false\n`,
47
+ );
48
+
49
+ // Change to temp dir so loadConfig() works
50
+ originalCwd = process.cwd();
51
+ process.chdir(tempDir);
52
+ });
53
+
54
+ afterEach(async () => {
55
+ process.stdout.write = originalWrite;
56
+ process.stderr.write = originalStderrWrite;
57
+ process.chdir(originalCwd);
58
+ await rm(tempDir, { recursive: true, force: true });
59
+ });
60
+
61
+ function output(): string {
62
+ return chunks.join("");
63
+ }
64
+
65
+ function stderr(): string {
66
+ return stderrChunks.join("");
67
+ }
68
+
69
+ describe("Help", () => {
70
+ test("--help shows help text", async () => {
71
+ await primeCommand(["--help"]);
72
+ const out = output();
73
+
74
+ expect(out).toContain("overstory prime");
75
+ expect(out).toContain("--agent");
76
+ expect(out).toContain("--compact");
77
+ });
78
+
79
+ test("-h shows help text", async () => {
80
+ await primeCommand(["-h"]);
81
+ const out = output();
82
+
83
+ expect(out).toContain("overstory prime");
84
+ expect(out).toContain("--agent");
85
+ expect(out).toContain("--compact");
86
+ });
87
+ });
88
+
89
+ describe("parseArgs validation", () => {
90
+ test("--agent without a name throws AgentError", async () => {
91
+ await expect(primeCommand(["--agent"])).rejects.toThrow("--agent requires a name argument");
92
+ });
93
+
94
+ test("--agent followed by another flag throws AgentError", async () => {
95
+ await expect(primeCommand(["--agent", "--compact"])).rejects.toThrow(
96
+ "--agent requires a name argument",
97
+ );
98
+ });
99
+ });
100
+
101
+ describe("Orchestrator priming (no --agent flag)", () => {
102
+ test("default prime outputs project context", async () => {
103
+ await primeCommand([]);
104
+ const out = output();
105
+
106
+ expect(out).toContain("# Overstory Context");
107
+ expect(out).toContain("## Project: test-project");
108
+ expect(out).toContain("Canonical branch: main");
109
+ expect(out).toContain("Max concurrent agents:");
110
+ expect(out).toContain("Max depth:");
111
+ });
112
+
113
+ test("includes agent manifest section", async () => {
114
+ await primeCommand([]);
115
+ const out = output();
116
+
117
+ expect(out).toContain("## Agent Manifest");
118
+ // Without manifest file, should show fallback message
119
+ expect(out).toContain("No agent manifest found.");
120
+ });
121
+
122
+ test("without metrics.db shows no recent sessions message", async () => {
123
+ await primeCommand([]);
124
+ const out = output();
125
+
126
+ expect(out).toContain("## Recent Activity");
127
+ expect(out).toContain("No recent sessions.");
128
+ });
129
+
130
+ test("--compact skips Recent Activity and Expertise sections", async () => {
131
+ await primeCommand(["--compact"]);
132
+ const out = output();
133
+
134
+ // Should still have project basics
135
+ expect(out).toContain("# Overstory Context");
136
+ expect(out).toContain("## Project: test-project");
137
+
138
+ // Should NOT have these sections
139
+ expect(out).not.toContain("## Recent Activity");
140
+ expect(out).not.toContain("## Expertise");
141
+ });
142
+ });
143
+
144
+ describe("Agent priming (--agent <name>)", () => {
145
+ test("unknown agent outputs basic context and warns", async () => {
146
+ await primeCommand(["--agent", "unknown-agent"]);
147
+ const out = output();
148
+ const err = stderr();
149
+
150
+ expect(out).toContain("# Agent Context: unknown-agent");
151
+ expect(out).toContain("## Identity");
152
+ expect(out).toContain("New agent - no prior sessions");
153
+ expect(err).toContain('Warning: agent "unknown-agent" not found');
154
+ });
155
+
156
+ test("agent with identity.yaml shows identity details", async () => {
157
+ // Write identity.yaml
158
+ const agentDir = join(tempDir, ".overstory", "agents", "my-builder");
159
+ await Bun.write(
160
+ join(agentDir, "identity.yaml"),
161
+ `name: my-builder
162
+ capability: builder
163
+ created: "2026-01-01T00:00:00Z"
164
+ sessionsCompleted: 3
165
+ expertiseDomains:
166
+ - typescript
167
+ - testing
168
+ recentTasks:
169
+ - beadId: task-001
170
+ summary: "Implemented feature X"
171
+ completedAt: "2026-01-10T12:00:00Z"
172
+ `,
173
+ );
174
+
175
+ await primeCommand(["--agent", "my-builder"]);
176
+ const out = output();
177
+
178
+ expect(out).toContain("# Agent Context: my-builder");
179
+ expect(out).toContain("Name: my-builder");
180
+ expect(out).toContain("Capability: builder");
181
+ expect(out).toContain("Sessions completed: 3");
182
+ expect(out).toContain("Expertise: typescript, testing");
183
+ expect(out).toContain("Recent tasks:");
184
+ expect(out).toContain("task-001: Implemented feature X");
185
+ });
186
+
187
+ test("agent with active session shows Activation section", async () => {
188
+ // Write sessions.json with active session
189
+ const sessions: AgentSession[] = [
190
+ {
191
+ id: "session-001",
192
+ agentName: "active-builder",
193
+ capability: "builder",
194
+ worktreePath: join(tempDir, ".overstory", "worktrees", "active-builder"),
195
+ branchName: "overstory/active-builder/task-001",
196
+ beadId: "task-001",
197
+ tmuxSession: "overstory-active-builder",
198
+ state: "working",
199
+ pid: 12345,
200
+ parentAgent: null,
201
+ depth: 0,
202
+ runId: null,
203
+ startedAt: new Date().toISOString(),
204
+ lastActivity: new Date().toISOString(),
205
+ escalationLevel: 0,
206
+ stalledSince: null,
207
+ },
208
+ ];
209
+
210
+ await Bun.write(
211
+ join(tempDir, ".overstory", "sessions.json"),
212
+ `${JSON.stringify(sessions, null, 2)}\n`,
213
+ );
214
+
215
+ await primeCommand(["--agent", "active-builder"]);
216
+ const out = output();
217
+
218
+ expect(out).toContain("# Agent Context: active-builder");
219
+ expect(out).toContain("## Activation");
220
+ expect(out).toContain("You have a bound task: **task-001**");
221
+ expect(out).toContain("begin working immediately");
222
+ });
223
+
224
+ test("agent with completed session does NOT show Activation", async () => {
225
+ // Write sessions.json with completed session
226
+ const sessions: AgentSession[] = [
227
+ {
228
+ id: "session-002",
229
+ agentName: "completed-builder",
230
+ capability: "builder",
231
+ worktreePath: join(tempDir, ".overstory", "worktrees", "completed-builder"),
232
+ branchName: "overstory/completed-builder/task-002",
233
+ beadId: "task-002",
234
+ tmuxSession: "overstory-completed-builder",
235
+ state: "completed",
236
+ pid: null,
237
+ parentAgent: null,
238
+ depth: 0,
239
+ runId: null,
240
+ startedAt: new Date(Date.now() - 3600000).toISOString(),
241
+ lastActivity: new Date().toISOString(),
242
+ escalationLevel: 0,
243
+ stalledSince: null,
244
+ },
245
+ ];
246
+
247
+ await Bun.write(
248
+ join(tempDir, ".overstory", "sessions.json"),
249
+ `${JSON.stringify(sessions, null, 2)}\n`,
250
+ );
251
+
252
+ await primeCommand(["--agent", "completed-builder"]);
253
+ const out = output();
254
+
255
+ expect(out).toContain("# Agent Context: completed-builder");
256
+ expect(out).not.toContain("## Activation");
257
+ expect(out).not.toContain("bound task");
258
+ });
259
+
260
+ test("--compact with checkpoint.json shows Session Recovery", async () => {
261
+ // Write checkpoint.json
262
+ const agentDir = join(tempDir, ".overstory", "agents", "recovery-agent");
263
+ await Bun.write(
264
+ join(agentDir, "checkpoint.json"),
265
+ `${JSON.stringify(
266
+ {
267
+ agentName: "recovery-agent",
268
+ beadId: "task-003",
269
+ sessionId: "session-003",
270
+ timestamp: new Date().toISOString(),
271
+ progressSummary: "Implemented initial tests for prime command",
272
+ filesModified: ["src/commands/prime.test.ts"],
273
+ currentBranch: "overstory/recovery-agent/task-003",
274
+ pendingWork: "Add tests for edge cases",
275
+ mulchDomains: ["typescript", "testing"],
276
+ },
277
+ null,
278
+ 2,
279
+ )}\n`,
280
+ );
281
+
282
+ // Also need identity to avoid warning
283
+ await Bun.write(
284
+ join(agentDir, "identity.yaml"),
285
+ `name: recovery-agent
286
+ capability: builder
287
+ created: "2026-01-01T00:00:00Z"
288
+ sessionsCompleted: 0
289
+ expertiseDomains: []
290
+ recentTasks: []
291
+ `,
292
+ );
293
+
294
+ await primeCommand(["--agent", "recovery-agent", "--compact"]);
295
+ const out = output();
296
+
297
+ expect(out).toContain("# Agent Context: recovery-agent");
298
+ expect(out).toContain("## Session Recovery");
299
+ expect(out).toContain("Progress so far:** Implemented initial tests for prime command");
300
+ expect(out).toContain("Files modified:** src/commands/prime.test.ts");
301
+ expect(out).toContain("Pending work:** Add tests for edge cases");
302
+ expect(out).toContain("Branch:** overstory/recovery-agent/task-003");
303
+ });
304
+
305
+ test("--compact skips Expertise section", async () => {
306
+ // Write identity with expertise
307
+ const agentDir = join(tempDir, ".overstory", "agents", "compact-agent");
308
+ await Bun.write(
309
+ join(agentDir, "identity.yaml"),
310
+ `name: compact-agent
311
+ capability: builder
312
+ created: "2026-01-01T00:00:00Z"
313
+ sessionsCompleted: 1
314
+ expertiseDomains:
315
+ - typescript
316
+ recentTasks: []
317
+ `,
318
+ );
319
+
320
+ await primeCommand(["--agent", "compact-agent", "--compact"]);
321
+ const out = output();
322
+
323
+ expect(out).toContain("# Agent Context: compact-agent");
324
+ expect(out).not.toContain("## Expertise");
325
+ });
326
+ });
327
+
328
+ describe("Session branch capture", () => {
329
+ test("orchestrator prime writes session-branch.txt with current git branch", async () => {
330
+ // Need a real git repo for branch detection
331
+ const gitRepoDir = await createTempGitRepo();
332
+ try {
333
+ const overstoryDir = join(gitRepoDir, ".overstory");
334
+ await mkdir(overstoryDir, { recursive: true });
335
+ await Bun.write(
336
+ join(overstoryDir, "config.yaml"),
337
+ `project:\n name: branch-test\n root: ${gitRepoDir}\n canonicalBranch: main\nmulch:\n enabled: false\n`,
338
+ );
339
+
340
+ // Save and change cwd to the git repo
341
+ process.chdir(gitRepoDir);
342
+
343
+ await primeCommand([]);
344
+ const out = output();
345
+
346
+ expect(out).toContain("# Overstory Context");
347
+
348
+ // Verify session-branch.txt was written
349
+ const sessionBranchPath = join(overstoryDir, "session-branch.txt");
350
+ const content = await Bun.file(sessionBranchPath).text();
351
+ expect(content.trim()).toBe("main");
352
+ } finally {
353
+ process.chdir(originalCwd);
354
+ await cleanupTempDir(gitRepoDir);
355
+ }
356
+ });
357
+
358
+ test("shows session branch in context when different from canonical", async () => {
359
+ const gitRepoDir = await createTempGitRepo();
360
+ try {
361
+ // Create and switch to a feature branch
362
+ const proc = Bun.spawn(["git", "checkout", "-b", "feature/my-work"], {
363
+ cwd: gitRepoDir,
364
+ stdout: "pipe",
365
+ stderr: "pipe",
366
+ });
367
+ await proc.exited;
368
+
369
+ const overstoryDir = join(gitRepoDir, ".overstory");
370
+ await mkdir(overstoryDir, { recursive: true });
371
+ await Bun.write(
372
+ join(overstoryDir, "config.yaml"),
373
+ `project:\n name: branch-test\n root: ${gitRepoDir}\n canonicalBranch: main\nmulch:\n enabled: false\n`,
374
+ );
375
+
376
+ process.chdir(gitRepoDir);
377
+
378
+ await primeCommand([]);
379
+ const out = output();
380
+
381
+ expect(out).toContain("Session branch: feature/my-work (merge target)");
382
+
383
+ // Verify session-branch.txt was written with the feature branch
384
+ const sessionBranchPath = join(overstoryDir, "session-branch.txt");
385
+ const content = await Bun.file(sessionBranchPath).text();
386
+ expect(content.trim()).toBe("feature/my-work");
387
+ } finally {
388
+ process.chdir(originalCwd);
389
+ await cleanupTempDir(gitRepoDir);
390
+ }
391
+ });
392
+ });
393
+
394
+ describe("Gitignore auto-heal", () => {
395
+ const expectedGitignore = `# Wildcard+whitelist: ignore everything, whitelist tracked files
396
+ # Auto-healed by overstory prime on each session start
397
+ *
398
+ !.gitignore
399
+ !config.yaml
400
+ !agent-manifest.json
401
+ !hooks.json
402
+ !groups.json
403
+ !agent-defs/
404
+ `;
405
+
406
+ test("creates .overstory/.gitignore if missing", async () => {
407
+ // The beforeEach creates .overstory/config.yaml but not .gitignore
408
+ const gitignorePath = join(tempDir, ".overstory", ".gitignore");
409
+
410
+ // Verify it doesn't exist
411
+ const existsBefore = await Bun.file(gitignorePath).exists();
412
+ expect(existsBefore).toBe(false);
413
+
414
+ // Run primeCommand
415
+ await primeCommand([]);
416
+
417
+ // Verify .gitignore was created with correct content
418
+ const content = await Bun.file(gitignorePath).text();
419
+ expect(content).toBe(expectedGitignore);
420
+ });
421
+
422
+ test("overwrites stale .overstory/.gitignore with current template", async () => {
423
+ // Write an old-style deny-list gitignore
424
+ const gitignorePath = join(tempDir, ".overstory", ".gitignore");
425
+ const staleContent = `# Old deny-list format
426
+ worktrees/
427
+ logs/
428
+ mail.db
429
+ sessions.db
430
+ `;
431
+ await Bun.write(gitignorePath, staleContent);
432
+
433
+ // Verify stale content is present
434
+ const contentBefore = await Bun.file(gitignorePath).text();
435
+ expect(contentBefore).toBe(staleContent);
436
+
437
+ // Run primeCommand
438
+ await primeCommand([]);
439
+
440
+ // Verify .gitignore now has the wildcard+whitelist content
441
+ const contentAfter = await Bun.file(gitignorePath).text();
442
+ expect(contentAfter).toBe(expectedGitignore);
443
+ });
444
+
445
+ test("does not overwrite .overstory/.gitignore if already correct", async () => {
446
+ // Write the correct OVERSTORY_GITIGNORE content
447
+ const gitignorePath = join(tempDir, ".overstory", ".gitignore");
448
+ await Bun.write(gitignorePath, expectedGitignore);
449
+
450
+ // Get file stat before
451
+ const statBefore = await Bun.file(gitignorePath).stat();
452
+ const mtimeBefore = statBefore?.mtime;
453
+
454
+ // Wait a tiny bit to ensure mtime would change if file is rewritten
455
+ await new Promise((resolve) => setTimeout(resolve, 10));
456
+
457
+ // Run primeCommand
458
+ await primeCommand([]);
459
+
460
+ // Verify content is still correct
461
+ const contentAfter = await Bun.file(gitignorePath).text();
462
+ expect(contentAfter).toBe(expectedGitignore);
463
+
464
+ // Verify mtime is unchanged (file was not rewritten)
465
+ const statAfter = await Bun.file(gitignorePath).stat();
466
+ const mtimeAfter = statAfter?.mtime;
467
+ expect(mtimeAfter).toEqual(mtimeBefore);
468
+ });
469
+ });
470
+ });