@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,651 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
+ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { createSessionStore } from "../sessions/store.ts";
6
+ import type { OverstoryConfig } from "../types.ts";
7
+ import type { ConsistencyCheckDeps } from "./consistency.ts";
8
+ import { checkConsistency } from "./consistency.ts";
9
+
10
+ /**
11
+ * Mock tmux functions using dependency injection instead of mock.module().
12
+ * This avoids test isolation issues from module-level mocking.
13
+ */
14
+ const mockListSessions = mock(() => Promise.resolve([] as Array<{ name: string; pid: number }>));
15
+ const mockIsProcessAlive = mock((_pid: number) => true);
16
+
17
+ /**
18
+ * Create a minimal temp git repo for worktree tests.
19
+ */
20
+ function createTempGitRepo(): string {
21
+ const dir = mkdtempSync(join(tmpdir(), "overstory-test-"));
22
+ const git = (args: string[]) => {
23
+ const proc = Bun.spawnSync(["git", ...args], { cwd: dir, stdout: "ignore", stderr: "pipe" });
24
+ if (proc.exitCode !== 0) {
25
+ throw new Error(`git ${args.join(" ")} failed: ${proc.stderr.toString()}`);
26
+ }
27
+ };
28
+
29
+ git(["init"]);
30
+ git(["config", "user.email", "test@test.com"]);
31
+ git(["config", "user.name", "Test User"]);
32
+ git(["config", "commit.gpgsign", "false"]);
33
+ writeFileSync(join(dir, "README.md"), "# Test Repo\n");
34
+ git(["add", "."]);
35
+ git(["commit", "-m", "Initial commit"]);
36
+
37
+ return dir;
38
+ }
39
+
40
+ /**
41
+ * Create a git worktree at the given path.
42
+ */
43
+ function createWorktree(repoRoot: string, worktreePath: string, branchName: string): void {
44
+ const proc = Bun.spawnSync(["git", "worktree", "add", "-b", branchName, worktreePath, "HEAD"], {
45
+ cwd: repoRoot,
46
+ stdout: "ignore",
47
+ stderr: "pipe",
48
+ });
49
+ if (proc.exitCode !== 0) {
50
+ throw new Error(`Failed to create worktree: ${proc.stderr.toString()}`);
51
+ }
52
+ }
53
+
54
+ describe("checkConsistency", () => {
55
+ let repoRoot: string;
56
+ let overstoryDir: string;
57
+ let config: OverstoryConfig;
58
+ let mockDeps: ConsistencyCheckDeps;
59
+
60
+ beforeEach(() => {
61
+ repoRoot = createTempGitRepo();
62
+ overstoryDir = join(repoRoot, ".overstory");
63
+ mkdirSync(overstoryDir, { recursive: true });
64
+ mkdirSync(join(overstoryDir, "worktrees"), { recursive: true });
65
+
66
+ config = {
67
+ project: {
68
+ name: "testproject",
69
+ root: repoRoot,
70
+ canonicalBranch: "main",
71
+ },
72
+ agents: {
73
+ manifestPath: join(overstoryDir, "agent-manifest.json"),
74
+ baseDir: join(repoRoot, "agents"),
75
+ maxConcurrent: 5,
76
+ staggerDelayMs: 100,
77
+ maxDepth: 2,
78
+ maxSessionsPerRun: 0,
79
+ },
80
+ worktrees: {
81
+ baseDir: join(overstoryDir, "worktrees"),
82
+ },
83
+ taskTracker: {
84
+ backend: "auto",
85
+ enabled: false,
86
+ },
87
+ mulch: {
88
+ enabled: false,
89
+ domains: [],
90
+ primeFormat: "markdown",
91
+ },
92
+ merge: {
93
+ aiResolveEnabled: false,
94
+ reimagineEnabled: false,
95
+ },
96
+ providers: {
97
+ anthropic: { type: "native" },
98
+ },
99
+ watchdog: {
100
+ tier0Enabled: false,
101
+ tier0IntervalMs: 30000,
102
+ tier1Enabled: false,
103
+ tier2Enabled: false,
104
+ staleThresholdMs: 60000,
105
+ zombieThresholdMs: 300000,
106
+ nudgeIntervalMs: 30000,
107
+ },
108
+ models: {},
109
+ logging: {
110
+ verbose: false,
111
+ redactSecrets: true,
112
+ },
113
+ };
114
+
115
+ // Reset mocks and create deps object
116
+ mockListSessions.mockReset();
117
+ mockIsProcessAlive.mockReset();
118
+ mockListSessions.mockResolvedValue([]);
119
+ mockIsProcessAlive.mockReturnValue(true);
120
+
121
+ mockDeps = {
122
+ listSessions: mockListSessions,
123
+ isProcessAlive: mockIsProcessAlive,
124
+ };
125
+ });
126
+
127
+ afterEach(() => {
128
+ if (existsSync(repoRoot)) {
129
+ rmSync(repoRoot, { recursive: true, force: true });
130
+ }
131
+ });
132
+
133
+ test("returns all pass when no sessions exist", async () => {
134
+ const checks = await checkConsistency(config, overstoryDir, mockDeps);
135
+
136
+ expect(checks.length).toBeGreaterThan(0);
137
+ const passChecks = checks.filter((c) => c.status === "pass");
138
+ expect(passChecks.length).toBeGreaterThan(0);
139
+
140
+ const failChecks = checks.filter((c) => c.status === "fail");
141
+ expect(failChecks.length).toBe(0);
142
+ });
143
+
144
+ test("detects orphaned worktrees", async () => {
145
+ // Create a worktree but don't add it to SessionStore
146
+ const worktreePath = join(overstoryDir, "worktrees", "orphan-agent");
147
+ createWorktree(repoRoot, worktreePath, "overstory/orphan-agent/test-123");
148
+
149
+ const checks = await checkConsistency(config, overstoryDir, mockDeps);
150
+
151
+ const orphanCheck = checks.find((c) => c.name === "orphaned-worktrees");
152
+ expect(orphanCheck).toBeDefined();
153
+ expect(orphanCheck?.status).toBe("warn");
154
+ expect(orphanCheck?.message).toContain("1 orphaned worktree");
155
+ expect(orphanCheck?.details?.length).toBe(1);
156
+ expect(orphanCheck?.fixable).toBe(true);
157
+ });
158
+
159
+ test("detects orphaned tmux sessions", async () => {
160
+ // Mock a tmux session that isn't in SessionStore
161
+ mockListSessions.mockResolvedValue([{ name: "overstory-testproject-orphan", pid: 9999 }]);
162
+
163
+ const checks = await checkConsistency(config, overstoryDir, mockDeps);
164
+
165
+ const orphanCheck = checks.find((c) => c.name === "orphaned-tmux");
166
+ expect(orphanCheck).toBeDefined();
167
+ expect(orphanCheck?.status).toBe("warn");
168
+ expect(orphanCheck?.message).toContain("1 orphaned tmux session");
169
+ expect(orphanCheck?.fixable).toBe(true);
170
+ });
171
+
172
+ test("ignores tmux sessions from other projects", async () => {
173
+ // Mock tmux sessions from different projects
174
+ mockListSessions.mockResolvedValue([
175
+ { name: "overstory-otherproject-agent1", pid: 9999 },
176
+ { name: "my-custom-session", pid: 8888 },
177
+ ]);
178
+
179
+ const checks = await checkConsistency(config, overstoryDir, mockDeps);
180
+
181
+ const orphanCheck = checks.find((c) => c.name === "orphaned-tmux");
182
+ expect(orphanCheck).toBeDefined();
183
+ expect(orphanCheck?.status).toBe("pass");
184
+ expect(orphanCheck?.message).toContain("No orphaned tmux sessions");
185
+ });
186
+
187
+ test("detects dead PIDs in SessionStore", async () => {
188
+ // Create a session with a PID that's marked as dead
189
+ const dbPath = join(overstoryDir, "sessions.db");
190
+ const store = createSessionStore(dbPath);
191
+
192
+ store.upsert({
193
+ id: "session-1",
194
+ agentName: "dead-agent",
195
+ capability: "builder",
196
+ worktreePath: join(overstoryDir, "worktrees", "dead-agent"),
197
+ branchName: "overstory/dead-agent/test-123",
198
+ beadId: "test-123",
199
+ tmuxSession: "overstory-testproject-dead-agent",
200
+ state: "working",
201
+ pid: 99999, // Non-existent PID
202
+ parentAgent: null,
203
+ depth: 0,
204
+ runId: null,
205
+ startedAt: new Date().toISOString(),
206
+ lastActivity: new Date().toISOString(),
207
+ escalationLevel: 0,
208
+ stalledSince: null,
209
+ });
210
+ store.close();
211
+
212
+ // Mock that this PID is not alive
213
+ mockIsProcessAlive.mockReturnValue(false);
214
+
215
+ const checks = await checkConsistency(config, overstoryDir, mockDeps);
216
+
217
+ const deadPidCheck = checks.find((c) => c.name === "dead-pids");
218
+ expect(deadPidCheck).toBeDefined();
219
+ expect(deadPidCheck?.status).toBe("warn");
220
+ expect(deadPidCheck?.message).toContain("1 session(s) with dead PIDs");
221
+ expect(deadPidCheck?.fixable).toBe(true);
222
+ });
223
+
224
+ test("passes when all PIDs are alive", async () => {
225
+ const dbPath = join(overstoryDir, "sessions.db");
226
+ const store = createSessionStore(dbPath);
227
+
228
+ store.upsert({
229
+ id: "session-1",
230
+ agentName: "live-agent",
231
+ capability: "builder",
232
+ worktreePath: join(overstoryDir, "worktrees", "live-agent"),
233
+ branchName: "overstory/live-agent/test-123",
234
+ beadId: "test-123",
235
+ tmuxSession: "overstory-testproject-live-agent",
236
+ state: "working",
237
+ pid: 12345,
238
+ parentAgent: null,
239
+ depth: 0,
240
+ runId: null,
241
+ startedAt: new Date().toISOString(),
242
+ lastActivity: new Date().toISOString(),
243
+ escalationLevel: 0,
244
+ stalledSince: null,
245
+ });
246
+ store.close();
247
+
248
+ // Mock that this PID is alive
249
+ mockIsProcessAlive.mockReturnValue(true);
250
+
251
+ const checks = await checkConsistency(config, overstoryDir, mockDeps);
252
+
253
+ const deadPidCheck = checks.find((c) => c.name === "dead-pids");
254
+ expect(deadPidCheck).toBeDefined();
255
+ expect(deadPidCheck?.status).toBe("pass");
256
+ });
257
+
258
+ test("detects missing worktrees for SessionStore entries", async () => {
259
+ const dbPath = join(overstoryDir, "sessions.db");
260
+ const store = createSessionStore(dbPath);
261
+
262
+ const missingWorktreePath = join(overstoryDir, "worktrees", "missing-agent");
263
+ store.upsert({
264
+ id: "session-1",
265
+ agentName: "missing-agent",
266
+ capability: "builder",
267
+ worktreePath: missingWorktreePath,
268
+ branchName: "overstory/missing-agent/test-123",
269
+ beadId: "test-123",
270
+ tmuxSession: "overstory-testproject-missing-agent",
271
+ state: "working",
272
+ pid: null,
273
+ parentAgent: null,
274
+ depth: 0,
275
+ runId: null,
276
+ startedAt: new Date().toISOString(),
277
+ lastActivity: new Date().toISOString(),
278
+ escalationLevel: 0,
279
+ stalledSince: null,
280
+ });
281
+ store.close();
282
+
283
+ const checks = await checkConsistency(config, overstoryDir, mockDeps);
284
+
285
+ const missingCheck = checks.find((c) => c.name === "missing-worktrees");
286
+ expect(missingCheck).toBeDefined();
287
+ expect(missingCheck?.status).toBe("warn");
288
+ expect(missingCheck?.message).toContain("1 session(s) with missing worktrees");
289
+ expect(missingCheck?.fixable).toBe(true);
290
+ });
291
+
292
+ test("detects missing tmux sessions for SessionStore entries", async () => {
293
+ const dbPath = join(overstoryDir, "sessions.db");
294
+ const store = createSessionStore(dbPath);
295
+
296
+ const worktreePath = join(overstoryDir, "worktrees", "agent-without-tmux");
297
+ createWorktree(repoRoot, worktreePath, "overstory/agent-without-tmux/test-123");
298
+
299
+ store.upsert({
300
+ id: "session-1",
301
+ agentName: "agent-without-tmux",
302
+ capability: "builder",
303
+ worktreePath,
304
+ branchName: "overstory/agent-without-tmux/test-123",
305
+ beadId: "test-123",
306
+ tmuxSession: "overstory-testproject-agent-without-tmux",
307
+ state: "working",
308
+ pid: null,
309
+ parentAgent: null,
310
+ depth: 0,
311
+ runId: null,
312
+ startedAt: new Date().toISOString(),
313
+ lastActivity: new Date().toISOString(),
314
+ escalationLevel: 0,
315
+ stalledSince: null,
316
+ });
317
+ store.close();
318
+
319
+ // Mock empty tmux sessions list
320
+ mockListSessions.mockResolvedValue([]);
321
+
322
+ const checks = await checkConsistency(config, overstoryDir, mockDeps);
323
+
324
+ const missingCheck = checks.find((c) => c.name === "missing-tmux");
325
+ expect(missingCheck).toBeDefined();
326
+ expect(missingCheck?.status).toBe("warn");
327
+ expect(missingCheck?.message).toContain("1 session(s) with missing tmux sessions");
328
+ expect(missingCheck?.fixable).toBe(true);
329
+ });
330
+
331
+ test("passes when everything is consistent", async () => {
332
+ const dbPath = join(overstoryDir, "sessions.db");
333
+ const store = createSessionStore(dbPath);
334
+
335
+ const worktreePath = join(overstoryDir, "worktrees", "consistent-agent");
336
+ createWorktree(repoRoot, worktreePath, "overstory/consistent-agent/test-123");
337
+
338
+ store.upsert({
339
+ id: "session-1",
340
+ agentName: "consistent-agent",
341
+ capability: "builder",
342
+ worktreePath,
343
+ branchName: "overstory/consistent-agent/test-123",
344
+ beadId: "test-123",
345
+ tmuxSession: "overstory-testproject-consistent-agent",
346
+ state: "working",
347
+ pid: 12345,
348
+ parentAgent: null,
349
+ depth: 0,
350
+ runId: null,
351
+ startedAt: new Date().toISOString(),
352
+ lastActivity: new Date().toISOString(),
353
+ escalationLevel: 0,
354
+ stalledSince: null,
355
+ });
356
+ store.close();
357
+
358
+ // Mock matching tmux session
359
+ mockListSessions.mockResolvedValue([
360
+ { name: "overstory-testproject-consistent-agent", pid: 12345 },
361
+ ]);
362
+
363
+ // Mock PID as alive
364
+ mockIsProcessAlive.mockReturnValue(true);
365
+
366
+ const checks = await checkConsistency(config, overstoryDir, mockDeps);
367
+
368
+ const warnOrFail = checks.filter((c) => c.status === "warn" || c.status === "fail");
369
+ expect(warnOrFail.length).toBe(0);
370
+ });
371
+
372
+ test("handles tmux not installed gracefully", async () => {
373
+ // Mock tmux listing to throw an error
374
+ mockListSessions.mockRejectedValue(new Error("tmux: command not found"));
375
+
376
+ const checks = await checkConsistency(config, overstoryDir, mockDeps);
377
+
378
+ const tmuxCheck = checks.find((c) => c.name === "tmux-listing");
379
+ expect(tmuxCheck).toBeDefined();
380
+ expect(tmuxCheck?.status).toBe("warn");
381
+ expect(tmuxCheck?.message).toContain("Failed to list tmux sessions");
382
+ });
383
+
384
+ test("fails early if git worktree list fails", async () => {
385
+ // Use a non-existent repo root to trigger worktree listing failure
386
+ const badConfig = { ...config, project: { ...config.project, root: "/nonexistent" } };
387
+
388
+ const checks = await checkConsistency(badConfig, overstoryDir, mockDeps);
389
+
390
+ expect(checks.length).toBe(1);
391
+ expect(checks[0]?.name).toBe("worktree-listing");
392
+ expect(checks[0]?.status).toBe("fail");
393
+ });
394
+
395
+ test("fails early if SessionStore cannot be opened", async () => {
396
+ // Use a bad overstory directory path
397
+ const badOverstoryDir = "/nonexistent/.overstory";
398
+
399
+ const checks = await checkConsistency(config, badOverstoryDir, mockDeps);
400
+
401
+ const storeCheck = checks.find((c) => c.name === "sessionstore-open");
402
+ expect(storeCheck).toBeDefined();
403
+ expect(storeCheck?.status).toBe("fail");
404
+ });
405
+
406
+ test("reviewer-coverage: leads without reviewers emits warn", async () => {
407
+ const dbPath = join(overstoryDir, "sessions.db");
408
+ const store = createSessionStore(dbPath);
409
+
410
+ // Add 2 builder sessions under lead-1, no reviewers
411
+ store.upsert({
412
+ id: "session-1",
413
+ agentName: "builder-1",
414
+ capability: "builder",
415
+ worktreePath: join(overstoryDir, "worktrees", "builder-1"),
416
+ branchName: "overstory/builder-1/test-123",
417
+ beadId: "test-123",
418
+ tmuxSession: "overstory-testproject-builder-1",
419
+ state: "working",
420
+ pid: null,
421
+ parentAgent: "lead-1",
422
+ depth: 1,
423
+ runId: null,
424
+ startedAt: new Date().toISOString(),
425
+ lastActivity: new Date().toISOString(),
426
+ escalationLevel: 0,
427
+ stalledSince: null,
428
+ });
429
+
430
+ store.upsert({
431
+ id: "session-2",
432
+ agentName: "builder-2",
433
+ capability: "builder",
434
+ worktreePath: join(overstoryDir, "worktrees", "builder-2"),
435
+ branchName: "overstory/builder-2/test-456",
436
+ beadId: "test-456",
437
+ tmuxSession: "overstory-testproject-builder-2",
438
+ state: "working",
439
+ pid: null,
440
+ parentAgent: "lead-1",
441
+ depth: 1,
442
+ runId: null,
443
+ startedAt: new Date().toISOString(),
444
+ lastActivity: new Date().toISOString(),
445
+ escalationLevel: 0,
446
+ stalledSince: null,
447
+ });
448
+ store.close();
449
+
450
+ const checks = await checkConsistency(config, overstoryDir, mockDeps);
451
+
452
+ const reviewerCheck = checks.find((c) => c.name === "reviewer-coverage");
453
+ expect(reviewerCheck).toBeDefined();
454
+ expect(reviewerCheck?.status).toBe("warn");
455
+ expect(reviewerCheck?.message).toContain("without any reviewers");
456
+ expect(reviewerCheck?.details).toBeDefined();
457
+ expect(reviewerCheck?.details?.length).toBeGreaterThan(0);
458
+ });
459
+
460
+ test("reviewer-coverage: partial reviewer coverage emits warn", async () => {
461
+ const dbPath = join(overstoryDir, "sessions.db");
462
+ const store = createSessionStore(dbPath);
463
+
464
+ // Add 3 builders and 1 reviewer under same parent
465
+ for (let i = 1; i <= 3; i++) {
466
+ store.upsert({
467
+ id: `session-builder-${i}`,
468
+ agentName: `builder-${i}`,
469
+ capability: "builder",
470
+ worktreePath: join(overstoryDir, "worktrees", `builder-${i}`),
471
+ branchName: `overstory/builder-${i}/test-${i}`,
472
+ beadId: `test-${i}`,
473
+ tmuxSession: `overstory-testproject-builder-${i}`,
474
+ state: "working",
475
+ pid: null,
476
+ parentAgent: "lead-1",
477
+ depth: 1,
478
+ runId: null,
479
+ startedAt: new Date().toISOString(),
480
+ lastActivity: new Date().toISOString(),
481
+ escalationLevel: 0,
482
+ stalledSince: null,
483
+ });
484
+ }
485
+
486
+ store.upsert({
487
+ id: "session-reviewer-1",
488
+ agentName: "reviewer-1",
489
+ capability: "reviewer",
490
+ worktreePath: join(overstoryDir, "worktrees", "reviewer-1"),
491
+ branchName: "overstory/reviewer-1/test-r1",
492
+ beadId: "test-r1",
493
+ tmuxSession: "overstory-testproject-reviewer-1",
494
+ state: "working",
495
+ pid: null,
496
+ parentAgent: "lead-1",
497
+ depth: 1,
498
+ runId: null,
499
+ startedAt: new Date().toISOString(),
500
+ lastActivity: new Date().toISOString(),
501
+ escalationLevel: 0,
502
+ stalledSince: null,
503
+ });
504
+ store.close();
505
+
506
+ const checks = await checkConsistency(config, overstoryDir, mockDeps);
507
+
508
+ const reviewerCheck = checks.find((c) => c.name === "reviewer-coverage");
509
+ expect(reviewerCheck).toBeDefined();
510
+ expect(reviewerCheck?.status).toBe("warn");
511
+ expect(reviewerCheck?.message).toContain("partial reviewer coverage");
512
+ });
513
+
514
+ test("reviewer-coverage: full reviewer coverage emits pass", async () => {
515
+ const dbPath = join(overstoryDir, "sessions.db");
516
+ const store = createSessionStore(dbPath);
517
+
518
+ // Add 2 builders and 2 reviewers under same parent
519
+ for (let i = 1; i <= 2; i++) {
520
+ store.upsert({
521
+ id: `session-builder-${i}`,
522
+ agentName: `builder-${i}`,
523
+ capability: "builder",
524
+ worktreePath: join(overstoryDir, "worktrees", `builder-${i}`),
525
+ branchName: `overstory/builder-${i}/test-${i}`,
526
+ beadId: `test-${i}`,
527
+ tmuxSession: `overstory-testproject-builder-${i}`,
528
+ state: "working",
529
+ pid: null,
530
+ parentAgent: "lead-1",
531
+ depth: 1,
532
+ runId: null,
533
+ startedAt: new Date().toISOString(),
534
+ lastActivity: new Date().toISOString(),
535
+ escalationLevel: 0,
536
+ stalledSince: null,
537
+ });
538
+
539
+ store.upsert({
540
+ id: `session-reviewer-${i}`,
541
+ agentName: `reviewer-${i}`,
542
+ capability: "reviewer",
543
+ worktreePath: join(overstoryDir, "worktrees", `reviewer-${i}`),
544
+ branchName: `overstory/reviewer-${i}/test-r${i}`,
545
+ beadId: `test-r${i}`,
546
+ tmuxSession: `overstory-testproject-reviewer-${i}`,
547
+ state: "working",
548
+ pid: null,
549
+ parentAgent: "lead-1",
550
+ depth: 1,
551
+ runId: null,
552
+ startedAt: new Date().toISOString(),
553
+ lastActivity: new Date().toISOString(),
554
+ escalationLevel: 0,
555
+ stalledSince: null,
556
+ });
557
+ }
558
+ store.close();
559
+
560
+ const checks = await checkConsistency(config, overstoryDir, mockDeps);
561
+
562
+ const reviewerCheck = checks.find((c) => c.name === "reviewer-coverage");
563
+ expect(reviewerCheck).toBeDefined();
564
+ expect(reviewerCheck?.status).toBe("pass");
565
+ });
566
+
567
+ test("reviewer-coverage: no builder sessions emits pass", async () => {
568
+ // Don't create any sessions at all
569
+ const checks = await checkConsistency(config, overstoryDir, mockDeps);
570
+
571
+ const reviewerCheck = checks.find((c) => c.name === "reviewer-coverage");
572
+ expect(reviewerCheck).toBeDefined();
573
+ expect(reviewerCheck?.status).toBe("pass");
574
+ expect(reviewerCheck?.message).toContain("No builder sessions found");
575
+ });
576
+
577
+ test("reviewer-coverage: multiple leads mixed coverage", async () => {
578
+ const dbPath = join(overstoryDir, "sessions.db");
579
+ const store = createSessionStore(dbPath);
580
+
581
+ // Lead-1 has builders + reviewers (good)
582
+ store.upsert({
583
+ id: "session-builder-1",
584
+ agentName: "builder-1",
585
+ capability: "builder",
586
+ worktreePath: join(overstoryDir, "worktrees", "builder-1"),
587
+ branchName: "overstory/builder-1/test-1",
588
+ beadId: "test-1",
589
+ tmuxSession: "overstory-testproject-builder-1",
590
+ state: "working",
591
+ pid: null,
592
+ parentAgent: "lead-1",
593
+ depth: 1,
594
+ runId: null,
595
+ startedAt: new Date().toISOString(),
596
+ lastActivity: new Date().toISOString(),
597
+ escalationLevel: 0,
598
+ stalledSince: null,
599
+ });
600
+
601
+ store.upsert({
602
+ id: "session-reviewer-1",
603
+ agentName: "reviewer-1",
604
+ capability: "reviewer",
605
+ worktreePath: join(overstoryDir, "worktrees", "reviewer-1"),
606
+ branchName: "overstory/reviewer-1/test-r1",
607
+ beadId: "test-r1",
608
+ tmuxSession: "overstory-testproject-reviewer-1",
609
+ state: "working",
610
+ pid: null,
611
+ parentAgent: "lead-1",
612
+ depth: 1,
613
+ runId: null,
614
+ startedAt: new Date().toISOString(),
615
+ lastActivity: new Date().toISOString(),
616
+ escalationLevel: 0,
617
+ stalledSince: null,
618
+ });
619
+
620
+ // Lead-2 has builders only (bad)
621
+ store.upsert({
622
+ id: "session-builder-2",
623
+ agentName: "builder-2",
624
+ capability: "builder",
625
+ worktreePath: join(overstoryDir, "worktrees", "builder-2"),
626
+ branchName: "overstory/builder-2/test-2",
627
+ beadId: "test-2",
628
+ tmuxSession: "overstory-testproject-builder-2",
629
+ state: "working",
630
+ pid: null,
631
+ parentAgent: "lead-2",
632
+ depth: 1,
633
+ runId: null,
634
+ startedAt: new Date().toISOString(),
635
+ lastActivity: new Date().toISOString(),
636
+ escalationLevel: 0,
637
+ stalledSince: null,
638
+ });
639
+ store.close();
640
+
641
+ const checks = await checkConsistency(config, overstoryDir, mockDeps);
642
+
643
+ const reviewerCheck = checks.find((c) => c.name === "reviewer-coverage");
644
+ expect(reviewerCheck).toBeDefined();
645
+ expect(reviewerCheck?.status).toBe("warn");
646
+ expect(reviewerCheck?.details).toBeDefined();
647
+ // Should contain lead-2 in the details
648
+ const detailsStr = reviewerCheck?.details?.join(" ");
649
+ expect(detailsStr).toContain("lead-2");
650
+ });
651
+ });