@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,578 @@
1
+ /**
2
+ * Tests for `overstory feed` command.
3
+ *
4
+ * Uses real bun:sqlite (temp files) to test the feed command end-to-end.
5
+ * Captures process.stdout.write to verify output formatting.
6
+ *
7
+ * Real implementations used for: filesystem (temp dirs), SQLite (EventStore).
8
+ * No mocks needed -- all dependencies are cheap and local.
9
+ */
10
+
11
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
12
+ import { mkdtemp, rm } from "node:fs/promises";
13
+ import { tmpdir } from "node:os";
14
+ import { join } from "node:path";
15
+ import { ValidationError } from "../errors.ts";
16
+ import { createEventStore } from "../events/store.ts";
17
+ import type { InsertEvent } from "../types.ts";
18
+ import { feedCommand } from "./feed.ts";
19
+
20
+ /** Helper to create an InsertEvent with sensible defaults. */
21
+ function makeEvent(overrides: Partial<InsertEvent> = {}): InsertEvent {
22
+ return {
23
+ runId: "run-001",
24
+ agentName: "builder-1",
25
+ sessionId: "sess-abc",
26
+ eventType: "tool_start",
27
+ toolName: "Read",
28
+ toolArgs: '{"file": "src/index.ts"}',
29
+ toolDurationMs: null,
30
+ level: "info",
31
+ data: null,
32
+ ...overrides,
33
+ };
34
+ }
35
+
36
+ describe("feedCommand", () => {
37
+ let chunks: string[];
38
+ let originalWrite: typeof process.stdout.write;
39
+ let tempDir: string;
40
+ let originalCwd: string;
41
+
42
+ beforeEach(async () => {
43
+ // Spy on stdout
44
+ chunks = [];
45
+ originalWrite = process.stdout.write;
46
+ process.stdout.write = ((chunk: string) => {
47
+ chunks.push(chunk);
48
+ return true;
49
+ }) as typeof process.stdout.write;
50
+
51
+ // Create temp dir with .overstory/config.yaml structure
52
+ tempDir = await mkdtemp(join(tmpdir(), "feed-test-"));
53
+ const overstoryDir = join(tempDir, ".overstory");
54
+ await Bun.write(
55
+ join(overstoryDir, "config.yaml"),
56
+ `project:\n name: test\n root: ${tempDir}\n canonicalBranch: main\n`,
57
+ );
58
+
59
+ // Change to temp dir so loadConfig() works
60
+ originalCwd = process.cwd();
61
+ process.chdir(tempDir);
62
+ });
63
+
64
+ afterEach(async () => {
65
+ process.stdout.write = originalWrite;
66
+ process.chdir(originalCwd);
67
+ await rm(tempDir, { recursive: true, force: true });
68
+ });
69
+
70
+ function output(): string {
71
+ return chunks.join("");
72
+ }
73
+
74
+ // === Help flag ===
75
+
76
+ describe("help flag", () => {
77
+ test("--help shows help text", async () => {
78
+ await feedCommand(["--help"]);
79
+ const out = output();
80
+
81
+ expect(out).toContain("overstory feed");
82
+ expect(out).toContain("--follow");
83
+ expect(out).toContain("--agent");
84
+ expect(out).toContain("--run");
85
+ expect(out).toContain("--since");
86
+ expect(out).toContain("--limit");
87
+ expect(out).toContain("--interval");
88
+ expect(out).toContain("--json");
89
+ });
90
+
91
+ test("-h shows help text", async () => {
92
+ await feedCommand(["-h"]);
93
+ const out = output();
94
+
95
+ expect(out).toContain("overstory feed");
96
+ });
97
+ });
98
+
99
+ // === Argument parsing ===
100
+
101
+ describe("argument parsing", () => {
102
+ test("--limit with non-numeric value throws ValidationError", async () => {
103
+ await expect(feedCommand(["--limit", "abc"])).rejects.toThrow(ValidationError);
104
+ });
105
+
106
+ test("--limit with zero throws ValidationError", async () => {
107
+ await expect(feedCommand(["--limit", "0"])).rejects.toThrow(ValidationError);
108
+ });
109
+
110
+ test("--limit with negative value throws ValidationError", async () => {
111
+ await expect(feedCommand(["--limit", "-5"])).rejects.toThrow(ValidationError);
112
+ });
113
+
114
+ test("--interval with non-numeric value throws ValidationError", async () => {
115
+ await expect(feedCommand(["--interval", "abc"])).rejects.toThrow(ValidationError);
116
+ });
117
+
118
+ test("--interval below 200 throws ValidationError", async () => {
119
+ await expect(feedCommand(["--interval", "100"])).rejects.toThrow(ValidationError);
120
+ });
121
+
122
+ test("--since with invalid timestamp throws ValidationError", async () => {
123
+ await expect(feedCommand(["--since", "not-a-date"])).rejects.toThrow(ValidationError);
124
+ });
125
+ });
126
+
127
+ // === Missing events.db (graceful handling) ===
128
+
129
+ describe("missing events.db", () => {
130
+ test("text mode outputs friendly message when no events.db exists", async () => {
131
+ await feedCommand([]);
132
+ const out = output();
133
+
134
+ expect(out).toBe("No events data yet.\n");
135
+ });
136
+
137
+ test("JSON mode outputs empty array when no events.db exists", async () => {
138
+ await feedCommand(["--json"]);
139
+ const out = output();
140
+
141
+ expect(out).toBe("[]\n");
142
+ });
143
+ });
144
+
145
+ // === JSON output mode ===
146
+
147
+ describe("JSON output mode", () => {
148
+ test("outputs valid JSON array with events", async () => {
149
+ const dbPath = join(tempDir, ".overstory", "events.db");
150
+ const store = createEventStore(dbPath);
151
+ store.insert(makeEvent({ agentName: "builder-1", eventType: "session_start" }));
152
+ store.insert(makeEvent({ agentName: "builder-2", eventType: "tool_start" }));
153
+ store.insert(makeEvent({ agentName: "builder-1", eventType: "session_end" }));
154
+ store.close();
155
+
156
+ await feedCommand(["--json"]);
157
+ const out = output();
158
+
159
+ const parsed = JSON.parse(out.trim()) as unknown[];
160
+ expect(parsed).toHaveLength(3);
161
+ expect(Array.isArray(parsed)).toBe(true);
162
+ });
163
+
164
+ test("JSON output includes expected fields", async () => {
165
+ const dbPath = join(tempDir, ".overstory", "events.db");
166
+ const store = createEventStore(dbPath);
167
+ store.insert(
168
+ makeEvent({
169
+ agentName: "builder-1",
170
+ eventType: "tool_start",
171
+ toolName: "Bash",
172
+ level: "info",
173
+ }),
174
+ );
175
+ store.close();
176
+
177
+ await feedCommand(["--json"]);
178
+ const out = output();
179
+
180
+ const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
181
+ expect(parsed).toHaveLength(1);
182
+ const event = parsed[0];
183
+ expect(event).toBeDefined();
184
+ expect(event?.agentName).toBe("builder-1");
185
+ expect(event?.eventType).toBe("tool_start");
186
+ expect(event?.toolName).toBe("Bash");
187
+ expect(event?.level).toBe("info");
188
+ expect(event?.createdAt).toBeTruthy();
189
+ });
190
+
191
+ test("JSON output returns empty array when no events match since filter", async () => {
192
+ const dbPath = join(tempDir, ".overstory", "events.db");
193
+ const store = createEventStore(dbPath);
194
+ store.insert(makeEvent({ agentName: "builder-1" }));
195
+ store.close();
196
+
197
+ // Query from future date
198
+ await feedCommand(["--json", "--since", "2099-01-01T00:00:00Z"]);
199
+ const out = output();
200
+
201
+ const parsed = JSON.parse(out.trim()) as unknown[];
202
+ expect(parsed).toEqual([]);
203
+ });
204
+ });
205
+
206
+ // === Feed output format ===
207
+
208
+ describe("feed output", () => {
209
+ test("shows events from multiple agents", async () => {
210
+ const dbPath = join(tempDir, ".overstory", "events.db");
211
+ const store = createEventStore(dbPath);
212
+ store.insert(makeEvent({ agentName: "builder-1" }));
213
+ store.insert(makeEvent({ agentName: "scout-1" }));
214
+ store.insert(makeEvent({ agentName: "builder-2" }));
215
+ store.close();
216
+
217
+ await feedCommand([]);
218
+ const out = output();
219
+
220
+ expect(out).toContain("builder-1");
221
+ expect(out).toContain("scout-1");
222
+ expect(out).toContain("builder-2");
223
+ });
224
+
225
+ test("compact event labels are shown", async () => {
226
+ const dbPath = join(tempDir, ".overstory", "events.db");
227
+ const store = createEventStore(dbPath);
228
+ store.insert(makeEvent({ agentName: "builder-1", eventType: "session_start" }));
229
+ store.insert(makeEvent({ agentName: "builder-1", eventType: "tool_start" }));
230
+ store.insert(makeEvent({ agentName: "builder-1", eventType: "mail_sent" }));
231
+ store.insert(makeEvent({ agentName: "builder-1", eventType: "error", level: "error" }));
232
+ store.close();
233
+
234
+ await feedCommand([]);
235
+ const out = output();
236
+
237
+ expect(out).toContain("SESS+");
238
+ expect(out).toContain("TOOL+");
239
+ expect(out).toContain("MAIL>");
240
+ expect(out).toContain("ERROR");
241
+ });
242
+
243
+ test("tool name is shown in detail", async () => {
244
+ const dbPath = join(tempDir, ".overstory", "events.db");
245
+ const store = createEventStore(dbPath);
246
+ store.insert(
247
+ makeEvent({
248
+ agentName: "builder-1",
249
+ eventType: "tool_start",
250
+ toolName: "Bash",
251
+ }),
252
+ );
253
+ store.close();
254
+
255
+ await feedCommand([]);
256
+ const out = output();
257
+
258
+ expect(out).toContain("tool=Bash");
259
+ });
260
+
261
+ test("tool duration is shown in detail", async () => {
262
+ const dbPath = join(tempDir, ".overstory", "events.db");
263
+ const store = createEventStore(dbPath);
264
+ store.insert(
265
+ makeEvent({
266
+ agentName: "builder-1",
267
+ eventType: "tool_start",
268
+ toolName: "Read",
269
+ toolDurationMs: 42,
270
+ }),
271
+ );
272
+ store.close();
273
+
274
+ await feedCommand([]);
275
+ const out = output();
276
+
277
+ expect(out).toContain("42ms");
278
+ });
279
+
280
+ test("absolute time format is shown (HH:MM:SS)", async () => {
281
+ const dbPath = join(tempDir, ".overstory", "events.db");
282
+ const store = createEventStore(dbPath);
283
+ store.insert(makeEvent({ agentName: "builder-1" }));
284
+ store.close();
285
+
286
+ await feedCommand([]);
287
+ const out = output();
288
+
289
+ // Should show HH:MM:SS format
290
+ expect(out).toMatch(/\d{2}:\d{2}:\d{2}/);
291
+ });
292
+
293
+ test("no events shows 'No events found' message", async () => {
294
+ const dbPath = join(tempDir, ".overstory", "events.db");
295
+ const store = createEventStore(dbPath);
296
+ // Create DB but no events
297
+ store.close();
298
+
299
+ await feedCommand([]);
300
+ const out = output();
301
+
302
+ expect(out).toContain("No events found");
303
+ });
304
+ });
305
+
306
+ // === --agent filter ===
307
+
308
+ describe("--agent filter", () => {
309
+ test("filters to single agent", async () => {
310
+ const dbPath = join(tempDir, ".overstory", "events.db");
311
+ const store = createEventStore(dbPath);
312
+ store.insert(makeEvent({ agentName: "builder-1" }));
313
+ store.insert(makeEvent({ agentName: "scout-1" }));
314
+ store.insert(makeEvent({ agentName: "builder-2" }));
315
+ store.close();
316
+
317
+ await feedCommand(["--agent", "builder-1", "--json"]);
318
+ const out = output();
319
+
320
+ const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
321
+ expect(parsed).toHaveLength(1);
322
+ expect(parsed[0]?.agentName).toBe("builder-1");
323
+ });
324
+
325
+ test("filters to multiple agents", async () => {
326
+ const dbPath = join(tempDir, ".overstory", "events.db");
327
+ const store = createEventStore(dbPath);
328
+ store.insert(makeEvent({ agentName: "builder-1" }));
329
+ store.insert(makeEvent({ agentName: "scout-1" }));
330
+ store.insert(makeEvent({ agentName: "builder-2" }));
331
+ store.close();
332
+
333
+ await feedCommand(["--agent", "builder-1", "--agent", "scout-1", "--json"]);
334
+ const out = output();
335
+
336
+ const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
337
+ expect(parsed).toHaveLength(2);
338
+ const agents = parsed.map((e) => e.agentName);
339
+ expect(agents).toContain("builder-1");
340
+ expect(agents).toContain("scout-1");
341
+ expect(agents).not.toContain("builder-2");
342
+ });
343
+ });
344
+
345
+ // === --run filter ===
346
+
347
+ describe("--run filter", () => {
348
+ test("filters events by run ID", async () => {
349
+ const dbPath = join(tempDir, ".overstory", "events.db");
350
+ const store = createEventStore(dbPath);
351
+ store.insert(makeEvent({ runId: "run-001", agentName: "builder-1" }));
352
+ store.insert(makeEvent({ runId: "run-002", agentName: "builder-2" }));
353
+ store.insert(makeEvent({ runId: "run-001", agentName: "scout-1" }));
354
+ store.close();
355
+
356
+ await feedCommand(["--run", "run-001", "--json"]);
357
+ const out = output();
358
+
359
+ const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
360
+ expect(parsed).toHaveLength(2);
361
+ for (const event of parsed) {
362
+ expect(event.runId).toBe("run-001");
363
+ }
364
+ });
365
+ });
366
+
367
+ // === --limit flag ===
368
+
369
+ describe("--limit flag", () => {
370
+ test("limits the number of events returned", async () => {
371
+ const dbPath = join(tempDir, ".overstory", "events.db");
372
+ const store = createEventStore(dbPath);
373
+ for (let i = 0; i < 100; i++) {
374
+ store.insert(makeEvent({ agentName: "builder-1" }));
375
+ }
376
+ store.close();
377
+
378
+ await feedCommand(["--json", "--limit", "10"]);
379
+ const out = output();
380
+
381
+ const parsed = JSON.parse(out.trim()) as unknown[];
382
+ expect(parsed).toHaveLength(10);
383
+ });
384
+
385
+ test("default limit is 50", async () => {
386
+ const dbPath = join(tempDir, ".overstory", "events.db");
387
+ const store = createEventStore(dbPath);
388
+ for (let i = 0; i < 100; i++) {
389
+ store.insert(makeEvent({ agentName: "builder-1" }));
390
+ }
391
+ store.close();
392
+
393
+ await feedCommand(["--json"]);
394
+ const out = output();
395
+
396
+ const parsed = JSON.parse(out.trim()) as unknown[];
397
+ expect(parsed).toHaveLength(50);
398
+ });
399
+ });
400
+
401
+ // === --since flag ===
402
+
403
+ describe("--since flag", () => {
404
+ test("--since filters events after a timestamp", async () => {
405
+ const dbPath = join(tempDir, ".overstory", "events.db");
406
+ const store = createEventStore(dbPath);
407
+ store.insert(makeEvent({ agentName: "builder-1" }));
408
+ store.close();
409
+
410
+ // A future timestamp should return no events
411
+ await feedCommand(["--json", "--since", "2099-01-01T00:00:00Z"]);
412
+ const out = output();
413
+
414
+ const parsed = JSON.parse(out.trim()) as unknown[];
415
+ expect(parsed).toEqual([]);
416
+ });
417
+
418
+ test("--since with past timestamp returns all events", async () => {
419
+ const dbPath = join(tempDir, ".overstory", "events.db");
420
+ const store = createEventStore(dbPath);
421
+ store.insert(makeEvent({ agentName: "builder-1" }));
422
+ store.insert(makeEvent({ agentName: "builder-2" }));
423
+ store.close();
424
+
425
+ await feedCommand(["--json", "--since", "2020-01-01T00:00:00Z"]);
426
+ const out = output();
427
+
428
+ const parsed = JSON.parse(out.trim()) as unknown[];
429
+ expect(parsed).toHaveLength(2);
430
+ });
431
+
432
+ test("default since is 5 minutes ago", async () => {
433
+ const dbPath = join(tempDir, ".overstory", "events.db");
434
+ const store = createEventStore(dbPath);
435
+ // Insert event with current timestamp
436
+ store.insert(makeEvent({ agentName: "builder-1" }));
437
+ store.close();
438
+
439
+ // Without --since, should get recent events
440
+ await feedCommand(["--json"]);
441
+ const out = output();
442
+
443
+ const parsed = JSON.parse(out.trim()) as unknown[];
444
+ expect(parsed).toHaveLength(1);
445
+ });
446
+ });
447
+
448
+ // === Event types coverage ===
449
+
450
+ describe("event types coverage", () => {
451
+ test("all event types have compact labels", async () => {
452
+ const dbPath = join(tempDir, ".overstory", "events.db");
453
+ const store = createEventStore(dbPath);
454
+ const eventTypes = [
455
+ "tool_start",
456
+ "tool_end",
457
+ "session_start",
458
+ "session_end",
459
+ "mail_sent",
460
+ "mail_received",
461
+ "spawn",
462
+ "error",
463
+ "custom",
464
+ ] as const;
465
+ for (const eventType of eventTypes) {
466
+ store.insert(
467
+ makeEvent({
468
+ agentName: "builder-1",
469
+ eventType,
470
+ level: eventType === "error" ? "error" : "info",
471
+ }),
472
+ );
473
+ }
474
+ store.close();
475
+
476
+ await feedCommand([]);
477
+ const out = output();
478
+
479
+ // Verify all compact labels appear
480
+ expect(out).toContain("TOOL+");
481
+ expect(out).toContain("TOOL-");
482
+ expect(out).toContain("SESS+");
483
+ expect(out).toContain("SESS-");
484
+ expect(out).toContain("MAIL>");
485
+ expect(out).toContain("MAIL<");
486
+ expect(out).toContain("SPAWN");
487
+ expect(out).toContain("ERROR");
488
+ expect(out).toContain("CUSTM");
489
+ });
490
+ });
491
+
492
+ // === Edge cases ===
493
+
494
+ describe("edge cases", () => {
495
+ test("events are ordered chronologically", async () => {
496
+ const dbPath = join(tempDir, ".overstory", "events.db");
497
+ const store = createEventStore(dbPath);
498
+ store.insert(makeEvent({ agentName: "builder-1", eventType: "session_start" }));
499
+ store.insert(makeEvent({ agentName: "scout-1", eventType: "tool_start" }));
500
+ store.insert(makeEvent({ agentName: "builder-1", eventType: "session_end" }));
501
+ store.close();
502
+
503
+ await feedCommand(["--json"]);
504
+ const out = output();
505
+
506
+ const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
507
+ expect(parsed).toHaveLength(3);
508
+ expect(parsed[0]?.eventType).toBe("session_start");
509
+ expect(parsed[1]?.eventType).toBe("tool_start");
510
+ expect(parsed[2]?.eventType).toBe("session_end");
511
+ });
512
+
513
+ test("handles event with all null optional fields", async () => {
514
+ const dbPath = join(tempDir, ".overstory", "events.db");
515
+ const store = createEventStore(dbPath);
516
+ store.insert(
517
+ makeEvent({
518
+ agentName: "builder-1",
519
+ eventType: "session_start",
520
+ runId: null,
521
+ sessionId: null,
522
+ toolName: null,
523
+ toolArgs: null,
524
+ toolDurationMs: null,
525
+ data: null,
526
+ }),
527
+ );
528
+ store.close();
529
+
530
+ // Should not throw
531
+ await feedCommand([]);
532
+ const out = output();
533
+
534
+ expect(out).toContain("SESS+");
535
+ expect(out).toContain("builder-1");
536
+ });
537
+
538
+ test("long data values are truncated in output", async () => {
539
+ const dbPath = join(tempDir, ".overstory", "events.db");
540
+ const store = createEventStore(dbPath);
541
+ const longValue = "x".repeat(200);
542
+ store.insert(
543
+ makeEvent({
544
+ agentName: "builder-1",
545
+ eventType: "custom",
546
+ toolName: null,
547
+ data: JSON.stringify({ message: longValue }),
548
+ }),
549
+ );
550
+ store.close();
551
+
552
+ await feedCommand([]);
553
+ const out = output();
554
+
555
+ // The full 200-char value should not appear
556
+ expect(out).not.toContain(longValue);
557
+ // But a truncated version with "..." should
558
+ expect(out).toContain("...");
559
+ });
560
+
561
+ test("agent color assignment is stable", async () => {
562
+ const dbPath = join(tempDir, ".overstory", "events.db");
563
+ const store = createEventStore(dbPath);
564
+ store.insert(makeEvent({ agentName: "builder-1" }));
565
+ store.insert(makeEvent({ agentName: "scout-1" }));
566
+ store.insert(makeEvent({ agentName: "builder-1" }));
567
+ store.close();
568
+
569
+ await feedCommand([]);
570
+ const out = output();
571
+
572
+ // Both builder-1 events should appear
573
+ expect(out).toContain("builder-1");
574
+ // scout-1 should appear
575
+ expect(out).toContain("scout-1");
576
+ });
577
+ });
578
+ });