@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,379 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdir, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { ValidationError } from "../errors.ts";
6
+ import type { LogEvent } from "../types.ts";
7
+ import { logsCommand } from "./logs.ts";
8
+
9
+ /**
10
+ * Test helper: capture stdout during command execution.
11
+ * Since logsCommand writes to process.stdout.write, we temporarily replace it.
12
+ */
13
+ async function captureStdout(fn: () => Promise<void>): Promise<string> {
14
+ let output = "";
15
+ const originalWrite = process.stdout.write;
16
+
17
+ process.stdout.write = ((chunk: string) => {
18
+ output += chunk;
19
+ return true;
20
+ }) as typeof process.stdout.write;
21
+
22
+ try {
23
+ await fn();
24
+ } finally {
25
+ process.stdout.write = originalWrite;
26
+ }
27
+
28
+ return output;
29
+ }
30
+
31
+ describe("logsCommand", () => {
32
+ let tmpDir: string;
33
+ let originalCwd: string;
34
+
35
+ beforeEach(async () => {
36
+ // Create a temp directory for each test
37
+ tmpDir = join(
38
+ tmpdir(),
39
+ `overstory-logs-test-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
40
+ );
41
+ await mkdir(tmpDir, { recursive: true });
42
+
43
+ // Save original cwd and change to tmpDir so loadConfig finds our test config
44
+ originalCwd = process.cwd();
45
+ process.chdir(tmpDir);
46
+ });
47
+
48
+ afterEach(async () => {
49
+ // Restore cwd
50
+ process.chdir(originalCwd);
51
+
52
+ // Clean up temp directory
53
+ try {
54
+ await rm(tmpDir, { recursive: true, force: true });
55
+ } catch {
56
+ // Ignore cleanup errors
57
+ }
58
+ });
59
+
60
+ /**
61
+ * Helper: create a minimal config.yaml in tmpDir.
62
+ */
63
+ async function createConfig(): Promise<void> {
64
+ const overstoryDir = join(tmpDir, ".overstory");
65
+ await mkdir(overstoryDir, { recursive: true });
66
+
67
+ const configContent = `project:
68
+ name: test-project
69
+ root: ${tmpDir}
70
+ canonicalBranch: main
71
+ `;
72
+
73
+ await writeFile(join(overstoryDir, "config.yaml"), configContent, "utf-8");
74
+ }
75
+
76
+ /**
77
+ * Helper: create an events.ndjson file for a given agent and session.
78
+ */
79
+ async function createLogFile(
80
+ agentName: string,
81
+ sessionTimestamp: string,
82
+ events: LogEvent[],
83
+ ): Promise<void> {
84
+ const logsDir = join(tmpDir, ".overstory", "logs", agentName, sessionTimestamp);
85
+ await mkdir(logsDir, { recursive: true });
86
+
87
+ const ndjson = events.map((e) => JSON.stringify(e)).join("\n");
88
+ await writeFile(join(logsDir, "events.ndjson"), ndjson, "utf-8");
89
+ }
90
+
91
+ test("shows help text", async () => {
92
+ await createConfig();
93
+
94
+ const output = await captureStdout(async () => {
95
+ await logsCommand(["--help"]);
96
+ });
97
+
98
+ expect(output).toContain("overstory logs");
99
+ expect(output).toContain("--agent");
100
+ expect(output).toContain("--level");
101
+ expect(output).toContain("--since");
102
+ });
103
+
104
+ test("no logs directory returns gracefully", async () => {
105
+ await createConfig();
106
+ // Do NOT create logs directory
107
+
108
+ const output = await captureStdout(async () => {
109
+ await logsCommand([]);
110
+ });
111
+
112
+ expect(output).toContain("No log files found");
113
+ });
114
+
115
+ test("lists all entries across agents", async () => {
116
+ await createConfig();
117
+
118
+ const eventsAgentA: LogEvent[] = [
119
+ {
120
+ timestamp: "2026-01-01T10:00:00.000Z",
121
+ level: "info",
122
+ event: "tool.start",
123
+ agentName: "agent-a",
124
+ data: { toolName: "Bash" },
125
+ },
126
+ ];
127
+
128
+ const eventsAgentB: LogEvent[] = [
129
+ {
130
+ timestamp: "2026-01-02T11:00:00.000Z",
131
+ level: "error",
132
+ event: "spawn.failed",
133
+ agentName: "agent-b",
134
+ data: { errorMessage: "worktree exists" },
135
+ },
136
+ ];
137
+
138
+ await createLogFile("agent-a", "2026-01-01T00-00-00-000Z", eventsAgentA);
139
+ await createLogFile("agent-b", "2026-01-02T00-00-00-000Z", eventsAgentB);
140
+
141
+ const output = await captureStdout(async () => {
142
+ await logsCommand([]);
143
+ });
144
+
145
+ expect(output).toContain("tool.start");
146
+ expect(output).toContain("agent-a");
147
+ expect(output).toContain("spawn.failed");
148
+ expect(output).toContain("agent-b");
149
+ expect(output).toContain("2 entries");
150
+ });
151
+
152
+ test("filters by agent", async () => {
153
+ await createConfig();
154
+
155
+ const eventsAgentA: LogEvent[] = [
156
+ {
157
+ timestamp: "2026-01-01T10:00:00.000Z",
158
+ level: "info",
159
+ event: "tool.start",
160
+ agentName: "agent-a",
161
+ data: {},
162
+ },
163
+ ];
164
+
165
+ const eventsAgentB: LogEvent[] = [
166
+ {
167
+ timestamp: "2026-01-02T11:00:00.000Z",
168
+ level: "info",
169
+ event: "worker.done",
170
+ agentName: "agent-b",
171
+ data: {},
172
+ },
173
+ ];
174
+
175
+ await createLogFile("agent-a", "2026-01-01T00-00-00-000Z", eventsAgentA);
176
+ await createLogFile("agent-b", "2026-01-02T00-00-00-000Z", eventsAgentB);
177
+
178
+ const output = await captureStdout(async () => {
179
+ await logsCommand(["--agent", "agent-a"]);
180
+ });
181
+
182
+ expect(output).toContain("tool.start");
183
+ expect(output).toContain("agent-a");
184
+ expect(output).not.toContain("worker.done");
185
+ expect(output).not.toContain("agent-b");
186
+ });
187
+
188
+ test("filters by level", async () => {
189
+ await createConfig();
190
+
191
+ const events: LogEvent[] = [
192
+ {
193
+ timestamp: "2026-01-01T10:00:00.000Z",
194
+ level: "info",
195
+ event: "info.event",
196
+ agentName: "agent-a",
197
+ data: {},
198
+ },
199
+ {
200
+ timestamp: "2026-01-01T10:01:00.000Z",
201
+ level: "error",
202
+ event: "error.event",
203
+ agentName: "agent-a",
204
+ data: {},
205
+ },
206
+ {
207
+ timestamp: "2026-01-01T10:02:00.000Z",
208
+ level: "warn",
209
+ event: "warn.event",
210
+ agentName: "agent-a",
211
+ data: {},
212
+ },
213
+ ];
214
+
215
+ await createLogFile("agent-a", "2026-01-01T00-00-00-000Z", events);
216
+
217
+ const output = await captureStdout(async () => {
218
+ await logsCommand(["--level", "error"]);
219
+ });
220
+
221
+ expect(output).toContain("error.event");
222
+ expect(output).not.toContain("info.event");
223
+ expect(output).not.toContain("warn.event");
224
+ expect(output).toContain("1 entry");
225
+ });
226
+
227
+ test("respects --limit", async () => {
228
+ await createConfig();
229
+
230
+ const events: LogEvent[] = [];
231
+ for (let i = 0; i < 10; i++) {
232
+ events.push({
233
+ timestamp: `2026-01-01T10:${i.toString().padStart(2, "0")}:00.000Z`,
234
+ level: "info",
235
+ event: `event-${i}`,
236
+ agentName: "agent-a",
237
+ data: {},
238
+ });
239
+ }
240
+
241
+ await createLogFile("agent-a", "2026-01-01T00-00-00-000Z", events);
242
+
243
+ const output = await captureStdout(async () => {
244
+ await logsCommand(["--limit", "3"]);
245
+ });
246
+
247
+ // Should show the 3 most recent entries (event-7, event-8, event-9)
248
+ expect(output).toContain("3 entries");
249
+ expect(output).toContain("event-7");
250
+ expect(output).toContain("event-8");
251
+ expect(output).toContain("event-9");
252
+ expect(output).not.toContain("event-0");
253
+ expect(output).not.toContain("event-6");
254
+ });
255
+
256
+ test("JSON output", async () => {
257
+ await createConfig();
258
+
259
+ const events: LogEvent[] = [
260
+ {
261
+ timestamp: "2026-01-01T10:00:00.000Z",
262
+ level: "info",
263
+ event: "tool.start",
264
+ agentName: "agent-a",
265
+ data: { toolName: "Bash" },
266
+ },
267
+ {
268
+ timestamp: "2026-01-02T11:00:00.000Z",
269
+ level: "error",
270
+ event: "spawn.failed",
271
+ agentName: "agent-b",
272
+ data: { errorMessage: "worktree exists" },
273
+ },
274
+ ];
275
+
276
+ await createLogFile("agent-a", "2026-01-01T00-00-00-000Z", [events[0] as LogEvent]);
277
+ await createLogFile("agent-b", "2026-01-02T00-00-00-000Z", [events[1] as LogEvent]);
278
+
279
+ const output = await captureStdout(async () => {
280
+ await logsCommand(["--json"]);
281
+ });
282
+
283
+ // Parse JSON output
284
+ const parsed: unknown = JSON.parse(output.trim());
285
+ expect(Array.isArray(parsed)).toBe(true);
286
+
287
+ const arr = parsed as LogEvent[];
288
+ expect(arr).toHaveLength(2);
289
+ expect(arr[0]?.event).toBe("tool.start");
290
+ expect(arr[1]?.event).toBe("spawn.failed");
291
+ });
292
+
293
+ test("filters by --since with ISO timestamp", async () => {
294
+ await createConfig();
295
+
296
+ const events: LogEvent[] = [
297
+ {
298
+ timestamp: "2026-01-01T10:00:00.000Z",
299
+ level: "info",
300
+ event: "event-10:00",
301
+ agentName: "agent-a",
302
+ data: {},
303
+ },
304
+ {
305
+ timestamp: "2026-01-01T11:00:00.000Z",
306
+ level: "info",
307
+ event: "event-11:00",
308
+ agentName: "agent-a",
309
+ data: {},
310
+ },
311
+ {
312
+ timestamp: "2026-01-01T12:00:00.000Z",
313
+ level: "info",
314
+ event: "event-12:00",
315
+ agentName: "agent-a",
316
+ data: {},
317
+ },
318
+ ];
319
+
320
+ await createLogFile("agent-a", "2026-01-01T00-00-00-000Z", events);
321
+
322
+ const output = await captureStdout(async () => {
323
+ await logsCommand(["--since", "2026-01-01T11:00:00.000Z"]);
324
+ });
325
+
326
+ expect(output).toContain("event-11:00");
327
+ expect(output).toContain("event-12:00");
328
+ expect(output).not.toContain("event-10:00");
329
+ expect(output).toContain("2 entries");
330
+ });
331
+
332
+ test("invalid level throws ValidationError", async () => {
333
+ await createConfig();
334
+
335
+ await expect(
336
+ captureStdout(async () => {
337
+ await logsCommand(["--level", "critical"]);
338
+ }),
339
+ ).rejects.toThrow(ValidationError);
340
+ });
341
+
342
+ test("invalid limit throws ValidationError", async () => {
343
+ await createConfig();
344
+
345
+ await expect(
346
+ captureStdout(async () => {
347
+ await logsCommand(["--limit", "abc"]);
348
+ }),
349
+ ).rejects.toThrow(ValidationError);
350
+ });
351
+
352
+ test("handles malformed NDJSON lines gracefully", async () => {
353
+ await createConfig();
354
+
355
+ const logsDir = join(tmpDir, ".overstory", "logs", "agent-a", "2026-01-01T00-00-00-000Z");
356
+ await mkdir(logsDir, { recursive: true });
357
+
358
+ // Write mixed valid and invalid NDJSON lines
359
+ const mixedContent = `{"timestamp":"2026-01-01T10:00:00.000Z","level":"info","event":"valid-event-1","agentName":"agent-a","data":{}}
360
+ this is not json
361
+ {"timestamp":"2026-01-01T10:01:00.000Z","level":"info","event":"valid-event-2","agentName":"agent-a","data":{}}
362
+ {"incomplete": "object"
363
+ {"timestamp":"2026-01-01T10:02:00.000Z","level":"info","event":"valid-event-3","agentName":"agent-a","data":{}}
364
+ `;
365
+
366
+ await writeFile(join(logsDir, "events.ndjson"), mixedContent, "utf-8");
367
+
368
+ const output = await captureStdout(async () => {
369
+ await logsCommand([]);
370
+ });
371
+
372
+ // Should show the 3 valid events, silently skip the malformed lines
373
+ expect(output).toContain("valid-event-1");
374
+ expect(output).toContain("valid-event-2");
375
+ expect(output).toContain("valid-event-3");
376
+ expect(output).toContain("3 entries");
377
+ expect(output).not.toContain("this is not json");
378
+ });
379
+ });