@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,1530 @@
1
+ /**
2
+ * Tests for overstory coordinator command.
3
+ *
4
+ * Uses real temp directories and real git repos for file I/O and config loading.
5
+ * Tmux is injected via the CoordinatorDeps DI interface instead of
6
+ * mock.module() to avoid the process-global mock leak issue
7
+ * (see mulch record mx-56558b).
8
+ *
9
+ * WHY DI instead of mock.module: mock.module() in bun:test is process-global
10
+ * and leaks across test files. The DI approach (same pattern as daemon.ts
11
+ * _tmux/_triage/_nudge) ensures mocks are scoped to each test invocation.
12
+ */
13
+
14
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
15
+ import { mkdir, realpath } from "node:fs/promises";
16
+ import { join } from "node:path";
17
+ import { AgentError, ValidationError } from "../errors.ts";
18
+ import { openSessionStore } from "../sessions/compat.ts";
19
+ import { createRunStore } from "../sessions/store.ts";
20
+ import { cleanupTempDir, createTempGitRepo } from "../test-helpers.ts";
21
+ import type { AgentSession } from "../types.ts";
22
+ import {
23
+ buildCoordinatorBeacon,
24
+ type CoordinatorDeps,
25
+ coordinatorCommand,
26
+ resolveAttach,
27
+ } from "./coordinator.ts";
28
+ import { isRunningAsRoot } from "./sling.ts";
29
+
30
+ // --- Fake Tmux ---
31
+
32
+ /** Track calls to fake tmux for assertions. */
33
+ interface TmuxCallTracker {
34
+ createSession: Array<{
35
+ name: string;
36
+ cwd: string;
37
+ command: string;
38
+ env?: Record<string, string>;
39
+ }>;
40
+ isSessionAlive: Array<{ name: string; result: boolean }>;
41
+ killSession: Array<{ name: string }>;
42
+ sendKeys: Array<{ name: string; keys: string }>;
43
+ waitForTuiReady: Array<{ name: string }>;
44
+ }
45
+
46
+ // --- Fake Watchdog ---
47
+
48
+ /** Track calls to fake watchdog for assertions. */
49
+ interface WatchdogCallTracker {
50
+ start: number;
51
+ stop: number;
52
+ isRunning: number;
53
+ }
54
+
55
+ // --- Fake Monitor ---
56
+
57
+ /** Track calls to fake monitor for assertions. */
58
+ interface MonitorCallTracker {
59
+ start: number;
60
+ stop: number;
61
+ isRunning: number;
62
+ }
63
+
64
+ /** Build a fake tmux DI object with configurable session liveness. */
65
+ function makeFakeTmux(sessionAliveMap: Record<string, boolean> = {}): {
66
+ tmux: NonNullable<CoordinatorDeps["_tmux"]>;
67
+ calls: TmuxCallTracker;
68
+ } {
69
+ const calls: TmuxCallTracker = {
70
+ createSession: [],
71
+ isSessionAlive: [],
72
+ killSession: [],
73
+ sendKeys: [],
74
+ waitForTuiReady: [],
75
+ };
76
+
77
+ const tmux: NonNullable<CoordinatorDeps["_tmux"]> = {
78
+ createSession: async (
79
+ name: string,
80
+ cwd: string,
81
+ command: string,
82
+ env?: Record<string, string>,
83
+ ): Promise<number> => {
84
+ calls.createSession.push({ name, cwd, command, env });
85
+ return 99999; // Fake PID
86
+ },
87
+ isSessionAlive: async (name: string): Promise<boolean> => {
88
+ const alive = sessionAliveMap[name] ?? false;
89
+ calls.isSessionAlive.push({ name, result: alive });
90
+ return alive;
91
+ },
92
+ killSession: async (name: string): Promise<void> => {
93
+ calls.killSession.push({ name });
94
+ },
95
+ sendKeys: async (name: string, keys: string): Promise<void> => {
96
+ calls.sendKeys.push({ name, keys });
97
+ },
98
+ waitForTuiReady: async (name: string): Promise<boolean> => {
99
+ calls.waitForTuiReady.push({ name });
100
+ return true;
101
+ },
102
+ };
103
+
104
+ return { tmux, calls };
105
+ }
106
+
107
+ /**
108
+ * Build a fake watchdog DI object with configurable behavior.
109
+ * @param running - Whether the watchdog should report as running
110
+ * @param startSuccess - Whether start() should succeed (return a PID)
111
+ * @param stopSuccess - Whether stop() should succeed (return true)
112
+ */
113
+ function makeFakeWatchdog(
114
+ running = false,
115
+ startSuccess = true,
116
+ stopSuccess = true,
117
+ ): {
118
+ watchdog: NonNullable<CoordinatorDeps["_watchdog"]>;
119
+ calls: WatchdogCallTracker;
120
+ } {
121
+ const calls: WatchdogCallTracker = {
122
+ start: 0,
123
+ stop: 0,
124
+ isRunning: 0,
125
+ };
126
+
127
+ const watchdog: NonNullable<CoordinatorDeps["_watchdog"]> = {
128
+ async start(): Promise<{ pid: number } | null> {
129
+ calls.start++;
130
+ return startSuccess ? { pid: 88888 } : null;
131
+ },
132
+ async stop(): Promise<boolean> {
133
+ calls.stop++;
134
+ return stopSuccess;
135
+ },
136
+ async isRunning(): Promise<boolean> {
137
+ calls.isRunning++;
138
+ return running;
139
+ },
140
+ };
141
+
142
+ return { watchdog, calls };
143
+ }
144
+
145
+ /**
146
+ * Build a fake monitor DI object with configurable behavior.
147
+ * @param running - Whether the monitor should report as running
148
+ * @param startSuccess - Whether start() should succeed (return a PID)
149
+ * @param stopSuccess - Whether stop() should succeed (return true)
150
+ */
151
+ function makeFakeMonitor(
152
+ running = false,
153
+ startSuccess = true,
154
+ stopSuccess = true,
155
+ ): {
156
+ monitor: NonNullable<CoordinatorDeps["_monitor"]>;
157
+ calls: MonitorCallTracker;
158
+ } {
159
+ const calls: MonitorCallTracker = {
160
+ start: 0,
161
+ stop: 0,
162
+ isRunning: 0,
163
+ };
164
+
165
+ const monitor: NonNullable<CoordinatorDeps["_monitor"]> = {
166
+ async start(): Promise<{ pid: number } | null> {
167
+ calls.start++;
168
+ return startSuccess ? { pid: 77777 } : null;
169
+ },
170
+ async stop(): Promise<boolean> {
171
+ calls.stop++;
172
+ return stopSuccess;
173
+ },
174
+ async isRunning(): Promise<boolean> {
175
+ calls.isRunning++;
176
+ return running;
177
+ },
178
+ };
179
+
180
+ return { monitor, calls };
181
+ }
182
+
183
+ // --- Test Setup ---
184
+
185
+ let tempDir: string;
186
+ let overstoryDir: string;
187
+ const originalCwd = process.cwd();
188
+
189
+ /** Save sessions to the SessionStore (sessions.db) for test setup. */
190
+ function saveSessionsToDb(sessions: AgentSession[]): void {
191
+ const { store } = openSessionStore(overstoryDir);
192
+ try {
193
+ for (const session of sessions) {
194
+ store.upsert(session);
195
+ }
196
+ } finally {
197
+ store.close();
198
+ }
199
+ }
200
+
201
+ /** Load all sessions from the SessionStore (sessions.db). */
202
+ function loadSessionsFromDb(): AgentSession[] {
203
+ const { store } = openSessionStore(overstoryDir);
204
+ try {
205
+ return store.getAll();
206
+ } finally {
207
+ store.close();
208
+ }
209
+ }
210
+
211
+ beforeEach(async () => {
212
+ // Restore cwd FIRST so createTempGitRepo's git operations don't fail
213
+ // if a prior test's tempDir was already cleaned up.
214
+ process.chdir(originalCwd);
215
+
216
+ tempDir = await realpath(await createTempGitRepo());
217
+ overstoryDir = join(tempDir, ".overstory");
218
+ await mkdir(overstoryDir, { recursive: true });
219
+
220
+ // Write a minimal config.yaml so loadConfig succeeds
221
+ // tier2Enabled: true so existing --monitor tests pass (new skipped tests override inline)
222
+ await Bun.write(
223
+ join(overstoryDir, "config.yaml"),
224
+ [
225
+ "project:",
226
+ " name: test-project",
227
+ ` root: ${tempDir}`,
228
+ " canonicalBranch: main",
229
+ "watchdog:",
230
+ " tier2Enabled: true",
231
+ ].join("\n"),
232
+ );
233
+
234
+ // Write agent-manifest.json and stub agent-def .md files so manifest loading succeeds
235
+ const agentDefsDir = join(overstoryDir, "agent-defs");
236
+ await mkdir(agentDefsDir, { recursive: true });
237
+ const manifest = {
238
+ version: "1.0",
239
+ agents: {
240
+ coordinator: {
241
+ file: "coordinator.md",
242
+ model: "opus",
243
+ tools: ["Read", "Bash"],
244
+ capabilities: ["coordinate"],
245
+ canSpawn: true,
246
+ constraints: [],
247
+ },
248
+ },
249
+ capabilityIndex: { coordinate: ["coordinator"] },
250
+ };
251
+ await Bun.write(
252
+ join(overstoryDir, "agent-manifest.json"),
253
+ `${JSON.stringify(manifest, null, "\t")}\n`,
254
+ );
255
+ await Bun.write(join(agentDefsDir, "coordinator.md"), "# Coordinator\n");
256
+
257
+ // Override cwd so coordinator commands find our temp project
258
+ process.chdir(tempDir);
259
+ });
260
+
261
+ afterEach(async () => {
262
+ process.chdir(originalCwd);
263
+ await cleanupTempDir(tempDir);
264
+ });
265
+
266
+ // --- Helpers ---
267
+
268
+ function makeCoordinatorSession(overrides: Partial<AgentSession> = {}): AgentSession {
269
+ return {
270
+ id: `session-${Date.now()}-coordinator`,
271
+ agentName: "coordinator",
272
+ capability: "coordinator",
273
+ worktreePath: tempDir,
274
+ branchName: "main",
275
+ beadId: "",
276
+ tmuxSession: "overstory-test-project-coordinator",
277
+ state: "working",
278
+ pid: 99999,
279
+ parentAgent: null,
280
+ depth: 0,
281
+ runId: null,
282
+ startedAt: new Date().toISOString(),
283
+ lastActivity: new Date().toISOString(),
284
+ escalationLevel: 0,
285
+ stalledSince: null,
286
+ ...overrides,
287
+ };
288
+ }
289
+
290
+ /** Capture stdout.write output during a function call. */
291
+ async function captureStdout(fn: () => Promise<void>): Promise<string> {
292
+ const chunks: string[] = [];
293
+ const originalWrite = process.stdout.write;
294
+ process.stdout.write = ((chunk: string) => {
295
+ chunks.push(chunk);
296
+ return true;
297
+ }) as typeof process.stdout.write;
298
+ try {
299
+ await fn();
300
+ } finally {
301
+ process.stdout.write = originalWrite;
302
+ }
303
+ return chunks.join("");
304
+ }
305
+
306
+ /** Build default CoordinatorDeps with fake tmux, watchdog, and monitor.
307
+ * Always injects fakes for all three to prevent real Bun.spawn(["overstory", ...])
308
+ * calls in tests (overstory CLI is not available in CI). */
309
+ function makeDeps(
310
+ sessionAliveMap: Record<string, boolean> = {},
311
+ watchdogConfig?: { running?: boolean; startSuccess?: boolean; stopSuccess?: boolean },
312
+ monitorConfig?: { running?: boolean; startSuccess?: boolean; stopSuccess?: boolean },
313
+ ): {
314
+ deps: CoordinatorDeps;
315
+ calls: TmuxCallTracker;
316
+ watchdogCalls: WatchdogCallTracker;
317
+ monitorCalls: MonitorCallTracker;
318
+ } {
319
+ const { tmux, calls } = makeFakeTmux(sessionAliveMap);
320
+ const { watchdog, calls: watchdogCalls } = makeFakeWatchdog(
321
+ watchdogConfig?.running,
322
+ watchdogConfig?.startSuccess,
323
+ watchdogConfig?.stopSuccess,
324
+ );
325
+ const { monitor, calls: monitorCalls } = makeFakeMonitor(
326
+ monitorConfig?.running,
327
+ monitorConfig?.startSuccess,
328
+ monitorConfig?.stopSuccess,
329
+ );
330
+
331
+ const deps: CoordinatorDeps = {
332
+ _tmux: tmux,
333
+ _watchdog: watchdog,
334
+ _monitor: monitor,
335
+ };
336
+
337
+ return {
338
+ deps,
339
+ calls,
340
+ watchdogCalls,
341
+ monitorCalls,
342
+ };
343
+ }
344
+
345
+ // --- Tests ---
346
+
347
+ describe("coordinatorCommand help", () => {
348
+ test("--help outputs help text", async () => {
349
+ const output = await captureStdout(() => coordinatorCommand(["--help"]));
350
+ expect(output).toContain("overstory coordinator");
351
+ expect(output).toContain("start");
352
+ expect(output).toContain("stop");
353
+ expect(output).toContain("status");
354
+ });
355
+
356
+ test("--help includes --attach and --no-attach flags", async () => {
357
+ const output = await captureStdout(() => coordinatorCommand(["--help"]));
358
+ expect(output).toContain("--attach");
359
+ expect(output).toContain("--no-attach");
360
+ });
361
+
362
+ test("-h outputs help text", async () => {
363
+ const output = await captureStdout(() => coordinatorCommand(["-h"]));
364
+ expect(output).toContain("overstory coordinator");
365
+ });
366
+
367
+ test("empty args outputs help text", async () => {
368
+ const output = await captureStdout(() => coordinatorCommand([]));
369
+ expect(output).toContain("overstory coordinator");
370
+ expect(output).toContain("Subcommands:");
371
+ });
372
+ });
373
+
374
+ describe("coordinatorCommand unknown subcommand", () => {
375
+ test("throws ValidationError for unknown subcommand", async () => {
376
+ await expect(coordinatorCommand(["frobnicate"])).rejects.toThrow(ValidationError);
377
+ });
378
+
379
+ test("error message includes the bad subcommand name", async () => {
380
+ try {
381
+ await coordinatorCommand(["frobnicate"]);
382
+ expect.unreachable("should have thrown");
383
+ } catch (err) {
384
+ expect(err).toBeInstanceOf(ValidationError);
385
+ const ve = err as ValidationError;
386
+ expect(ve.message).toContain("frobnicate");
387
+ expect(ve.field).toBe("subcommand");
388
+ expect(ve.value).toBe("frobnicate");
389
+ }
390
+ });
391
+ });
392
+
393
+ describe("startCoordinator", () => {
394
+ test("writes session to sessions.json with correct fields", async () => {
395
+ const { deps, calls } = makeDeps();
396
+
397
+ // Override Bun.sleep to skip the 3s and 0.5s waits
398
+ const originalSleep = Bun.sleep;
399
+ Bun.sleep = (() => Promise.resolve()) as typeof Bun.sleep;
400
+
401
+ try {
402
+ await captureStdout(() => coordinatorCommand(["start"], deps));
403
+ } finally {
404
+ Bun.sleep = originalSleep;
405
+ }
406
+
407
+ // Verify sessions.json was written
408
+ const sessions = loadSessionsFromDb();
409
+ expect(sessions).toHaveLength(1);
410
+
411
+ const session = sessions[0];
412
+ expect(session).toBeDefined();
413
+ expect(session?.agentName).toBe("coordinator");
414
+ expect(session?.capability).toBe("coordinator");
415
+ expect(session?.tmuxSession).toBe("overstory-test-project-coordinator");
416
+ expect(session?.state).toBe("booting");
417
+ expect(session?.pid).toBe(99999);
418
+ expect(session?.parentAgent).toBeNull();
419
+ expect(session?.depth).toBe(0);
420
+ expect(session?.beadId).toBe("");
421
+ expect(session?.branchName).toBe("main");
422
+ expect(session?.worktreePath).toBe(tempDir);
423
+ expect(session?.id).toMatch(/^session-\d+-coordinator$/);
424
+
425
+ // Verify tmux createSession was called
426
+ expect(calls.createSession).toHaveLength(1);
427
+ expect(calls.createSession[0]?.name).toBe("overstory-test-project-coordinator");
428
+ expect(calls.createSession[0]?.cwd).toBe(tempDir);
429
+
430
+ // Verify sendKeys was called (beacon + follow-up Enter)
431
+ expect(calls.sendKeys.length).toBeGreaterThanOrEqual(1);
432
+ });
433
+
434
+ test("deploys hooks to project root .claude/settings.local.json", async () => {
435
+ const { deps } = makeDeps();
436
+ const originalSleep = Bun.sleep;
437
+ Bun.sleep = (() => Promise.resolve()) as typeof Bun.sleep;
438
+
439
+ try {
440
+ await captureStdout(() => coordinatorCommand(["start", "--no-attach"], deps));
441
+ } finally {
442
+ Bun.sleep = originalSleep;
443
+ }
444
+
445
+ // Verify .claude/settings.local.json was created at the project root
446
+ const settingsPath = join(tempDir, ".claude", "settings.local.json");
447
+ const settingsFile = Bun.file(settingsPath);
448
+ expect(await settingsFile.exists()).toBe(true);
449
+
450
+ const content = await settingsFile.text();
451
+ const config = JSON.parse(content) as {
452
+ hooks: Record<string, unknown[]>;
453
+ };
454
+
455
+ // Verify hook categories exist
456
+ expect(config.hooks).toBeDefined();
457
+ expect(config.hooks.SessionStart).toBeDefined();
458
+ expect(config.hooks.UserPromptSubmit).toBeDefined();
459
+ expect(config.hooks.PreToolUse).toBeDefined();
460
+ expect(config.hooks.PostToolUse).toBeDefined();
461
+ expect(config.hooks.Stop).toBeDefined();
462
+ });
463
+
464
+ test("hooks use coordinator agent name for event logging", async () => {
465
+ const { deps } = makeDeps();
466
+ const originalSleep = Bun.sleep;
467
+ Bun.sleep = (() => Promise.resolve()) as typeof Bun.sleep;
468
+
469
+ try {
470
+ await captureStdout(() => coordinatorCommand(["start", "--no-attach"], deps));
471
+ } finally {
472
+ Bun.sleep = originalSleep;
473
+ }
474
+
475
+ const settingsPath = join(tempDir, ".claude", "settings.local.json");
476
+ const content = await Bun.file(settingsPath).text();
477
+
478
+ // The hooks should reference the coordinator agent name
479
+ expect(content).toContain("--agent coordinator");
480
+ });
481
+
482
+ test("hooks include ENV_GUARD to avoid affecting user's Claude Code session", async () => {
483
+ const { deps } = makeDeps();
484
+ const originalSleep = Bun.sleep;
485
+ Bun.sleep = (() => Promise.resolve()) as typeof Bun.sleep;
486
+
487
+ try {
488
+ await captureStdout(() => coordinatorCommand(["start", "--no-attach"], deps));
489
+ } finally {
490
+ Bun.sleep = originalSleep;
491
+ }
492
+
493
+ const settingsPath = join(tempDir, ".claude", "settings.local.json");
494
+ const content = await Bun.file(settingsPath).text();
495
+
496
+ // PreToolUse guards should include the ENV_GUARD prefix
497
+ expect(content).toContain("OVERSTORY_AGENT_NAME");
498
+ });
499
+
500
+ test("injects agent definition via --append-system-prompt when agent-defs/coordinator.md exists", async () => {
501
+ // Deploy a coordinator agent definition
502
+ const agentDefsDir = join(overstoryDir, "agent-defs");
503
+ await mkdir(agentDefsDir, { recursive: true });
504
+ await Bun.write(
505
+ join(agentDefsDir, "coordinator.md"),
506
+ "# Coordinator Agent\n\nYou are the coordinator.\n",
507
+ );
508
+
509
+ const { deps, calls } = makeDeps();
510
+ const originalSleep = Bun.sleep;
511
+ Bun.sleep = (() => Promise.resolve()) as typeof Bun.sleep;
512
+
513
+ try {
514
+ await captureStdout(() => coordinatorCommand(["start", "--no-attach", "--json"], deps));
515
+ } finally {
516
+ Bun.sleep = originalSleep;
517
+ }
518
+
519
+ expect(calls.createSession).toHaveLength(1);
520
+ const cmd = calls.createSession[0]?.command ?? "";
521
+ expect(cmd).toContain("--append-system-prompt");
522
+ expect(cmd).toContain("# Coordinator Agent");
523
+ });
524
+
525
+ test("reads model from manifest instead of hardcoding", async () => {
526
+ // Override the manifest to use sonnet instead of default opus
527
+ const manifest = {
528
+ version: "1.0",
529
+ agents: {
530
+ coordinator: {
531
+ file: "coordinator.md",
532
+ model: "sonnet",
533
+ tools: ["Read", "Bash"],
534
+ capabilities: ["coordinate"],
535
+ canSpawn: true,
536
+ constraints: [],
537
+ },
538
+ },
539
+ capabilityIndex: { coordinate: ["coordinator"] },
540
+ };
541
+ await Bun.write(
542
+ join(overstoryDir, "agent-manifest.json"),
543
+ `${JSON.stringify(manifest, null, "\t")}\n`,
544
+ );
545
+
546
+ const { deps, calls } = makeDeps();
547
+ const originalSleep = Bun.sleep;
548
+ Bun.sleep = (() => Promise.resolve()) as typeof Bun.sleep;
549
+
550
+ try {
551
+ await captureStdout(() => coordinatorCommand(["start", "--no-attach", "--json"], deps));
552
+ } finally {
553
+ Bun.sleep = originalSleep;
554
+ }
555
+
556
+ expect(calls.createSession).toHaveLength(1);
557
+ const cmd = calls.createSession[0]?.command ?? "";
558
+ expect(cmd).toContain("--model sonnet");
559
+ expect(cmd).not.toContain("--model opus");
560
+ });
561
+
562
+ test("--json outputs JSON with expected fields", async () => {
563
+ const { deps } = makeDeps();
564
+ const originalSleep = Bun.sleep;
565
+ Bun.sleep = (() => Promise.resolve()) as typeof Bun.sleep;
566
+
567
+ let output: string;
568
+ try {
569
+ output = await captureStdout(() => coordinatorCommand(["start", "--json"], deps));
570
+ } finally {
571
+ Bun.sleep = originalSleep;
572
+ }
573
+
574
+ const parsed = JSON.parse(output) as Record<string, unknown>;
575
+ expect(parsed.agentName).toBe("coordinator");
576
+ expect(parsed.capability).toBe("coordinator");
577
+ expect(parsed.tmuxSession).toBe("overstory-test-project-coordinator");
578
+ expect(parsed.pid).toBe(99999);
579
+ expect(parsed.projectRoot).toBe(tempDir);
580
+ });
581
+
582
+ test("rejects duplicate when coordinator is already running", async () => {
583
+ // Write an existing active coordinator session
584
+ const existing = makeCoordinatorSession({ state: "working" });
585
+ saveSessionsToDb([existing]);
586
+
587
+ // Mock tmux as alive for the existing session
588
+ const { deps } = makeDeps({ "overstory-test-project-coordinator": true });
589
+
590
+ await expect(coordinatorCommand(["start"], deps)).rejects.toThrow(AgentError);
591
+
592
+ try {
593
+ await coordinatorCommand(["start"], deps);
594
+ } catch (err) {
595
+ expect(err).toBeInstanceOf(AgentError);
596
+ const ae = err as AgentError;
597
+ expect(ae.message).toContain("already running");
598
+ }
599
+ });
600
+
601
+ test("cleans up dead session and starts new one", async () => {
602
+ // Write an existing session that claims to be working
603
+ const deadSession = makeCoordinatorSession({
604
+ id: "session-dead-coordinator",
605
+ state: "working",
606
+ });
607
+ saveSessionsToDb([deadSession]);
608
+
609
+ // Mock tmux as NOT alive for the existing session
610
+ const { deps } = makeDeps({ "overstory-test-project-coordinator": false });
611
+
612
+ const originalSleep = Bun.sleep;
613
+ Bun.sleep = (() => Promise.resolve()) as typeof Bun.sleep;
614
+
615
+ try {
616
+ await captureStdout(() => coordinatorCommand(["start"], deps));
617
+ } finally {
618
+ Bun.sleep = originalSleep;
619
+ }
620
+
621
+ // SessionStore uses UNIQUE(agent_name), so the new session replaces the old one.
622
+ // Verify the new session is in booting state with the coordinator name.
623
+ const sessions = loadSessionsFromDb();
624
+ expect(sessions).toHaveLength(1);
625
+
626
+ const newSession = sessions[0];
627
+ expect(newSession).toBeDefined();
628
+ expect(newSession?.state).toBe("booting");
629
+ expect(newSession?.agentName).toBe("coordinator");
630
+ // The new session should have a different ID than the dead one
631
+ expect(newSession?.id).not.toBe("session-dead-coordinator");
632
+ });
633
+ });
634
+
635
+ describe("stopCoordinator", () => {
636
+ test("marks session as completed after stopping", async () => {
637
+ const session = makeCoordinatorSession({ state: "working" });
638
+ saveSessionsToDb([session]);
639
+
640
+ // Tmux is alive so killSession will be called
641
+ const { deps, calls } = makeDeps({ "overstory-test-project-coordinator": true });
642
+
643
+ await captureStdout(() => coordinatorCommand(["stop"], deps));
644
+
645
+ // Verify session is now completed
646
+ const sessions = loadSessionsFromDb();
647
+ expect(sessions).toHaveLength(1);
648
+ expect(sessions[0]?.state).toBe("completed");
649
+
650
+ // Verify killSession was called
651
+ expect(calls.killSession).toHaveLength(1);
652
+ expect(calls.killSession[0]?.name).toBe("overstory-test-project-coordinator");
653
+ });
654
+
655
+ test("--json outputs JSON with stopped flag", async () => {
656
+ const session = makeCoordinatorSession({ state: "working" });
657
+ saveSessionsToDb([session]);
658
+ const { deps } = makeDeps({ "overstory-test-project-coordinator": true });
659
+
660
+ const output = await captureStdout(() => coordinatorCommand(["stop", "--json"], deps));
661
+ const parsed = JSON.parse(output) as Record<string, unknown>;
662
+ expect(parsed.stopped).toBe(true);
663
+ expect(parsed.sessionId).toBe(session.id);
664
+ });
665
+
666
+ test("handles already-dead tmux session gracefully", async () => {
667
+ const session = makeCoordinatorSession({ state: "working" });
668
+ saveSessionsToDb([session]);
669
+
670
+ // Tmux is NOT alive — should skip killSession
671
+ const { deps, calls } = makeDeps({ "overstory-test-project-coordinator": false });
672
+
673
+ await captureStdout(() => coordinatorCommand(["stop"], deps));
674
+
675
+ // Verify session is completed
676
+ const sessions = loadSessionsFromDb();
677
+ expect(sessions[0]?.state).toBe("completed");
678
+
679
+ // killSession should NOT have been called since session was already dead
680
+ expect(calls.killSession).toHaveLength(0);
681
+ });
682
+
683
+ test("throws AgentError when no coordinator session exists", async () => {
684
+ const { deps } = makeDeps();
685
+
686
+ // No sessions.json at all
687
+ await expect(coordinatorCommand(["stop"], deps)).rejects.toThrow(AgentError);
688
+
689
+ try {
690
+ await coordinatorCommand(["stop"], deps);
691
+ } catch (err) {
692
+ expect(err).toBeInstanceOf(AgentError);
693
+ const ae = err as AgentError;
694
+ expect(ae.message).toContain("No active coordinator session");
695
+ }
696
+ });
697
+
698
+ test("throws AgentError when only completed sessions exist", async () => {
699
+ const completed = makeCoordinatorSession({ state: "completed" });
700
+ saveSessionsToDb([completed]);
701
+ const { deps } = makeDeps();
702
+
703
+ await expect(coordinatorCommand(["stop"], deps)).rejects.toThrow(AgentError);
704
+ });
705
+ });
706
+
707
+ describe("stopCoordinator run completion", () => {
708
+ test("coordinator stop auto-completes the active run", async () => {
709
+ // Create a coordinator session
710
+ const session = makeCoordinatorSession({ state: "working" });
711
+ saveSessionsToDb([session]);
712
+
713
+ // Create a run in RunStore
714
+ const dbPath = join(overstoryDir, "sessions.db");
715
+ const runStore = createRunStore(dbPath);
716
+ runStore.createRun({
717
+ id: "run-test-123",
718
+ startedAt: new Date().toISOString(),
719
+ coordinatorSessionId: null,
720
+ status: "active",
721
+ });
722
+ runStore.close();
723
+
724
+ // Write current-run.txt
725
+ await Bun.write(join(overstoryDir, "current-run.txt"), "run-test-123");
726
+
727
+ // Stop coordinator
728
+ const { deps } = makeDeps({ "overstory-test-project-coordinator": true });
729
+ await captureStdout(() => coordinatorCommand(["stop"], deps));
730
+
731
+ // Verify run status is "completed"
732
+ const runStoreCheck = createRunStore(dbPath);
733
+ const run = runStoreCheck.getRun("run-test-123");
734
+ runStoreCheck.close();
735
+ expect(run?.status).toBe("completed");
736
+
737
+ // Verify current-run.txt is deleted
738
+ const currentRunFile = Bun.file(join(overstoryDir, "current-run.txt"));
739
+ expect(await currentRunFile.exists()).toBe(false);
740
+ });
741
+
742
+ test("coordinator stop succeeds when no active run exists", async () => {
743
+ // Create a coordinator session
744
+ const session = makeCoordinatorSession({ state: "working" });
745
+ saveSessionsToDb([session]);
746
+
747
+ // No current-run.txt
748
+
749
+ // Stop coordinator (should succeed without errors)
750
+ const { deps } = makeDeps({ "overstory-test-project-coordinator": true });
751
+ await expect(captureStdout(() => coordinatorCommand(["stop"], deps))).resolves.toBeDefined();
752
+
753
+ // Verify session is completed
754
+ const sessions = loadSessionsFromDb();
755
+ expect(sessions[0]?.state).toBe("completed");
756
+ });
757
+
758
+ test("coordinator stop succeeds when current-run.txt is empty", async () => {
759
+ // Create a coordinator session
760
+ const session = makeCoordinatorSession({ state: "working" });
761
+ saveSessionsToDb([session]);
762
+
763
+ // Write empty current-run.txt
764
+ await Bun.write(join(overstoryDir, "current-run.txt"), "");
765
+
766
+ // Stop coordinator (should succeed without errors)
767
+ const { deps } = makeDeps({ "overstory-test-project-coordinator": true });
768
+ await expect(captureStdout(() => coordinatorCommand(["stop"], deps))).resolves.toBeDefined();
769
+
770
+ // Verify session is completed
771
+ const sessions = loadSessionsFromDb();
772
+ expect(sessions[0]?.state).toBe("completed");
773
+ });
774
+
775
+ test("--json output includes runCompleted field", async () => {
776
+ // Create a coordinator session
777
+ const session = makeCoordinatorSession({ state: "working" });
778
+ saveSessionsToDb([session]);
779
+
780
+ // Create a run in RunStore
781
+ const dbPath = join(overstoryDir, "sessions.db");
782
+ const runStore = createRunStore(dbPath);
783
+ runStore.createRun({
784
+ id: "run-test-456",
785
+ startedAt: new Date().toISOString(),
786
+ coordinatorSessionId: null,
787
+ status: "active",
788
+ });
789
+ runStore.close();
790
+
791
+ // Write current-run.txt
792
+ await Bun.write(join(overstoryDir, "current-run.txt"), "run-test-456");
793
+
794
+ // Stop coordinator with --json
795
+ const { deps } = makeDeps({ "overstory-test-project-coordinator": true });
796
+ const output = await captureStdout(() => coordinatorCommand(["stop", "--json"], deps));
797
+
798
+ // Verify output includes runCompleted: true
799
+ const parsed = JSON.parse(output) as Record<string, unknown>;
800
+ expect(parsed.runCompleted).toBe(true);
801
+ });
802
+
803
+ test("--json output includes runCompleted:false when no run", async () => {
804
+ // Create a coordinator session
805
+ const session = makeCoordinatorSession({ state: "working" });
806
+ saveSessionsToDb([session]);
807
+
808
+ // No current-run.txt
809
+
810
+ // Stop coordinator with --json
811
+ const { deps } = makeDeps({ "overstory-test-project-coordinator": true });
812
+ const output = await captureStdout(() => coordinatorCommand(["stop", "--json"], deps));
813
+
814
+ // Verify output includes runCompleted: false
815
+ const parsed = JSON.parse(output) as Record<string, unknown>;
816
+ expect(parsed.runCompleted).toBe(false);
817
+ });
818
+ });
819
+
820
+ describe("statusCoordinator", () => {
821
+ test("shows 'not running' when no session exists", async () => {
822
+ const { deps } = makeDeps();
823
+ const output = await captureStdout(() => coordinatorCommand(["status"], deps));
824
+ expect(output).toContain("not running");
825
+ });
826
+
827
+ test("--json shows running:false when no session exists", async () => {
828
+ const { deps } = makeDeps();
829
+ const output = await captureStdout(() => coordinatorCommand(["status", "--json"], deps));
830
+ const parsed = JSON.parse(output) as Record<string, unknown>;
831
+ expect(parsed.running).toBe(false);
832
+ });
833
+
834
+ test("shows running state when coordinator is alive", async () => {
835
+ const session = makeCoordinatorSession({ state: "working" });
836
+ saveSessionsToDb([session]);
837
+ const { deps } = makeDeps({ "overstory-test-project-coordinator": true });
838
+
839
+ const output = await captureStdout(() => coordinatorCommand(["status"], deps));
840
+ expect(output).toContain("running");
841
+ expect(output).toContain(session.id);
842
+ expect(output).toContain("overstory-test-project-coordinator");
843
+ });
844
+
845
+ test("--json shows correct fields when running", async () => {
846
+ const session = makeCoordinatorSession({ state: "working", pid: 99999 });
847
+ saveSessionsToDb([session]);
848
+ const { deps } = makeDeps({ "overstory-test-project-coordinator": true });
849
+
850
+ const output = await captureStdout(() => coordinatorCommand(["status", "--json"], deps));
851
+ const parsed = JSON.parse(output) as Record<string, unknown>;
852
+ expect(parsed.running).toBe(true);
853
+ expect(parsed.sessionId).toBe(session.id);
854
+ expect(parsed.state).toBe("working");
855
+ expect(parsed.tmuxSession).toBe("overstory-test-project-coordinator");
856
+ expect(parsed.pid).toBe(99999);
857
+ });
858
+
859
+ test("reconciles zombie: updates state when tmux is dead but session says working", async () => {
860
+ const session = makeCoordinatorSession({ state: "working" });
861
+ saveSessionsToDb([session]);
862
+
863
+ // Tmux is NOT alive — triggers zombie reconciliation
864
+ const { deps } = makeDeps({ "overstory-test-project-coordinator": false });
865
+
866
+ const output = await captureStdout(() => coordinatorCommand(["status", "--json"], deps));
867
+ const parsed = JSON.parse(output) as Record<string, unknown>;
868
+ expect(parsed.running).toBe(false);
869
+ expect(parsed.state).toBe("zombie");
870
+
871
+ // Verify sessions.json was updated
872
+ const sessions = loadSessionsFromDb();
873
+ expect(sessions[0]?.state).toBe("zombie");
874
+ });
875
+
876
+ test("reconciles zombie for booting state too", async () => {
877
+ const session = makeCoordinatorSession({ state: "booting" });
878
+ saveSessionsToDb([session]);
879
+ const { deps } = makeDeps({ "overstory-test-project-coordinator": false });
880
+
881
+ const output = await captureStdout(() => coordinatorCommand(["status", "--json"], deps));
882
+ const parsed = JSON.parse(output) as Record<string, unknown>;
883
+ expect(parsed.state).toBe("zombie");
884
+ });
885
+
886
+ test("does not show completed sessions as active", async () => {
887
+ const completed = makeCoordinatorSession({ state: "completed" });
888
+ saveSessionsToDb([completed]);
889
+ const { deps } = makeDeps();
890
+
891
+ const output = await captureStdout(() => coordinatorCommand(["status", "--json"], deps));
892
+ const parsed = JSON.parse(output) as Record<string, unknown>;
893
+ expect(parsed.running).toBe(false);
894
+ });
895
+ });
896
+
897
+ describe("buildCoordinatorBeacon", () => {
898
+ test("is a single line (no newlines)", () => {
899
+ const beacon = buildCoordinatorBeacon();
900
+ expect(beacon).not.toContain("\n");
901
+ });
902
+
903
+ test("includes coordinator identity in header", () => {
904
+ const beacon = buildCoordinatorBeacon();
905
+ expect(beacon).toContain("[OVERSTORY] coordinator (coordinator)");
906
+ });
907
+
908
+ test("includes ISO timestamp", () => {
909
+ const beacon = buildCoordinatorBeacon();
910
+ expect(beacon).toMatch(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
911
+ });
912
+
913
+ test("includes depth and parent info", () => {
914
+ const beacon = buildCoordinatorBeacon();
915
+ expect(beacon).toContain("Depth: 0 | Parent: none");
916
+ });
917
+
918
+ test("includes persistent orchestrator role", () => {
919
+ const beacon = buildCoordinatorBeacon();
920
+ expect(beacon).toContain("Role: persistent orchestrator");
921
+ });
922
+
923
+ test("includes startup instructions", () => {
924
+ const beacon = buildCoordinatorBeacon();
925
+ expect(beacon).toContain("mulch prime");
926
+ expect(beacon).toContain("overstory mail check --agent coordinator");
927
+ expect(beacon).toContain("bd ready");
928
+ expect(beacon).toContain("overstory group status");
929
+ });
930
+
931
+ test("defaults to bd ready when no cliName provided", () => {
932
+ const beacon = buildCoordinatorBeacon();
933
+ expect(beacon).toContain("bd ready");
934
+ });
935
+
936
+ test("uses sd ready when cliName is sd", () => {
937
+ const beacon = buildCoordinatorBeacon("sd");
938
+ expect(beacon).toContain("sd ready");
939
+ expect(beacon).not.toContain("bd ready");
940
+ });
941
+
942
+ test("includes hierarchy enforcement instruction", () => {
943
+ const beacon = buildCoordinatorBeacon();
944
+ expect(beacon).toContain("ONLY spawn leads");
945
+ expect(beacon).toContain("NEVER spawn non-lead agents directly");
946
+ });
947
+
948
+ test("includes delegation instruction", () => {
949
+ const beacon = buildCoordinatorBeacon();
950
+ expect(beacon).toContain("DELEGATION");
951
+ expect(beacon).toContain("spawn a lead who will spawn scouts");
952
+ });
953
+
954
+ test("parts are joined with em-dash separator", () => {
955
+ const beacon = buildCoordinatorBeacon();
956
+ // Should have exactly 4 " — " separators (5 parts)
957
+ const dashes = beacon.split(" — ");
958
+ expect(dashes).toHaveLength(5);
959
+ });
960
+ });
961
+
962
+ describe("resolveAttach", () => {
963
+ test("--attach flag forces attach regardless of TTY", () => {
964
+ expect(resolveAttach(["--attach"], false)).toBe(true);
965
+ expect(resolveAttach(["--attach"], true)).toBe(true);
966
+ });
967
+
968
+ test("--no-attach flag forces no attach regardless of TTY", () => {
969
+ expect(resolveAttach(["--no-attach"], false)).toBe(false);
970
+ expect(resolveAttach(["--no-attach"], true)).toBe(false);
971
+ });
972
+
973
+ test("--attach takes precedence when both flags are present", () => {
974
+ expect(resolveAttach(["--attach", "--no-attach"], false)).toBe(true);
975
+ expect(resolveAttach(["--attach", "--no-attach"], true)).toBe(true);
976
+ });
977
+
978
+ test("defaults to TTY state when no flag is set", () => {
979
+ expect(resolveAttach([], true)).toBe(true);
980
+ expect(resolveAttach([], false)).toBe(false);
981
+ });
982
+
983
+ test("works with other flags present", () => {
984
+ expect(resolveAttach(["--json", "--attach"], false)).toBe(true);
985
+ expect(resolveAttach(["--json", "--no-attach"], true)).toBe(false);
986
+ expect(resolveAttach(["--json"], true)).toBe(true);
987
+ });
988
+ });
989
+
990
+ describe("watchdog integration", () => {
991
+ describe("startCoordinator with --watchdog", () => {
992
+ test("calls watchdog.start() when --watchdog flag is present", async () => {
993
+ const { deps, watchdogCalls } = makeDeps({}, { startSuccess: true });
994
+ const originalSleep = Bun.sleep;
995
+ Bun.sleep = (() => Promise.resolve()) as typeof Bun.sleep;
996
+
997
+ try {
998
+ await captureStdout(() => coordinatorCommand(["start", "--watchdog", "--json"], deps));
999
+ } finally {
1000
+ Bun.sleep = originalSleep;
1001
+ }
1002
+
1003
+ expect(watchdogCalls?.start).toBe(1);
1004
+ });
1005
+
1006
+ test("does NOT call watchdog.start() when --watchdog flag is absent", async () => {
1007
+ const { deps, watchdogCalls } = makeDeps({}, { startSuccess: true });
1008
+ const originalSleep = Bun.sleep;
1009
+ Bun.sleep = (() => Promise.resolve()) as typeof Bun.sleep;
1010
+
1011
+ try {
1012
+ await captureStdout(() => coordinatorCommand(["start", "--json"], deps));
1013
+ } finally {
1014
+ Bun.sleep = originalSleep;
1015
+ }
1016
+
1017
+ expect(watchdogCalls?.start).toBe(0);
1018
+ });
1019
+
1020
+ test("--json output includes watchdog field when --watchdog is present and succeeds", async () => {
1021
+ const { deps } = makeDeps({}, { startSuccess: true });
1022
+ const originalSleep = Bun.sleep;
1023
+ Bun.sleep = (() => Promise.resolve()) as typeof Bun.sleep;
1024
+
1025
+ let output: string;
1026
+ try {
1027
+ output = await captureStdout(() =>
1028
+ coordinatorCommand(["start", "--watchdog", "--json"], deps),
1029
+ );
1030
+ } finally {
1031
+ Bun.sleep = originalSleep;
1032
+ }
1033
+
1034
+ const parsed = JSON.parse(output) as Record<string, unknown>;
1035
+ expect(parsed.watchdog).toBe(true);
1036
+ });
1037
+
1038
+ test("--json output includes watchdog:false when --watchdog is present but start fails", async () => {
1039
+ const { deps } = makeDeps({}, { startSuccess: false });
1040
+ const originalSleep = Bun.sleep;
1041
+ Bun.sleep = (() => Promise.resolve()) as typeof Bun.sleep;
1042
+
1043
+ let output: string;
1044
+ try {
1045
+ output = await captureStdout(() =>
1046
+ coordinatorCommand(["start", "--watchdog", "--json"], deps),
1047
+ );
1048
+ } finally {
1049
+ Bun.sleep = originalSleep;
1050
+ }
1051
+
1052
+ const parsed = JSON.parse(output) as Record<string, unknown>;
1053
+ expect(parsed.watchdog).toBe(false);
1054
+ });
1055
+
1056
+ test("--json output includes watchdog:false when --watchdog is absent", async () => {
1057
+ const { deps } = makeDeps({}, { startSuccess: true });
1058
+ const originalSleep = Bun.sleep;
1059
+ Bun.sleep = (() => Promise.resolve()) as typeof Bun.sleep;
1060
+
1061
+ let output: string;
1062
+ try {
1063
+ output = await captureStdout(() => coordinatorCommand(["start", "--json"], deps));
1064
+ } finally {
1065
+ Bun.sleep = originalSleep;
1066
+ }
1067
+
1068
+ const parsed = JSON.parse(output) as Record<string, unknown>;
1069
+ expect(parsed.watchdog).toBe(false);
1070
+ });
1071
+
1072
+ test("text output includes watchdog PID when --watchdog succeeds", async () => {
1073
+ const { deps } = makeDeps({}, { startSuccess: true });
1074
+ const originalSleep = Bun.sleep;
1075
+ Bun.sleep = (() => Promise.resolve()) as typeof Bun.sleep;
1076
+
1077
+ let output: string;
1078
+ try {
1079
+ output = await captureStdout(() =>
1080
+ coordinatorCommand(["start", "--watchdog", "--no-attach"], deps),
1081
+ );
1082
+ } finally {
1083
+ Bun.sleep = originalSleep;
1084
+ }
1085
+
1086
+ expect(output).toContain("Watchdog: started (PID 88888)");
1087
+ });
1088
+ });
1089
+
1090
+ describe("stopCoordinator watchdog cleanup", () => {
1091
+ test("always calls watchdog.stop() when stopping coordinator", async () => {
1092
+ const session = makeCoordinatorSession({ state: "working" });
1093
+ saveSessionsToDb([session]);
1094
+ const { deps, watchdogCalls } = makeDeps(
1095
+ { "overstory-test-project-coordinator": true },
1096
+ { stopSuccess: true },
1097
+ );
1098
+
1099
+ await captureStdout(() => coordinatorCommand(["stop"], deps));
1100
+
1101
+ expect(watchdogCalls?.stop).toBe(1);
1102
+ });
1103
+
1104
+ test("--json output includes watchdogStopped:true when watchdog was running", async () => {
1105
+ const session = makeCoordinatorSession({ state: "working" });
1106
+ saveSessionsToDb([session]);
1107
+ const { deps } = makeDeps(
1108
+ { "overstory-test-project-coordinator": true },
1109
+ { stopSuccess: true },
1110
+ );
1111
+
1112
+ const output = await captureStdout(() => coordinatorCommand(["stop", "--json"], deps));
1113
+ const parsed = JSON.parse(output) as Record<string, unknown>;
1114
+ expect(parsed.watchdogStopped).toBe(true);
1115
+ });
1116
+
1117
+ test("--json output includes watchdogStopped:false when no watchdog was running", async () => {
1118
+ const session = makeCoordinatorSession({ state: "working" });
1119
+ saveSessionsToDb([session]);
1120
+ const { deps } = makeDeps(
1121
+ { "overstory-test-project-coordinator": true },
1122
+ { stopSuccess: false },
1123
+ );
1124
+
1125
+ const output = await captureStdout(() => coordinatorCommand(["stop", "--json"], deps));
1126
+ const parsed = JSON.parse(output) as Record<string, unknown>;
1127
+ expect(parsed.watchdogStopped).toBe(false);
1128
+ });
1129
+
1130
+ test("text output shows 'Watchdog stopped' when watchdog was running", async () => {
1131
+ const session = makeCoordinatorSession({ state: "working" });
1132
+ saveSessionsToDb([session]);
1133
+ const { deps } = makeDeps(
1134
+ { "overstory-test-project-coordinator": true },
1135
+ { stopSuccess: true },
1136
+ );
1137
+
1138
+ const output = await captureStdout(() => coordinatorCommand(["stop"], deps));
1139
+ expect(output).toContain("Watchdog stopped");
1140
+ });
1141
+
1142
+ test("text output shows 'No watchdog running' when no watchdog was running", async () => {
1143
+ const session = makeCoordinatorSession({ state: "working" });
1144
+ saveSessionsToDb([session]);
1145
+ const { deps } = makeDeps(
1146
+ { "overstory-test-project-coordinator": true },
1147
+ { stopSuccess: false },
1148
+ );
1149
+
1150
+ const output = await captureStdout(() => coordinatorCommand(["stop"], deps));
1151
+ expect(output).toContain("No watchdog running");
1152
+ });
1153
+ });
1154
+
1155
+ describe("statusCoordinator watchdog state", () => {
1156
+ test("includes watchdogRunning in JSON output when coordinator is running", async () => {
1157
+ const session = makeCoordinatorSession({ state: "working" });
1158
+ saveSessionsToDb([session]);
1159
+ const { deps } = makeDeps({ "overstory-test-project-coordinator": true }, { running: true });
1160
+
1161
+ const output = await captureStdout(() => coordinatorCommand(["status", "--json"], deps));
1162
+ const parsed = JSON.parse(output) as Record<string, unknown>;
1163
+ expect(parsed.watchdogRunning).toBe(true);
1164
+ });
1165
+
1166
+ test("includes watchdogRunning:false in JSON output when watchdog is not running", async () => {
1167
+ const session = makeCoordinatorSession({ state: "working" });
1168
+ saveSessionsToDb([session]);
1169
+ const { deps } = makeDeps({ "overstory-test-project-coordinator": true }, { running: false });
1170
+
1171
+ const output = await captureStdout(() => coordinatorCommand(["status", "--json"], deps));
1172
+ const parsed = JSON.parse(output) as Record<string, unknown>;
1173
+ expect(parsed.watchdogRunning).toBe(false);
1174
+ });
1175
+
1176
+ test("text output shows watchdog status when coordinator is running", async () => {
1177
+ const session = makeCoordinatorSession({ state: "working" });
1178
+ saveSessionsToDb([session]);
1179
+ const { deps } = makeDeps({ "overstory-test-project-coordinator": true }, { running: true });
1180
+
1181
+ const output = await captureStdout(() => coordinatorCommand(["status"], deps));
1182
+ expect(output).toContain("Watchdog: running");
1183
+ });
1184
+
1185
+ test("text output shows 'not running' when watchdog is not running", async () => {
1186
+ const session = makeCoordinatorSession({ state: "working" });
1187
+ saveSessionsToDb([session]);
1188
+ const { deps } = makeDeps({ "overstory-test-project-coordinator": true }, { running: false });
1189
+
1190
+ const output = await captureStdout(() => coordinatorCommand(["status"], deps));
1191
+ expect(output).toContain("Watchdog: not running");
1192
+ });
1193
+
1194
+ test("includes watchdogRunning in JSON output when coordinator is not running", async () => {
1195
+ const { deps } = makeDeps({}, { running: true });
1196
+
1197
+ const output = await captureStdout(() => coordinatorCommand(["status", "--json"], deps));
1198
+ const parsed = JSON.parse(output) as Record<string, unknown>;
1199
+ expect(parsed.running).toBe(false);
1200
+ expect(parsed.watchdogRunning).toBe(true);
1201
+ });
1202
+ });
1203
+
1204
+ describe("COORDINATOR_HELP", () => {
1205
+ test("help text includes --watchdog flag", async () => {
1206
+ const output = await captureStdout(() => coordinatorCommand(["--help"]));
1207
+ expect(output).toContain("--watchdog");
1208
+ expect(output).toContain("Auto-start watchdog daemon with coordinator");
1209
+ });
1210
+ });
1211
+ });
1212
+
1213
+ describe("monitor integration", () => {
1214
+ describe("startCoordinator with --monitor", () => {
1215
+ test("calls monitor.start() when --monitor flag is present", async () => {
1216
+ const { deps, monitorCalls } = makeDeps({}, undefined, { startSuccess: true });
1217
+ const originalSleep = Bun.sleep;
1218
+ Bun.sleep = (() => Promise.resolve()) as typeof Bun.sleep;
1219
+
1220
+ try {
1221
+ await captureStdout(() => coordinatorCommand(["start", "--monitor", "--json"], deps));
1222
+ } finally {
1223
+ Bun.sleep = originalSleep;
1224
+ }
1225
+
1226
+ expect(monitorCalls?.start).toBe(1);
1227
+ });
1228
+
1229
+ test("does NOT call monitor.start() when --monitor flag is absent", async () => {
1230
+ const { deps, monitorCalls } = makeDeps({}, undefined, { startSuccess: true });
1231
+ const originalSleep = Bun.sleep;
1232
+ Bun.sleep = (() => Promise.resolve()) as typeof Bun.sleep;
1233
+
1234
+ try {
1235
+ await captureStdout(() => coordinatorCommand(["start", "--json"], deps));
1236
+ } finally {
1237
+ Bun.sleep = originalSleep;
1238
+ }
1239
+
1240
+ expect(monitorCalls?.start).toBe(0);
1241
+ });
1242
+
1243
+ test("--json output includes monitor field when --monitor is present and succeeds", async () => {
1244
+ const { deps } = makeDeps({}, undefined, { startSuccess: true });
1245
+ const originalSleep = Bun.sleep;
1246
+ Bun.sleep = (() => Promise.resolve()) as typeof Bun.sleep;
1247
+
1248
+ let output: string;
1249
+ try {
1250
+ output = await captureStdout(() =>
1251
+ coordinatorCommand(["start", "--monitor", "--json"], deps),
1252
+ );
1253
+ } finally {
1254
+ Bun.sleep = originalSleep;
1255
+ }
1256
+
1257
+ const parsed = JSON.parse(output) as Record<string, unknown>;
1258
+ expect(parsed.monitor).toBe(true);
1259
+ });
1260
+
1261
+ test("--json output includes monitor:false when --monitor is present but start fails", async () => {
1262
+ const { deps } = makeDeps({}, undefined, { startSuccess: false });
1263
+ const originalSleep = Bun.sleep;
1264
+ Bun.sleep = (() => Promise.resolve()) as typeof Bun.sleep;
1265
+
1266
+ let output: string;
1267
+ try {
1268
+ output = await captureStdout(() =>
1269
+ coordinatorCommand(["start", "--monitor", "--json"], deps),
1270
+ );
1271
+ } finally {
1272
+ Bun.sleep = originalSleep;
1273
+ }
1274
+
1275
+ const parsed = JSON.parse(output) as Record<string, unknown>;
1276
+ expect(parsed.monitor).toBe(false);
1277
+ });
1278
+
1279
+ test("--json output includes monitor:false when --monitor is absent", async () => {
1280
+ const { deps } = makeDeps({}, undefined, { startSuccess: true });
1281
+ const originalSleep = Bun.sleep;
1282
+ Bun.sleep = (() => Promise.resolve()) as typeof Bun.sleep;
1283
+
1284
+ let output: string;
1285
+ try {
1286
+ output = await captureStdout(() => coordinatorCommand(["start", "--json"], deps));
1287
+ } finally {
1288
+ Bun.sleep = originalSleep;
1289
+ }
1290
+
1291
+ const parsed = JSON.parse(output) as Record<string, unknown>;
1292
+ expect(parsed.monitor).toBe(false);
1293
+ });
1294
+
1295
+ test("text output includes monitor PID when --monitor succeeds", async () => {
1296
+ const { deps } = makeDeps({}, undefined, { startSuccess: true });
1297
+ const originalSleep = Bun.sleep;
1298
+ Bun.sleep = (() => Promise.resolve()) as typeof Bun.sleep;
1299
+
1300
+ let output: string;
1301
+ try {
1302
+ output = await captureStdout(() =>
1303
+ coordinatorCommand(["start", "--monitor", "--no-attach"], deps),
1304
+ );
1305
+ } finally {
1306
+ Bun.sleep = originalSleep;
1307
+ }
1308
+
1309
+ expect(output).toContain("Monitor: started (PID 77777)");
1310
+ });
1311
+
1312
+ test("does NOT call monitor.start() when tier2Enabled is false", async () => {
1313
+ // Override config with tier2Enabled: false
1314
+ await Bun.write(
1315
+ join(overstoryDir, "config.yaml"),
1316
+ [
1317
+ "project:",
1318
+ " name: test-project",
1319
+ ` root: ${tempDir}`,
1320
+ " canonicalBranch: main",
1321
+ "watchdog:",
1322
+ " tier2Enabled: false",
1323
+ ].join("\n"),
1324
+ );
1325
+ const { deps, monitorCalls } = makeDeps({}, undefined, { startSuccess: true });
1326
+ const originalSleep = Bun.sleep;
1327
+ Bun.sleep = (() => Promise.resolve()) as typeof Bun.sleep;
1328
+
1329
+ try {
1330
+ await captureStdout(() => coordinatorCommand(["start", "--monitor", "--json"], deps));
1331
+ } finally {
1332
+ Bun.sleep = originalSleep;
1333
+ }
1334
+
1335
+ expect(monitorCalls?.start).toBe(0);
1336
+ });
1337
+
1338
+ test("text output shows skipped message when tier2Enabled is false", async () => {
1339
+ // Override config with tier2Enabled: false
1340
+ await Bun.write(
1341
+ join(overstoryDir, "config.yaml"),
1342
+ [
1343
+ "project:",
1344
+ " name: test-project",
1345
+ ` root: ${tempDir}`,
1346
+ " canonicalBranch: main",
1347
+ "watchdog:",
1348
+ " tier2Enabled: false",
1349
+ ].join("\n"),
1350
+ );
1351
+ const { deps } = makeDeps({}, undefined, { startSuccess: true });
1352
+ const originalSleep = Bun.sleep;
1353
+ Bun.sleep = (() => Promise.resolve()) as typeof Bun.sleep;
1354
+
1355
+ let stderrOutput = "";
1356
+ const origStderrWrite = process.stderr.write.bind(process.stderr);
1357
+ process.stderr.write = (chunk: string | Uint8Array) => {
1358
+ stderrOutput += typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk);
1359
+ return true;
1360
+ };
1361
+
1362
+ try {
1363
+ await captureStdout(() => coordinatorCommand(["start", "--monitor", "--no-attach"], deps));
1364
+ } finally {
1365
+ Bun.sleep = originalSleep;
1366
+ process.stderr.write = origStderrWrite;
1367
+ }
1368
+
1369
+ expect(stderrOutput).toContain("skipped");
1370
+ });
1371
+ });
1372
+
1373
+ describe("stopCoordinator monitor cleanup", () => {
1374
+ test("always calls monitor.stop() when stopping coordinator", async () => {
1375
+ const session = makeCoordinatorSession({ state: "working" });
1376
+ saveSessionsToDb([session]);
1377
+ const { deps, monitorCalls } = makeDeps(
1378
+ { "overstory-test-project-coordinator": true },
1379
+ undefined,
1380
+ { stopSuccess: true },
1381
+ );
1382
+
1383
+ await captureStdout(() => coordinatorCommand(["stop"], deps));
1384
+
1385
+ expect(monitorCalls?.stop).toBe(1);
1386
+ });
1387
+
1388
+ test("--json output includes monitorStopped:true when monitor was running", async () => {
1389
+ const session = makeCoordinatorSession({ state: "working" });
1390
+ saveSessionsToDb([session]);
1391
+ const { deps } = makeDeps({ "overstory-test-project-coordinator": true }, undefined, {
1392
+ stopSuccess: true,
1393
+ });
1394
+
1395
+ const output = await captureStdout(() => coordinatorCommand(["stop", "--json"], deps));
1396
+ const parsed = JSON.parse(output) as Record<string, unknown>;
1397
+ expect(parsed.monitorStopped).toBe(true);
1398
+ });
1399
+
1400
+ test("--json output includes monitorStopped:false when no monitor was running", async () => {
1401
+ const session = makeCoordinatorSession({ state: "working" });
1402
+ saveSessionsToDb([session]);
1403
+ const { deps } = makeDeps({ "overstory-test-project-coordinator": true }, undefined, {
1404
+ stopSuccess: false,
1405
+ });
1406
+
1407
+ const output = await captureStdout(() => coordinatorCommand(["stop", "--json"], deps));
1408
+ const parsed = JSON.parse(output) as Record<string, unknown>;
1409
+ expect(parsed.monitorStopped).toBe(false);
1410
+ });
1411
+
1412
+ test("text output shows 'Monitor stopped' when monitor was running", async () => {
1413
+ const session = makeCoordinatorSession({ state: "working" });
1414
+ saveSessionsToDb([session]);
1415
+ const { deps } = makeDeps({ "overstory-test-project-coordinator": true }, undefined, {
1416
+ stopSuccess: true,
1417
+ });
1418
+
1419
+ const output = await captureStdout(() => coordinatorCommand(["stop"], deps));
1420
+ expect(output).toContain("Monitor stopped");
1421
+ });
1422
+
1423
+ test("text output shows 'No monitor running' when no monitor was running", async () => {
1424
+ const session = makeCoordinatorSession({ state: "working" });
1425
+ saveSessionsToDb([session]);
1426
+ const { deps } = makeDeps({ "overstory-test-project-coordinator": true }, undefined, {
1427
+ stopSuccess: false,
1428
+ });
1429
+
1430
+ const output = await captureStdout(() => coordinatorCommand(["stop"], deps));
1431
+ expect(output).toContain("No monitor running");
1432
+ });
1433
+ });
1434
+
1435
+ describe("statusCoordinator monitor state", () => {
1436
+ test("includes monitorRunning in JSON output when coordinator is running", async () => {
1437
+ const session = makeCoordinatorSession({ state: "working" });
1438
+ saveSessionsToDb([session]);
1439
+ const { deps } = makeDeps({ "overstory-test-project-coordinator": true }, undefined, {
1440
+ running: true,
1441
+ });
1442
+
1443
+ const output = await captureStdout(() => coordinatorCommand(["status", "--json"], deps));
1444
+ const parsed = JSON.parse(output) as Record<string, unknown>;
1445
+ expect(parsed.monitorRunning).toBe(true);
1446
+ });
1447
+
1448
+ test("includes monitorRunning:false in JSON output when monitor is not running", async () => {
1449
+ const session = makeCoordinatorSession({ state: "working" });
1450
+ saveSessionsToDb([session]);
1451
+ const { deps } = makeDeps({ "overstory-test-project-coordinator": true }, undefined, {
1452
+ running: false,
1453
+ });
1454
+
1455
+ const output = await captureStdout(() => coordinatorCommand(["status", "--json"], deps));
1456
+ const parsed = JSON.parse(output) as Record<string, unknown>;
1457
+ expect(parsed.monitorRunning).toBe(false);
1458
+ });
1459
+
1460
+ test("text output shows monitor status when coordinator is running", async () => {
1461
+ const session = makeCoordinatorSession({ state: "working" });
1462
+ saveSessionsToDb([session]);
1463
+ const { deps } = makeDeps({ "overstory-test-project-coordinator": true }, undefined, {
1464
+ running: true,
1465
+ });
1466
+
1467
+ const output = await captureStdout(() => coordinatorCommand(["status"], deps));
1468
+ expect(output).toContain("Monitor: running");
1469
+ });
1470
+
1471
+ test("text output shows 'not running' when monitor is not running", async () => {
1472
+ const session = makeCoordinatorSession({ state: "working" });
1473
+ saveSessionsToDb([session]);
1474
+ const { deps } = makeDeps({ "overstory-test-project-coordinator": true }, undefined, {
1475
+ running: false,
1476
+ });
1477
+
1478
+ const output = await captureStdout(() => coordinatorCommand(["status"], deps));
1479
+ expect(output).toContain("Monitor: not running");
1480
+ });
1481
+
1482
+ test("includes monitorRunning in JSON output when coordinator is not running", async () => {
1483
+ const { deps } = makeDeps({}, undefined, { running: true });
1484
+
1485
+ const output = await captureStdout(() => coordinatorCommand(["status", "--json"], deps));
1486
+ const parsed = JSON.parse(output) as Record<string, unknown>;
1487
+ expect(parsed.running).toBe(false);
1488
+ expect(parsed.monitorRunning).toBe(true);
1489
+ });
1490
+ });
1491
+
1492
+ describe("COORDINATOR_HELP", () => {
1493
+ test("help text includes --monitor flag", async () => {
1494
+ const output = await captureStdout(() => coordinatorCommand(["--help"]));
1495
+ expect(output).toContain("--monitor");
1496
+ expect(output).toContain("Auto-start monitor agent (Tier 2) with coordinator");
1497
+ });
1498
+ });
1499
+ });
1500
+
1501
+ describe("SessionStore round-trip", () => {
1502
+ test("returns empty array when no sessions exist", () => {
1503
+ const sessions = loadSessionsFromDb();
1504
+ expect(sessions).toEqual([]);
1505
+ });
1506
+
1507
+ test("save then load round-trips correctly", () => {
1508
+ const original = [makeCoordinatorSession()];
1509
+ saveSessionsToDb(original);
1510
+ const loaded = loadSessionsFromDb();
1511
+
1512
+ expect(loaded).toHaveLength(1);
1513
+ expect(loaded[0]?.agentName).toBe("coordinator");
1514
+ expect(loaded[0]?.capability).toBe("coordinator");
1515
+ });
1516
+
1517
+ test("sessions.db is created after save", () => {
1518
+ saveSessionsToDb([makeCoordinatorSession()]);
1519
+ const dbPath = join(overstoryDir, "sessions.db");
1520
+ const exists = Bun.file(dbPath).size > 0;
1521
+ expect(exists).toBe(true);
1522
+ });
1523
+ });
1524
+
1525
+ describe("isRunningAsRoot (imported from sling)", () => {
1526
+ test("is accessible from coordinator test file", () => {
1527
+ expect(isRunningAsRoot(() => 0)).toBe(true);
1528
+ expect(isRunningAsRoot(() => 1000)).toBe(false);
1529
+ });
1530
+ });