@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,670 @@
1
+ /**
2
+ * Tests for `overstory inspect` command.
3
+ *
4
+ * Uses real bun:sqlite (temp files) to test the inspect 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
+ * SessionStore, MetricsStore). 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 { createMetricsStore } from "../metrics/store.ts";
18
+ import { createSessionStore } from "../sessions/store.ts";
19
+ import type { InsertEvent, SessionMetrics } from "../types.ts";
20
+ import { gatherInspectData, inspectCommand } from "./inspect.ts";
21
+
22
+ /** Helper to create an InsertEvent with sensible defaults. */
23
+ function makeEvent(overrides: Partial<InsertEvent> = {}): InsertEvent {
24
+ return {
25
+ runId: "run-001",
26
+ agentName: "builder-1",
27
+ sessionId: "sess-abc",
28
+ eventType: "tool_start",
29
+ toolName: "Read",
30
+ toolArgs: '{"file_path": "src/index.ts"}',
31
+ toolDurationMs: null,
32
+ level: "info",
33
+ data: null,
34
+ ...overrides,
35
+ };
36
+ }
37
+
38
+ /** Helper to create a SessionMetrics with sensible defaults. */
39
+ function makeMetrics(overrides: Partial<SessionMetrics> = {}): SessionMetrics {
40
+ return {
41
+ agentName: "builder-1",
42
+ beadId: "overstory-001",
43
+ capability: "builder",
44
+ startedAt: new Date().toISOString(),
45
+ completedAt: null,
46
+ durationMs: 0,
47
+ exitCode: null,
48
+ mergeResult: null,
49
+ parentAgent: null,
50
+ inputTokens: 1000,
51
+ outputTokens: 500,
52
+ cacheReadTokens: 200,
53
+ cacheCreationTokens: 100,
54
+ estimatedCostUsd: 0.025,
55
+ modelUsed: "claude-sonnet-4-5-20250929",
56
+ runId: null,
57
+ ...overrides,
58
+ };
59
+ }
60
+
61
+ describe("inspectCommand", () => {
62
+ let chunks: string[];
63
+ let originalWrite: typeof process.stdout.write;
64
+ let tempDir: string;
65
+ let originalCwd: string;
66
+
67
+ beforeEach(async () => {
68
+ // Spy on stdout
69
+ chunks = [];
70
+ originalWrite = process.stdout.write;
71
+ process.stdout.write = ((chunk: string) => {
72
+ chunks.push(chunk);
73
+ return true;
74
+ }) as typeof process.stdout.write;
75
+
76
+ // Create temp dir with .overstory/config.yaml structure
77
+ tempDir = await mkdtemp(join(tmpdir(), "inspect-test-"));
78
+ const overstoryDir = join(tempDir, ".overstory");
79
+ await Bun.write(
80
+ join(overstoryDir, "config.yaml"),
81
+ `project:\n name: test\n root: ${tempDir}\n canonicalBranch: main\n`,
82
+ );
83
+
84
+ // Change to temp dir so loadConfig() works
85
+ originalCwd = process.cwd();
86
+ process.chdir(tempDir);
87
+ });
88
+
89
+ afterEach(async () => {
90
+ process.stdout.write = originalWrite;
91
+ process.chdir(originalCwd);
92
+ await rm(tempDir, { recursive: true, force: true });
93
+ });
94
+
95
+ function output(): string {
96
+ return chunks.join("");
97
+ }
98
+
99
+ // === Help flag ===
100
+
101
+ describe("help flag", () => {
102
+ test("--help shows help text", async () => {
103
+ await inspectCommand(["--help"]);
104
+ const out = output();
105
+ expect(out).toContain("overstory inspect");
106
+ expect(out).toContain("--json");
107
+ expect(out).toContain("--follow");
108
+ expect(out).toContain("--limit");
109
+ expect(out).toContain("--no-tmux");
110
+ });
111
+
112
+ test("-h shows help text", async () => {
113
+ await inspectCommand(["-h"]);
114
+ const out = output();
115
+ expect(out).toContain("overstory inspect");
116
+ });
117
+ });
118
+
119
+ // === Validation errors ===
120
+
121
+ describe("validation", () => {
122
+ test("throws if no agent name provided", async () => {
123
+ await expect(inspectCommand([])).rejects.toThrow(ValidationError);
124
+ });
125
+
126
+ test("throws if agent not found", async () => {
127
+ const overstoryDir = join(tempDir, ".overstory");
128
+ const sessionsDbPath = join(overstoryDir, "sessions.db");
129
+ const store = createSessionStore(sessionsDbPath);
130
+ store.close();
131
+
132
+ await expect(inspectCommand(["nonexistent-agent"])).rejects.toThrow(ValidationError);
133
+ });
134
+
135
+ test("throws if --interval is invalid", async () => {
136
+ const overstoryDir = join(tempDir, ".overstory");
137
+ const sessionsDbPath = join(overstoryDir, "sessions.db");
138
+ const store = createSessionStore(sessionsDbPath);
139
+ store.upsert({
140
+ id: "sess-1",
141
+ agentName: "builder-1",
142
+ capability: "builder",
143
+ worktreePath: "/tmp/wt",
144
+ branchName: "overstory/builder-1/test",
145
+ beadId: "overstory-001",
146
+ tmuxSession: "overstory-test-builder-1",
147
+ state: "working",
148
+ pid: 12345,
149
+ parentAgent: null,
150
+ depth: 0,
151
+ runId: null,
152
+ startedAt: new Date().toISOString(),
153
+ lastActivity: new Date().toISOString(),
154
+ escalationLevel: 0,
155
+ stalledSince: null,
156
+ });
157
+ store.close();
158
+
159
+ await expect(inspectCommand(["builder-1", "--interval", "abc"])).rejects.toThrow(
160
+ ValidationError,
161
+ );
162
+ await expect(inspectCommand(["builder-1", "--interval", "100"])).rejects.toThrow(
163
+ ValidationError,
164
+ );
165
+ });
166
+
167
+ test("throws if --limit is invalid", async () => {
168
+ const overstoryDir = join(tempDir, ".overstory");
169
+ const sessionsDbPath = join(overstoryDir, "sessions.db");
170
+ const store = createSessionStore(sessionsDbPath);
171
+ store.upsert({
172
+ id: "sess-1",
173
+ agentName: "builder-1",
174
+ capability: "builder",
175
+ worktreePath: "/tmp/wt",
176
+ branchName: "overstory/builder-1/test",
177
+ beadId: "overstory-001",
178
+ tmuxSession: "overstory-test-builder-1",
179
+ state: "working",
180
+ pid: 12345,
181
+ parentAgent: null,
182
+ depth: 0,
183
+ runId: null,
184
+ startedAt: new Date().toISOString(),
185
+ lastActivity: new Date().toISOString(),
186
+ escalationLevel: 0,
187
+ stalledSince: null,
188
+ });
189
+ store.close();
190
+
191
+ await expect(inspectCommand(["builder-1", "--limit", "abc"])).rejects.toThrow(
192
+ ValidationError,
193
+ );
194
+ await expect(inspectCommand(["builder-1", "--limit", "0"])).rejects.toThrow(ValidationError);
195
+ });
196
+ });
197
+
198
+ // === gatherInspectData ===
199
+
200
+ describe("gatherInspectData", () => {
201
+ test("gathers basic session data", async () => {
202
+ const overstoryDir = join(tempDir, ".overstory");
203
+ const sessionsDbPath = join(overstoryDir, "sessions.db");
204
+ const store = createSessionStore(sessionsDbPath);
205
+
206
+ const startedAt = new Date(Date.now() - 60_000).toISOString(); // 60s ago
207
+ store.upsert({
208
+ id: "sess-1",
209
+ agentName: "builder-1",
210
+ capability: "builder",
211
+ worktreePath: "/tmp/wt",
212
+ branchName: "overstory/builder-1/test",
213
+ beadId: "overstory-001",
214
+ tmuxSession: "overstory-test-builder-1",
215
+ state: "working",
216
+ pid: 12345,
217
+ parentAgent: "orchestrator",
218
+ depth: 1,
219
+ runId: "run-001",
220
+ startedAt,
221
+ lastActivity: new Date(Date.now() - 5_000).toISOString(),
222
+ escalationLevel: 0,
223
+ stalledSince: null,
224
+ });
225
+ store.close();
226
+
227
+ const data = await gatherInspectData(tempDir, "builder-1", { noTmux: true });
228
+
229
+ expect(data.session.agentName).toBe("builder-1");
230
+ expect(data.session.capability).toBe("builder");
231
+ expect(data.session.state).toBe("working");
232
+ expect(data.session.beadId).toBe("overstory-001");
233
+ expect(data.timeSinceLastActivity).toBeGreaterThan(4000);
234
+ expect(data.timeSinceLastActivity).toBeLessThan(10000);
235
+ });
236
+
237
+ test("extracts current file from recent Edit tool_start event", async () => {
238
+ const overstoryDir = join(tempDir, ".overstory");
239
+ const sessionsDbPath = join(overstoryDir, "sessions.db");
240
+ const eventsDbPath = join(overstoryDir, "events.db");
241
+
242
+ const store = createSessionStore(sessionsDbPath);
243
+ store.upsert({
244
+ id: "sess-1",
245
+ agentName: "builder-1",
246
+ capability: "builder",
247
+ worktreePath: "/tmp/wt",
248
+ branchName: "overstory/builder-1/test",
249
+ beadId: "overstory-001",
250
+ tmuxSession: "overstory-test-builder-1",
251
+ state: "working",
252
+ pid: 12345,
253
+ parentAgent: null,
254
+ depth: 0,
255
+ runId: null,
256
+ startedAt: new Date().toISOString(),
257
+ lastActivity: new Date().toISOString(),
258
+ escalationLevel: 0,
259
+ stalledSince: null,
260
+ });
261
+ store.close();
262
+
263
+ const eventStore = createEventStore(eventsDbPath);
264
+ eventStore.insert(makeEvent({ toolName: "Read", toolArgs: '{"file_path": "src/a.ts"}' }));
265
+ eventStore.insert(
266
+ makeEvent({ toolName: "Edit", toolArgs: '{"file_path": "src/commands/inspect.ts"}' }),
267
+ );
268
+ eventStore.insert(makeEvent({ toolName: "Bash", toolArgs: '{"command": "bun test"}' }));
269
+ eventStore.close();
270
+
271
+ const data = await gatherInspectData(tempDir, "builder-1", { noTmux: true });
272
+
273
+ expect(data.currentFile).toBe("src/commands/inspect.ts");
274
+ });
275
+
276
+ test("extracts current file from Write tool_start with path field", async () => {
277
+ const overstoryDir = join(tempDir, ".overstory");
278
+ const sessionsDbPath = join(overstoryDir, "sessions.db");
279
+ const eventsDbPath = join(overstoryDir, "events.db");
280
+
281
+ const store = createSessionStore(sessionsDbPath);
282
+ store.upsert({
283
+ id: "sess-1",
284
+ agentName: "builder-1",
285
+ capability: "builder",
286
+ worktreePath: "/tmp/wt",
287
+ branchName: "overstory/builder-1/test",
288
+ beadId: "overstory-001",
289
+ tmuxSession: "overstory-test-builder-1",
290
+ state: "working",
291
+ pid: 12345,
292
+ parentAgent: null,
293
+ depth: 0,
294
+ runId: null,
295
+ startedAt: new Date().toISOString(),
296
+ lastActivity: new Date().toISOString(),
297
+ escalationLevel: 0,
298
+ stalledSince: null,
299
+ });
300
+ store.close();
301
+
302
+ const eventStore = createEventStore(eventsDbPath);
303
+ eventStore.insert(makeEvent({ toolName: "Write", toolArgs: '{"path": "src/new-file.ts"}' }));
304
+ eventStore.close();
305
+
306
+ const data = await gatherInspectData(tempDir, "builder-1", { noTmux: true });
307
+
308
+ expect(data.currentFile).toBe("src/new-file.ts");
309
+ });
310
+
311
+ test("returns null current file if no Edit/Write/Read events", async () => {
312
+ const overstoryDir = join(tempDir, ".overstory");
313
+ const sessionsDbPath = join(overstoryDir, "sessions.db");
314
+ const eventsDbPath = join(overstoryDir, "events.db");
315
+
316
+ const store = createSessionStore(sessionsDbPath);
317
+ store.upsert({
318
+ id: "sess-1",
319
+ agentName: "builder-1",
320
+ capability: "builder",
321
+ worktreePath: "/tmp/wt",
322
+ branchName: "overstory/builder-1/test",
323
+ beadId: "overstory-001",
324
+ tmuxSession: "overstory-test-builder-1",
325
+ state: "working",
326
+ pid: 12345,
327
+ parentAgent: null,
328
+ depth: 0,
329
+ runId: null,
330
+ startedAt: new Date().toISOString(),
331
+ lastActivity: new Date().toISOString(),
332
+ escalationLevel: 0,
333
+ stalledSince: null,
334
+ });
335
+ store.close();
336
+
337
+ const eventStore = createEventStore(eventsDbPath);
338
+ eventStore.insert(makeEvent({ toolName: "Bash", toolArgs: '{"command": "bun test"}' }));
339
+ eventStore.close();
340
+
341
+ const data = await gatherInspectData(tempDir, "builder-1", { noTmux: true });
342
+
343
+ expect(data.currentFile).toBeNull();
344
+ });
345
+
346
+ test("gathers recent tool calls (respects limit)", async () => {
347
+ const overstoryDir = join(tempDir, ".overstory");
348
+ const sessionsDbPath = join(overstoryDir, "sessions.db");
349
+ const eventsDbPath = join(overstoryDir, "events.db");
350
+
351
+ const store = createSessionStore(sessionsDbPath);
352
+ store.upsert({
353
+ id: "sess-1",
354
+ agentName: "builder-1",
355
+ capability: "builder",
356
+ worktreePath: "/tmp/wt",
357
+ branchName: "overstory/builder-1/test",
358
+ beadId: "overstory-001",
359
+ tmuxSession: "overstory-test-builder-1",
360
+ state: "working",
361
+ pid: 12345,
362
+ parentAgent: null,
363
+ depth: 0,
364
+ runId: null,
365
+ startedAt: new Date().toISOString(),
366
+ lastActivity: new Date().toISOString(),
367
+ escalationLevel: 0,
368
+ stalledSince: null,
369
+ });
370
+ store.close();
371
+
372
+ const eventStore = createEventStore(eventsDbPath);
373
+ for (let i = 0; i < 30; i++) {
374
+ eventStore.insert(
375
+ makeEvent({
376
+ toolName: "Read",
377
+ toolArgs: `{"file_path": "src/file${i}.ts"}`,
378
+ toolDurationMs: 10 + i,
379
+ }),
380
+ );
381
+ }
382
+ eventStore.close();
383
+
384
+ const data = await gatherInspectData(tempDir, "builder-1", { noTmux: true, limit: 5 });
385
+
386
+ expect(data.recentToolCalls.length).toBe(5);
387
+ expect(data.recentToolCalls[0]?.toolName).toBe("Read");
388
+ });
389
+
390
+ test("gathers tool stats", async () => {
391
+ const overstoryDir = join(tempDir, ".overstory");
392
+ const sessionsDbPath = join(overstoryDir, "sessions.db");
393
+ const eventsDbPath = join(overstoryDir, "events.db");
394
+
395
+ const store = createSessionStore(sessionsDbPath);
396
+ store.upsert({
397
+ id: "sess-1",
398
+ agentName: "builder-1",
399
+ capability: "builder",
400
+ worktreePath: "/tmp/wt",
401
+ branchName: "overstory/builder-1/test",
402
+ beadId: "overstory-001",
403
+ tmuxSession: "overstory-test-builder-1",
404
+ state: "working",
405
+ pid: 12345,
406
+ parentAgent: null,
407
+ depth: 0,
408
+ runId: null,
409
+ startedAt: new Date().toISOString(),
410
+ lastActivity: new Date().toISOString(),
411
+ escalationLevel: 0,
412
+ stalledSince: null,
413
+ });
414
+ store.close();
415
+
416
+ const eventStore = createEventStore(eventsDbPath);
417
+ for (let i = 0; i < 10; i++) {
418
+ eventStore.insert(makeEvent({ toolName: "Read", toolDurationMs: 100 }));
419
+ }
420
+ for (let i = 0; i < 5; i++) {
421
+ eventStore.insert(makeEvent({ toolName: "Edit", toolDurationMs: 200 }));
422
+ }
423
+ eventStore.close();
424
+
425
+ const data = await gatherInspectData(tempDir, "builder-1", { noTmux: true });
426
+
427
+ expect(data.toolStats.length).toBeGreaterThan(0);
428
+ const readStats = data.toolStats.find((s) => s.toolName === "Read");
429
+ expect(readStats?.count).toBe(10);
430
+ expect(readStats?.avgDurationMs).toBe(100);
431
+ });
432
+
433
+ test("gathers token usage from metrics", async () => {
434
+ const overstoryDir = join(tempDir, ".overstory");
435
+ const sessionsDbPath = join(overstoryDir, "sessions.db");
436
+ const metricsDbPath = join(overstoryDir, "metrics.db");
437
+
438
+ const store = createSessionStore(sessionsDbPath);
439
+ store.upsert({
440
+ id: "sess-1",
441
+ agentName: "builder-1",
442
+ capability: "builder",
443
+ worktreePath: "/tmp/wt",
444
+ branchName: "overstory/builder-1/test",
445
+ beadId: "overstory-001",
446
+ tmuxSession: "overstory-test-builder-1",
447
+ state: "working",
448
+ pid: 12345,
449
+ parentAgent: null,
450
+ depth: 0,
451
+ runId: null,
452
+ startedAt: new Date().toISOString(),
453
+ lastActivity: new Date().toISOString(),
454
+ escalationLevel: 0,
455
+ stalledSince: null,
456
+ });
457
+ store.close();
458
+
459
+ const metricsStore = createMetricsStore(metricsDbPath);
460
+ metricsStore.recordSession(
461
+ makeMetrics({
462
+ inputTokens: 5000,
463
+ outputTokens: 3000,
464
+ cacheReadTokens: 1000,
465
+ cacheCreationTokens: 500,
466
+ estimatedCostUsd: 0.123,
467
+ modelUsed: "claude-sonnet-4-5-20250929",
468
+ }),
469
+ );
470
+ metricsStore.close();
471
+
472
+ const data = await gatherInspectData(tempDir, "builder-1", { noTmux: true });
473
+
474
+ expect(data.tokenUsage).not.toBeNull();
475
+ expect(data.tokenUsage?.inputTokens).toBe(5000);
476
+ expect(data.tokenUsage?.outputTokens).toBe(3000);
477
+ expect(data.tokenUsage?.cacheReadTokens).toBe(1000);
478
+ expect(data.tokenUsage?.cacheCreationTokens).toBe(500);
479
+ expect(data.tokenUsage?.estimatedCostUsd).toBe(0.123);
480
+ expect(data.tokenUsage?.modelUsed).toBe("claude-sonnet-4-5-20250929");
481
+ });
482
+
483
+ test("handles missing databases gracefully", async () => {
484
+ const overstoryDir = join(tempDir, ".overstory");
485
+ const sessionsDbPath = join(overstoryDir, "sessions.db");
486
+
487
+ const store = createSessionStore(sessionsDbPath);
488
+ store.upsert({
489
+ id: "sess-1",
490
+ agentName: "builder-1",
491
+ capability: "builder",
492
+ worktreePath: "/tmp/wt",
493
+ branchName: "overstory/builder-1/test",
494
+ beadId: "overstory-001",
495
+ tmuxSession: "overstory-test-builder-1",
496
+ state: "working",
497
+ pid: 12345,
498
+ parentAgent: null,
499
+ depth: 0,
500
+ runId: null,
501
+ startedAt: new Date().toISOString(),
502
+ lastActivity: new Date().toISOString(),
503
+ escalationLevel: 0,
504
+ stalledSince: null,
505
+ });
506
+ store.close();
507
+
508
+ // Don't create events.db or metrics.db
509
+ const data = await gatherInspectData(tempDir, "builder-1", { noTmux: true });
510
+
511
+ expect(data.recentToolCalls).toEqual([]);
512
+ expect(data.currentFile).toBeNull();
513
+ expect(data.toolStats).toEqual([]);
514
+ expect(data.tokenUsage).toBeNull();
515
+ });
516
+ });
517
+
518
+ // === JSON output ===
519
+
520
+ describe("json output", () => {
521
+ test("--json outputs valid JSON", async () => {
522
+ const overstoryDir = join(tempDir, ".overstory");
523
+ const sessionsDbPath = join(overstoryDir, "sessions.db");
524
+ const store = createSessionStore(sessionsDbPath);
525
+ store.upsert({
526
+ id: "sess-1",
527
+ agentName: "builder-1",
528
+ capability: "builder",
529
+ worktreePath: "/tmp/wt",
530
+ branchName: "overstory/builder-1/test",
531
+ beadId: "overstory-001",
532
+ tmuxSession: "overstory-test-builder-1",
533
+ state: "working",
534
+ pid: 12345,
535
+ parentAgent: null,
536
+ depth: 0,
537
+ runId: null,
538
+ startedAt: new Date().toISOString(),
539
+ lastActivity: new Date().toISOString(),
540
+ escalationLevel: 0,
541
+ stalledSince: null,
542
+ });
543
+ store.close();
544
+
545
+ await inspectCommand(["builder-1", "--json", "--no-tmux"]);
546
+ const out = output();
547
+
548
+ const parsed = JSON.parse(out);
549
+ expect(parsed.session.agentName).toBe("builder-1");
550
+ expect(parsed.timeSinceLastActivity).toBeGreaterThan(0);
551
+ });
552
+ });
553
+
554
+ // === Human-readable output ===
555
+
556
+ describe("human-readable output", () => {
557
+ test("displays agent metadata", async () => {
558
+ const overstoryDir = join(tempDir, ".overstory");
559
+ const sessionsDbPath = join(overstoryDir, "sessions.db");
560
+ const store = createSessionStore(sessionsDbPath);
561
+ store.upsert({
562
+ id: "sess-1",
563
+ agentName: "builder-1",
564
+ capability: "builder",
565
+ worktreePath: "/tmp/wt",
566
+ branchName: "overstory/builder-1/test",
567
+ beadId: "overstory-001",
568
+ tmuxSession: "overstory-test-builder-1",
569
+ state: "working",
570
+ pid: 12345,
571
+ parentAgent: "orchestrator",
572
+ depth: 1,
573
+ runId: null,
574
+ startedAt: new Date().toISOString(),
575
+ lastActivity: new Date().toISOString(),
576
+ escalationLevel: 0,
577
+ stalledSince: null,
578
+ });
579
+ store.close();
580
+
581
+ await inspectCommand(["builder-1", "--no-tmux"]);
582
+ const out = output();
583
+
584
+ expect(out).toContain("builder-1");
585
+ expect(out).toContain("working");
586
+ expect(out).toContain("overstory-001");
587
+ expect(out).toContain("builder");
588
+ expect(out).toContain("overstory/builder-1/test");
589
+ expect(out).toContain("orchestrator");
590
+ });
591
+
592
+ test("displays token usage", async () => {
593
+ const overstoryDir = join(tempDir, ".overstory");
594
+ const sessionsDbPath = join(overstoryDir, "sessions.db");
595
+ const metricsDbPath = join(overstoryDir, "metrics.db");
596
+
597
+ const store = createSessionStore(sessionsDbPath);
598
+ store.upsert({
599
+ id: "sess-1",
600
+ agentName: "builder-1",
601
+ capability: "builder",
602
+ worktreePath: "/tmp/wt",
603
+ branchName: "overstory/builder-1/test",
604
+ beadId: "overstory-001",
605
+ tmuxSession: "overstory-test-builder-1",
606
+ state: "working",
607
+ pid: 12345,
608
+ parentAgent: null,
609
+ depth: 0,
610
+ runId: null,
611
+ startedAt: new Date().toISOString(),
612
+ lastActivity: new Date().toISOString(),
613
+ escalationLevel: 0,
614
+ stalledSince: null,
615
+ });
616
+ store.close();
617
+
618
+ const metricsStore = createMetricsStore(metricsDbPath);
619
+ metricsStore.recordSession(makeMetrics({ estimatedCostUsd: 0.123 }));
620
+ metricsStore.close();
621
+
622
+ await inspectCommand(["builder-1", "--no-tmux"]);
623
+ const out = output();
624
+
625
+ expect(out).toContain("Token Usage");
626
+ expect(out).toContain("1,000");
627
+ expect(out).toContain("$0.1230");
628
+ });
629
+
630
+ test("displays tool stats and recent calls", async () => {
631
+ const overstoryDir = join(tempDir, ".overstory");
632
+ const sessionsDbPath = join(overstoryDir, "sessions.db");
633
+ const eventsDbPath = join(overstoryDir, "events.db");
634
+
635
+ const store = createSessionStore(sessionsDbPath);
636
+ store.upsert({
637
+ id: "sess-1",
638
+ agentName: "builder-1",
639
+ capability: "builder",
640
+ worktreePath: "/tmp/wt",
641
+ branchName: "overstory/builder-1/test",
642
+ beadId: "overstory-001",
643
+ tmuxSession: "overstory-test-builder-1",
644
+ state: "working",
645
+ pid: 12345,
646
+ parentAgent: null,
647
+ depth: 0,
648
+ runId: null,
649
+ startedAt: new Date().toISOString(),
650
+ lastActivity: new Date().toISOString(),
651
+ escalationLevel: 0,
652
+ stalledSince: null,
653
+ });
654
+ store.close();
655
+
656
+ const eventStore = createEventStore(eventsDbPath);
657
+ eventStore.insert(makeEvent({ toolName: "Read", toolDurationMs: 100 }));
658
+ eventStore.insert(makeEvent({ toolName: "Edit", toolDurationMs: 200 }));
659
+ eventStore.close();
660
+
661
+ await inspectCommand(["builder-1", "--no-tmux"]);
662
+ const out = output();
663
+
664
+ expect(out).toContain("Tool Usage");
665
+ expect(out).toContain("Recent Tool Calls");
666
+ expect(out).toContain("Read");
667
+ expect(out).toContain("Edit");
668
+ });
669
+ });
670
+ });