@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,430 @@
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 { createSessionStore } from "../sessions/store.ts";
6
+ import { createTempGitRepo } from "../test-helpers.ts";
7
+ import type { AgentSession } from "../types.ts";
8
+ import {
9
+ gatherStatus,
10
+ invalidateStatusCache,
11
+ printStatus,
12
+ type StatusData,
13
+ statusCommand,
14
+ type VerboseAgentDetail,
15
+ } from "./status.ts";
16
+
17
+ /**
18
+ * Tests for the --verbose flag in overstory status.
19
+ *
20
+ * printStatus is tested by capturing process.stdout.write output.
21
+ * We spy on stdout.write because printStatus uses it directly.
22
+ */
23
+
24
+ function makeAgent(overrides: Partial<AgentSession> = {}): AgentSession {
25
+ return {
26
+ id: "sess-001",
27
+ agentName: "test-builder",
28
+ capability: "builder",
29
+ worktreePath: "/tmp/worktrees/test-builder",
30
+ branchName: "overstory/test-builder/task-1",
31
+ beadId: "task-1",
32
+ tmuxSession: "overstory-test-builder",
33
+ state: "working",
34
+ pid: 12345,
35
+ parentAgent: null,
36
+ depth: 0,
37
+ runId: null,
38
+ startedAt: new Date(Date.now() - 60_000).toISOString(),
39
+ lastActivity: new Date().toISOString(),
40
+ escalationLevel: 0,
41
+ stalledSince: null,
42
+ ...overrides,
43
+ };
44
+ }
45
+
46
+ function makeStatusData(overrides: Partial<StatusData> = {}): StatusData {
47
+ return {
48
+ agents: [makeAgent()],
49
+ worktrees: [],
50
+ tmuxSessions: [{ name: "overstory-test-builder", pid: 12345 }],
51
+ unreadMailCount: 0,
52
+ mergeQueueCount: 0,
53
+ recentMetricsCount: 0,
54
+ ...overrides,
55
+ };
56
+ }
57
+
58
+ describe("printStatus", () => {
59
+ let chunks: string[];
60
+ let originalWrite: typeof process.stdout.write;
61
+
62
+ beforeEach(() => {
63
+ chunks = [];
64
+ originalWrite = process.stdout.write;
65
+ process.stdout.write = ((chunk: string) => {
66
+ chunks.push(chunk);
67
+ return true;
68
+ }) as typeof process.stdout.write;
69
+ });
70
+
71
+ afterEach(() => {
72
+ process.stdout.write = originalWrite;
73
+ });
74
+
75
+ function output(): string {
76
+ return chunks.join("");
77
+ }
78
+
79
+ test("non-verbose: does not show worktree path or logs dir", () => {
80
+ const data = makeStatusData();
81
+ printStatus(data);
82
+ const out = output();
83
+
84
+ expect(out).toContain("test-builder");
85
+ expect(out).toContain("[builder]");
86
+ expect(out).not.toContain("Worktree:");
87
+ expect(out).not.toContain("Logs:");
88
+ expect(out).not.toContain("Mail sent:");
89
+ });
90
+
91
+ test("verbose: shows worktree path, logs dir, and mail timestamps", () => {
92
+ const detail: VerboseAgentDetail = {
93
+ worktreePath: "/tmp/worktrees/test-builder",
94
+ logsDir: "/tmp/.overstory/logs/test-builder",
95
+ lastMailSent: "2025-01-15T10:00:00.000Z",
96
+ lastMailReceived: "2025-01-15T10:05:00.000Z",
97
+ capability: "builder",
98
+ };
99
+
100
+ const data = makeStatusData({
101
+ verboseDetails: { "test-builder": detail },
102
+ });
103
+ printStatus(data);
104
+ const out = output();
105
+
106
+ expect(out).toContain("Worktree: /tmp/worktrees/test-builder");
107
+ expect(out).toContain("Logs: /tmp/.overstory/logs/test-builder");
108
+ expect(out).toContain("Mail sent: 2025-01-15T10:00:00.000Z");
109
+ expect(out).toContain("received: 2025-01-15T10:05:00.000Z");
110
+ });
111
+
112
+ test("verbose: shows 'none' for null mail timestamps", () => {
113
+ const detail: VerboseAgentDetail = {
114
+ worktreePath: "/tmp/worktrees/test-builder",
115
+ logsDir: "/tmp/.overstory/logs/test-builder",
116
+ lastMailSent: null,
117
+ lastMailReceived: null,
118
+ capability: "builder",
119
+ };
120
+
121
+ const data = makeStatusData({
122
+ verboseDetails: { "test-builder": detail },
123
+ });
124
+ printStatus(data);
125
+ const out = output();
126
+
127
+ expect(out).toContain("Mail sent: none");
128
+ expect(out).toContain("received: none");
129
+ });
130
+
131
+ test("verbose: zombie agents do not get verbose detail", () => {
132
+ const agent = makeAgent({ state: "zombie", agentName: "zombie-agent" });
133
+ const detail: VerboseAgentDetail = {
134
+ worktreePath: "/tmp/worktrees/zombie-agent",
135
+ logsDir: "/tmp/.overstory/logs/zombie-agent",
136
+ lastMailSent: null,
137
+ lastMailReceived: null,
138
+ capability: "builder",
139
+ };
140
+
141
+ const data = makeStatusData({
142
+ agents: [agent],
143
+ verboseDetails: { "zombie-agent": detail },
144
+ });
145
+ printStatus(data);
146
+ const out = output();
147
+
148
+ // Zombie agents are filtered from the active list
149
+ expect(out).toContain("0 active");
150
+ expect(out).not.toContain("Worktree:");
151
+ });
152
+
153
+ test("verbose with multiple agents: each gets its own detail", () => {
154
+ const agent1 = makeAgent({ agentName: "builder-1", tmuxSession: "overstory-builder-1" });
155
+ const agent2 = makeAgent({
156
+ agentName: "scout-1",
157
+ capability: "scout",
158
+ tmuxSession: "overstory-scout-1",
159
+ });
160
+
161
+ const data = makeStatusData({
162
+ agents: [agent1, agent2],
163
+ tmuxSessions: [
164
+ { name: "overstory-builder-1", pid: 100 },
165
+ { name: "overstory-scout-1", pid: 200 },
166
+ ],
167
+ verboseDetails: {
168
+ "builder-1": {
169
+ worktreePath: "/tmp/wt/builder-1",
170
+ logsDir: "/tmp/logs/builder-1",
171
+ lastMailSent: "2025-01-15T10:00:00.000Z",
172
+ lastMailReceived: null,
173
+ capability: "builder",
174
+ },
175
+ "scout-1": {
176
+ worktreePath: "/tmp/wt/scout-1",
177
+ logsDir: "/tmp/logs/scout-1",
178
+ lastMailSent: null,
179
+ lastMailReceived: "2025-01-15T11:00:00.000Z",
180
+ capability: "scout",
181
+ },
182
+ },
183
+ });
184
+ printStatus(data);
185
+ const out = output();
186
+
187
+ expect(out).toContain("Worktree: /tmp/wt/builder-1");
188
+ expect(out).toContain("Worktree: /tmp/wt/scout-1");
189
+ expect(out).toContain("Logs: /tmp/logs/builder-1");
190
+ expect(out).toContain("Logs: /tmp/logs/scout-1");
191
+ });
192
+ });
193
+
194
+ describe("--verbose --json", () => {
195
+ test("verboseDetails is included in StatusData when present", () => {
196
+ const detail: VerboseAgentDetail = {
197
+ worktreePath: "/tmp/wt/agent",
198
+ logsDir: "/tmp/logs/agent",
199
+ lastMailSent: "2025-01-15T10:00:00.000Z",
200
+ lastMailReceived: null,
201
+ capability: "builder",
202
+ };
203
+
204
+ const data: StatusData = {
205
+ agents: [],
206
+ worktrees: [],
207
+ tmuxSessions: [],
208
+ unreadMailCount: 0,
209
+ mergeQueueCount: 0,
210
+ recentMetricsCount: 0,
211
+ verboseDetails: { agent: detail },
212
+ };
213
+
214
+ const json = JSON.parse(JSON.stringify(data)) as StatusData;
215
+ expect(json.verboseDetails).toBeDefined();
216
+ expect(json.verboseDetails?.agent?.worktreePath).toBe("/tmp/wt/agent");
217
+ expect(json.verboseDetails?.agent?.lastMailSent).toBe("2025-01-15T10:00:00.000Z");
218
+ expect(json.verboseDetails?.agent?.lastMailReceived).toBeNull();
219
+ });
220
+
221
+ test("verboseDetails is omitted from JSON when undefined", () => {
222
+ const data: StatusData = {
223
+ agents: [],
224
+ worktrees: [],
225
+ tmuxSessions: [],
226
+ unreadMailCount: 0,
227
+ mergeQueueCount: 0,
228
+ recentMetricsCount: 0,
229
+ };
230
+
231
+ const json = JSON.stringify(data);
232
+ expect(json).not.toContain("verboseDetails");
233
+ });
234
+ });
235
+
236
+ describe("run scoping", () => {
237
+ let chunks: string[];
238
+ let originalWrite: typeof process.stdout.write;
239
+
240
+ beforeEach(() => {
241
+ chunks = [];
242
+ originalWrite = process.stdout.write;
243
+ process.stdout.write = ((chunk: string) => {
244
+ chunks.push(chunk);
245
+ return true;
246
+ }) as typeof process.stdout.write;
247
+ });
248
+
249
+ afterEach(() => {
250
+ process.stdout.write = originalWrite;
251
+ invalidateStatusCache();
252
+ });
253
+
254
+ function output(): string {
255
+ return chunks.join("");
256
+ }
257
+
258
+ test("printStatus shows run ID when currentRunId is set", () => {
259
+ const data = makeStatusData({ currentRunId: "run-123" });
260
+ printStatus(data);
261
+ expect(output()).toContain("Run: run-123");
262
+ });
263
+
264
+ test("printStatus does not show run line when currentRunId is undefined", () => {
265
+ const data = makeStatusData();
266
+ printStatus(data);
267
+ expect(output()).not.toContain("Run:");
268
+ });
269
+
270
+ test("printStatus does not show run line when currentRunId is null", () => {
271
+ const data = makeStatusData({ currentRunId: null });
272
+ printStatus(data);
273
+ expect(output()).not.toContain("Run:");
274
+ });
275
+
276
+ test("help text includes --all", async () => {
277
+ const helpChunks: string[] = [];
278
+ const origWrite = process.stdout.write;
279
+ process.stdout.write = ((chunk: string) => {
280
+ helpChunks.push(chunk);
281
+ return true;
282
+ }) as typeof process.stdout.write;
283
+
284
+ try {
285
+ await statusCommand(["--help"]);
286
+ } finally {
287
+ process.stdout.write = origWrite;
288
+ }
289
+
290
+ const out = helpChunks.join("");
291
+ expect(out).toContain("--all");
292
+ });
293
+
294
+ test("gatherStatus includes null-runId sessions when run-scoped", async () => {
295
+ // Use a real git repo so listWorktrees() doesn't throw
296
+ const tempDir = await createTempGitRepo();
297
+ const overstoryDir = join(tempDir, ".overstory");
298
+ await mkdir(overstoryDir, { recursive: true });
299
+
300
+ // Seed sessions.db with three sessions:
301
+ // coordinator: runId=null
302
+ // builder-1: runId="run-001" (in-scope)
303
+ // builder-2: runId="run-002" (out-of-scope)
304
+ const store = createSessionStore(join(overstoryDir, "sessions.db"));
305
+ const now = new Date().toISOString();
306
+ for (const session of [
307
+ makeAgent({
308
+ agentName: "coordinator",
309
+ capability: "coordinator",
310
+ runId: null,
311
+ tmuxSession: "overstory-fake-coordinator",
312
+ }),
313
+ makeAgent({
314
+ id: "sess-002",
315
+ agentName: "builder-1",
316
+ capability: "builder",
317
+ runId: "run-001",
318
+ tmuxSession: "overstory-fake-builder-1",
319
+ }),
320
+ makeAgent({
321
+ id: "sess-003",
322
+ agentName: "builder-2",
323
+ capability: "builder",
324
+ runId: "run-002",
325
+ tmuxSession: "overstory-fake-builder-2",
326
+ }),
327
+ ] as AgentSession[]) {
328
+ session.startedAt = now;
329
+ session.lastActivity = now;
330
+ store.upsert(session);
331
+ }
332
+ store.close();
333
+
334
+ try {
335
+ const result = await gatherStatus(tempDir, "orchestrator", false, "run-001");
336
+ const names = result.agents.map((a) => a.agentName);
337
+
338
+ // coordinator (null runId) must appear
339
+ expect(names).toContain("coordinator");
340
+ // in-scope builder must appear
341
+ expect(names).toContain("builder-1");
342
+ // out-of-scope builder must NOT appear
343
+ expect(names).not.toContain("builder-2");
344
+ } finally {
345
+ await rm(tempDir, { recursive: true, force: true });
346
+ }
347
+ });
348
+ });
349
+
350
+ describe("--watch deprecation", () => {
351
+ test("help text marks --watch as deprecated", async () => {
352
+ const chunks: string[] = [];
353
+ const originalWrite = process.stdout.write;
354
+ process.stdout.write = ((chunk: string) => {
355
+ chunks.push(chunk);
356
+ return true;
357
+ }) as typeof process.stdout.write;
358
+
359
+ try {
360
+ await statusCommand(["--help"]);
361
+ } finally {
362
+ process.stdout.write = originalWrite;
363
+ }
364
+
365
+ const out = chunks.join("");
366
+ expect(out).toContain("deprecated");
367
+ expect(out).toContain("overstory dashboard");
368
+ });
369
+
370
+ test("--watch writes deprecation notice to stderr", async () => {
371
+ const stderrChunks: string[] = [];
372
+ const originalStderr = process.stderr.write;
373
+ process.stderr.write = ((chunk: string) => {
374
+ stderrChunks.push(chunk);
375
+ return true;
376
+ }) as typeof process.stderr.write;
377
+
378
+ // statusCommand with --watch will fail at loadConfig (no .overstory/)
379
+ // but the deprecation notice is written before that. We just verify
380
+ // the notice was emitted.
381
+ const tmpDir = await mkdtemp(join(tmpdir(), "status-deprecation-"));
382
+ const originalCwd = process.cwd();
383
+ process.chdir(tmpDir);
384
+
385
+ try {
386
+ await statusCommand(["--watch"]);
387
+ } catch {
388
+ // Expected: loadConfig fails without .overstory/
389
+ } finally {
390
+ process.stderr.write = originalStderr;
391
+ process.chdir(originalCwd);
392
+ await rm(tmpDir, { recursive: true, force: true });
393
+ }
394
+
395
+ const err = stderrChunks.join("");
396
+ expect(err).toContain("--watch is deprecated");
397
+ expect(err).toContain("overstory dashboard");
398
+ });
399
+ });
400
+
401
+ describe("subprocess caching (invalidateStatusCache)", () => {
402
+ afterEach(() => {
403
+ invalidateStatusCache();
404
+ });
405
+
406
+ test("invalidateStatusCache is exported and callable", () => {
407
+ // Should not throw
408
+ invalidateStatusCache();
409
+ });
410
+
411
+ test("invalidateStatusCache resets cache so gatherStatus re-fetches on next call", async () => {
412
+ const tempDir = await createTempGitRepo();
413
+ const overstoryDir = join(tempDir, ".overstory");
414
+ await mkdir(overstoryDir, { recursive: true });
415
+
416
+ try {
417
+ // First call populates the cache
418
+ const result1 = await gatherStatus(tempDir, "orchestrator", false, undefined);
419
+ // Invalidate cache
420
+ invalidateStatusCache();
421
+ // Second call must succeed (re-fetches, no stale cache issues)
422
+ const result2 = await gatherStatus(tempDir, "orchestrator", false, undefined);
423
+ // Both results should have the same structure
424
+ expect(Array.isArray(result1.worktrees)).toBe(true);
425
+ expect(Array.isArray(result2.worktrees)).toBe(true);
426
+ } finally {
427
+ await rm(tempDir, { recursive: true, force: true });
428
+ }
429
+ });
430
+ });