@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,647 @@
1
+ /**
2
+ * Tests for `overstory errors` command.
3
+ *
4
+ * Uses real bun:sqlite (temp files) to test the errors 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 { errorsCommand } from "./errors.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: "error",
27
+ toolName: null,
28
+ toolArgs: null,
29
+ toolDurationMs: null,
30
+ level: "error",
31
+ data: null,
32
+ ...overrides,
33
+ };
34
+ }
35
+
36
+ describe("errorsCommand", () => {
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(), "errors-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 errorsCommand(["--help"]);
79
+ const out = output();
80
+
81
+ expect(out).toContain("overstory errors");
82
+ expect(out).toContain("--agent");
83
+ expect(out).toContain("--run");
84
+ expect(out).toContain("--json");
85
+ expect(out).toContain("--since");
86
+ expect(out).toContain("--until");
87
+ expect(out).toContain("--limit");
88
+ });
89
+
90
+ test("-h shows help text", async () => {
91
+ await errorsCommand(["-h"]);
92
+ const out = output();
93
+
94
+ expect(out).toContain("overstory errors");
95
+ });
96
+ });
97
+
98
+ // === Argument parsing ===
99
+
100
+ describe("argument parsing", () => {
101
+ test("--limit with non-numeric value throws ValidationError", async () => {
102
+ await expect(errorsCommand(["--limit", "abc"])).rejects.toThrow(ValidationError);
103
+ });
104
+
105
+ test("--limit with zero throws ValidationError", async () => {
106
+ await expect(errorsCommand(["--limit", "0"])).rejects.toThrow(ValidationError);
107
+ });
108
+
109
+ test("--limit with negative value throws ValidationError", async () => {
110
+ await expect(errorsCommand(["--limit", "-5"])).rejects.toThrow(ValidationError);
111
+ });
112
+
113
+ test("--since with invalid timestamp throws ValidationError", async () => {
114
+ await expect(errorsCommand(["--since", "not-a-date"])).rejects.toThrow(ValidationError);
115
+ });
116
+
117
+ test("--until with invalid timestamp throws ValidationError", async () => {
118
+ await expect(errorsCommand(["--until", "not-a-date"])).rejects.toThrow(ValidationError);
119
+ });
120
+ });
121
+
122
+ // === Missing events.db (graceful handling) ===
123
+
124
+ describe("missing events.db", () => {
125
+ test("text mode outputs friendly message when no events.db exists", async () => {
126
+ await errorsCommand([]);
127
+ const out = output();
128
+
129
+ expect(out).toBe("No events data yet.\n");
130
+ });
131
+
132
+ test("JSON mode outputs empty array when no events.db exists", async () => {
133
+ await errorsCommand(["--json"]);
134
+ const out = output();
135
+
136
+ expect(out).toBe("[]\n");
137
+ });
138
+ });
139
+
140
+ // === JSON output mode ===
141
+
142
+ describe("JSON output mode", () => {
143
+ test("outputs valid JSON array with error events", async () => {
144
+ const dbPath = join(tempDir, ".overstory", "events.db");
145
+ const store = createEventStore(dbPath);
146
+ store.insert(makeEvent({ agentName: "builder-1" }));
147
+ store.insert(makeEvent({ agentName: "builder-2" }));
148
+ store.insert(
149
+ makeEvent({
150
+ agentName: "builder-1",
151
+ eventType: "tool_start",
152
+ level: "info",
153
+ }),
154
+ );
155
+ store.close();
156
+
157
+ await errorsCommand(["--json"]);
158
+ const out = output();
159
+
160
+ const parsed = JSON.parse(out.trim()) as unknown[];
161
+ expect(parsed).toHaveLength(2);
162
+ expect(Array.isArray(parsed)).toBe(true);
163
+ });
164
+
165
+ test("JSON output includes expected fields", async () => {
166
+ const dbPath = join(tempDir, ".overstory", "events.db");
167
+ const store = createEventStore(dbPath);
168
+ store.insert(
169
+ makeEvent({
170
+ agentName: "builder-1",
171
+ data: '{"message":"something broke"}',
172
+ }),
173
+ );
174
+ store.close();
175
+
176
+ await errorsCommand(["--json"]);
177
+ const out = output();
178
+
179
+ const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
180
+ expect(parsed).toHaveLength(1);
181
+ const event = parsed[0];
182
+ expect(event).toBeDefined();
183
+ expect(event?.agentName).toBe("builder-1");
184
+ expect(event?.eventType).toBe("error");
185
+ expect(event?.level).toBe("error");
186
+ expect(event?.createdAt).toBeTruthy();
187
+ });
188
+
189
+ test("JSON output returns empty array when no errors exist", async () => {
190
+ const dbPath = join(tempDir, ".overstory", "events.db");
191
+ const store = createEventStore(dbPath);
192
+ store.insert(
193
+ makeEvent({
194
+ agentName: "builder-1",
195
+ eventType: "tool_start",
196
+ level: "info",
197
+ }),
198
+ );
199
+ store.close();
200
+
201
+ await errorsCommand(["--json"]);
202
+ const out = output();
203
+
204
+ const parsed = JSON.parse(out.trim()) as unknown[];
205
+ expect(parsed).toEqual([]);
206
+ });
207
+ });
208
+
209
+ // === Human output format ===
210
+
211
+ describe("human output", () => {
212
+ test("shows header", async () => {
213
+ const dbPath = join(tempDir, ".overstory", "events.db");
214
+ const store = createEventStore(dbPath);
215
+ store.insert(makeEvent());
216
+ store.close();
217
+
218
+ await errorsCommand([]);
219
+ const out = output();
220
+
221
+ expect(out).toContain("Errors");
222
+ expect(out).toContain("=".repeat(70));
223
+ });
224
+
225
+ test("shows error count", async () => {
226
+ const dbPath = join(tempDir, ".overstory", "events.db");
227
+ const store = createEventStore(dbPath);
228
+ store.insert(makeEvent({ agentName: "builder-1" }));
229
+ store.insert(makeEvent({ agentName: "builder-2" }));
230
+ store.insert(makeEvent({ agentName: "builder-1" }));
231
+ store.close();
232
+
233
+ await errorsCommand([]);
234
+ const out = output();
235
+
236
+ expect(out).toContain("3 errors");
237
+ });
238
+
239
+ test("shows singular error count", async () => {
240
+ const dbPath = join(tempDir, ".overstory", "events.db");
241
+ const store = createEventStore(dbPath);
242
+ store.insert(makeEvent());
243
+ store.close();
244
+
245
+ await errorsCommand([]);
246
+ const out = output();
247
+
248
+ expect(out).toContain("1 error");
249
+ // Should NOT say "1 errors"
250
+ expect(out).not.toMatch(/1 errors/);
251
+ });
252
+
253
+ test("no errors shows 'No errors found' message", async () => {
254
+ const dbPath = join(tempDir, ".overstory", "events.db");
255
+ const store = createEventStore(dbPath);
256
+ // Insert a non-error event so the DB exists but has no errors
257
+ store.insert(
258
+ makeEvent({
259
+ eventType: "tool_start",
260
+ level: "info",
261
+ }),
262
+ );
263
+ store.close();
264
+
265
+ await errorsCommand([]);
266
+ const out = output();
267
+
268
+ expect(out).toContain("No errors found");
269
+ });
270
+
271
+ test("groups errors by agent name", async () => {
272
+ const dbPath = join(tempDir, ".overstory", "events.db");
273
+ const store = createEventStore(dbPath);
274
+ store.insert(makeEvent({ agentName: "builder-1" }));
275
+ store.insert(makeEvent({ agentName: "scout-1" }));
276
+ store.insert(makeEvent({ agentName: "builder-1" }));
277
+ store.close();
278
+
279
+ await errorsCommand([]);
280
+ const out = output();
281
+
282
+ // Both agent names should appear as group headers
283
+ expect(out).toContain("builder-1");
284
+ expect(out).toContain("scout-1");
285
+ // Per-agent counts should appear
286
+ expect(out).toContain("2 errors");
287
+ expect(out).toContain("1 error");
288
+ });
289
+
290
+ test("shows ERROR label for each event", async () => {
291
+ const dbPath = join(tempDir, ".overstory", "events.db");
292
+ const store = createEventStore(dbPath);
293
+ store.insert(makeEvent());
294
+ store.close();
295
+
296
+ await errorsCommand([]);
297
+ const out = output();
298
+
299
+ expect(out).toContain("ERROR");
300
+ });
301
+
302
+ test("shows timestamp for each error", async () => {
303
+ const dbPath = join(tempDir, ".overstory", "events.db");
304
+ const store = createEventStore(dbPath);
305
+ store.insert(makeEvent());
306
+ store.close();
307
+
308
+ await errorsCommand([]);
309
+ const out = output();
310
+
311
+ // Should contain a timestamp in HH:MM:SS format
312
+ expect(out).toMatch(/\d{2}:\d{2}:\d{2}/);
313
+ });
314
+
315
+ test("shows tool name in detail", async () => {
316
+ const dbPath = join(tempDir, ".overstory", "events.db");
317
+ const store = createEventStore(dbPath);
318
+ store.insert(makeEvent({ toolName: "Bash" }));
319
+ store.close();
320
+
321
+ await errorsCommand([]);
322
+ const out = output();
323
+
324
+ expect(out).toContain("tool=Bash");
325
+ });
326
+
327
+ test("shows custom data fields in detail", async () => {
328
+ const dbPath = join(tempDir, ".overstory", "events.db");
329
+ const store = createEventStore(dbPath);
330
+ store.insert(
331
+ makeEvent({
332
+ data: '{"reason":"disk full","code":500}',
333
+ }),
334
+ );
335
+ store.close();
336
+
337
+ await errorsCommand([]);
338
+ const out = output();
339
+
340
+ expect(out).toContain("reason=disk full");
341
+ expect(out).toContain("code=500");
342
+ });
343
+
344
+ test("long data values are truncated", async () => {
345
+ const dbPath = join(tempDir, ".overstory", "events.db");
346
+ const store = createEventStore(dbPath);
347
+ const longValue = "x".repeat(200);
348
+ store.insert(
349
+ makeEvent({
350
+ data: JSON.stringify({ message: longValue }),
351
+ }),
352
+ );
353
+ store.close();
354
+
355
+ await errorsCommand([]);
356
+ const out = output();
357
+
358
+ // The full 200-char value should not appear
359
+ expect(out).not.toContain(longValue);
360
+ // But a truncated version with "..." should
361
+ expect(out).toContain("...");
362
+ });
363
+
364
+ test("non-JSON data is shown raw if short", async () => {
365
+ const dbPath = join(tempDir, ".overstory", "events.db");
366
+ const store = createEventStore(dbPath);
367
+ store.insert(makeEvent({ data: "simple error text" }));
368
+ store.close();
369
+
370
+ await errorsCommand([]);
371
+ const out = output();
372
+
373
+ expect(out).toContain("simple error text");
374
+ });
375
+ });
376
+
377
+ // === --agent filter ===
378
+
379
+ describe("--agent filter", () => {
380
+ test("filters errors to a specific agent", async () => {
381
+ const dbPath = join(tempDir, ".overstory", "events.db");
382
+ const store = createEventStore(dbPath);
383
+ store.insert(makeEvent({ agentName: "builder-1" }));
384
+ store.insert(makeEvent({ agentName: "builder-2" }));
385
+ store.insert(makeEvent({ agentName: "builder-1" }));
386
+ store.close();
387
+
388
+ await errorsCommand(["--agent", "builder-1", "--json"]);
389
+ const out = output();
390
+
391
+ const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
392
+ expect(parsed).toHaveLength(2);
393
+ for (const event of parsed) {
394
+ expect(event.agentName).toBe("builder-1");
395
+ }
396
+ });
397
+
398
+ test("returns empty when agent has no errors", async () => {
399
+ const dbPath = join(tempDir, ".overstory", "events.db");
400
+ const store = createEventStore(dbPath);
401
+ store.insert(makeEvent({ agentName: "builder-1" }));
402
+ store.close();
403
+
404
+ await errorsCommand(["--agent", "nonexistent", "--json"]);
405
+ const out = output();
406
+
407
+ const parsed = JSON.parse(out.trim()) as unknown[];
408
+ expect(parsed).toEqual([]);
409
+ });
410
+
411
+ test("only returns error-level events for the agent", async () => {
412
+ const dbPath = join(tempDir, ".overstory", "events.db");
413
+ const store = createEventStore(dbPath);
414
+ store.insert(makeEvent({ agentName: "builder-1", level: "error" }));
415
+ store.insert(
416
+ makeEvent({
417
+ agentName: "builder-1",
418
+ eventType: "tool_start",
419
+ level: "info",
420
+ }),
421
+ );
422
+ store.insert(
423
+ makeEvent({
424
+ agentName: "builder-1",
425
+ eventType: "tool_end",
426
+ level: "warn",
427
+ }),
428
+ );
429
+ store.close();
430
+
431
+ await errorsCommand(["--agent", "builder-1", "--json"]);
432
+ const out = output();
433
+
434
+ const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
435
+ expect(parsed).toHaveLength(1);
436
+ expect(parsed[0]?.level).toBe("error");
437
+ });
438
+ });
439
+
440
+ // === --run filter ===
441
+
442
+ describe("--run filter", () => {
443
+ test("filters errors to a specific run", async () => {
444
+ const dbPath = join(tempDir, ".overstory", "events.db");
445
+ const store = createEventStore(dbPath);
446
+ store.insert(makeEvent({ runId: "run-001" }));
447
+ store.insert(makeEvent({ runId: "run-002" }));
448
+ store.insert(makeEvent({ runId: "run-001" }));
449
+ store.close();
450
+
451
+ await errorsCommand(["--run", "run-001", "--json"]);
452
+ const out = output();
453
+
454
+ const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
455
+ expect(parsed).toHaveLength(2);
456
+ for (const event of parsed) {
457
+ expect(event.runId).toBe("run-001");
458
+ }
459
+ });
460
+
461
+ test("returns empty when run has no errors", async () => {
462
+ const dbPath = join(tempDir, ".overstory", "events.db");
463
+ const store = createEventStore(dbPath);
464
+ store.insert(makeEvent({ runId: "run-001" }));
465
+ store.close();
466
+
467
+ await errorsCommand(["--run", "run-999", "--json"]);
468
+ const out = output();
469
+
470
+ const parsed = JSON.parse(out.trim()) as unknown[];
471
+ expect(parsed).toEqual([]);
472
+ });
473
+
474
+ test("only returns error-level events for the run", async () => {
475
+ const dbPath = join(tempDir, ".overstory", "events.db");
476
+ const store = createEventStore(dbPath);
477
+ store.insert(makeEvent({ runId: "run-001", level: "error" }));
478
+ store.insert(
479
+ makeEvent({
480
+ runId: "run-001",
481
+ eventType: "tool_start",
482
+ level: "info",
483
+ }),
484
+ );
485
+ store.close();
486
+
487
+ await errorsCommand(["--run", "run-001", "--json"]);
488
+ const out = output();
489
+
490
+ const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
491
+ expect(parsed).toHaveLength(1);
492
+ expect(parsed[0]?.level).toBe("error");
493
+ });
494
+ });
495
+
496
+ // === --limit flag ===
497
+
498
+ describe("--limit flag", () => {
499
+ test("limits the number of errors returned", async () => {
500
+ const dbPath = join(tempDir, ".overstory", "events.db");
501
+ const store = createEventStore(dbPath);
502
+ for (let i = 0; i < 10; i++) {
503
+ store.insert(makeEvent());
504
+ }
505
+ store.close();
506
+
507
+ await errorsCommand(["--json", "--limit", "3"]);
508
+ const out = output();
509
+
510
+ const parsed = JSON.parse(out.trim()) as unknown[];
511
+ expect(parsed).toHaveLength(3);
512
+ });
513
+
514
+ test("default limit is 100", async () => {
515
+ const dbPath = join(tempDir, ".overstory", "events.db");
516
+ const store = createEventStore(dbPath);
517
+ for (let i = 0; i < 120; i++) {
518
+ store.insert(makeEvent());
519
+ }
520
+ store.close();
521
+
522
+ await errorsCommand(["--json"]);
523
+ const out = output();
524
+
525
+ const parsed = JSON.parse(out.trim()) as unknown[];
526
+ expect(parsed).toHaveLength(100);
527
+ });
528
+ });
529
+
530
+ // === --since and --until flags ===
531
+
532
+ describe("--since and --until flags", () => {
533
+ test("--since with future timestamp returns no errors", async () => {
534
+ const dbPath = join(tempDir, ".overstory", "events.db");
535
+ const store = createEventStore(dbPath);
536
+ store.insert(makeEvent());
537
+ store.close();
538
+
539
+ await errorsCommand(["--json", "--since", "2099-01-01T00:00:00Z"]);
540
+ const out = output();
541
+
542
+ const parsed = JSON.parse(out.trim()) as unknown[];
543
+ expect(parsed).toEqual([]);
544
+ });
545
+
546
+ test("--since with past timestamp returns all errors", async () => {
547
+ const dbPath = join(tempDir, ".overstory", "events.db");
548
+ const store = createEventStore(dbPath);
549
+ store.insert(makeEvent());
550
+ store.insert(makeEvent());
551
+ store.close();
552
+
553
+ await errorsCommand(["--json", "--since", "2020-01-01T00:00:00Z"]);
554
+ const out = output();
555
+
556
+ const parsed = JSON.parse(out.trim()) as unknown[];
557
+ expect(parsed).toHaveLength(2);
558
+ });
559
+
560
+ test("--until with past timestamp returns no errors", async () => {
561
+ const dbPath = join(tempDir, ".overstory", "events.db");
562
+ const store = createEventStore(dbPath);
563
+ store.insert(makeEvent());
564
+ store.close();
565
+
566
+ await errorsCommand(["--json", "--until", "2000-01-01T00:00:00Z"]);
567
+ const out = output();
568
+
569
+ const parsed = JSON.parse(out.trim()) as unknown[];
570
+ expect(parsed).toEqual([]);
571
+ });
572
+ });
573
+
574
+ // === Edge cases ===
575
+
576
+ describe("edge cases", () => {
577
+ test("handles event with all null optional fields", async () => {
578
+ const dbPath = join(tempDir, ".overstory", "events.db");
579
+ const store = createEventStore(dbPath);
580
+ store.insert(
581
+ makeEvent({
582
+ runId: null,
583
+ sessionId: null,
584
+ toolName: null,
585
+ toolArgs: null,
586
+ toolDurationMs: null,
587
+ data: null,
588
+ }),
589
+ );
590
+ store.close();
591
+
592
+ // Should not throw
593
+ await errorsCommand([]);
594
+ const out = output();
595
+
596
+ expect(out).toContain("Errors");
597
+ expect(out).toContain("1 error");
598
+ });
599
+
600
+ test("no arguments shows all errors (global view)", async () => {
601
+ const dbPath = join(tempDir, ".overstory", "events.db");
602
+ const store = createEventStore(dbPath);
603
+ store.insert(makeEvent({ agentName: "builder-1" }));
604
+ store.insert(makeEvent({ agentName: "scout-1" }));
605
+ store.insert(makeEvent({ agentName: "builder-2" }));
606
+ store.close();
607
+
608
+ await errorsCommand(["--json"]);
609
+ const out = output();
610
+
611
+ const parsed = JSON.parse(out.trim()) as unknown[];
612
+ expect(parsed).toHaveLength(3);
613
+ });
614
+
615
+ test("excludes non-error events from global view", async () => {
616
+ const dbPath = join(tempDir, ".overstory", "events.db");
617
+ const store = createEventStore(dbPath);
618
+ store.insert(makeEvent({ level: "error" }));
619
+ store.insert(
620
+ makeEvent({
621
+ eventType: "tool_start",
622
+ level: "info",
623
+ }),
624
+ );
625
+ store.insert(
626
+ makeEvent({
627
+ eventType: "session_start",
628
+ level: "info",
629
+ }),
630
+ );
631
+ store.insert(
632
+ makeEvent({
633
+ eventType: "tool_end",
634
+ level: "warn",
635
+ }),
636
+ );
637
+ store.close();
638
+
639
+ await errorsCommand(["--json"]);
640
+ const out = output();
641
+
642
+ const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
643
+ expect(parsed).toHaveLength(1);
644
+ expect(parsed[0]?.level).toBe("error");
645
+ });
646
+ });
647
+ });