@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,360 @@
1
+ /**
2
+ * CLI command: overstory replay [--run <id>] [--agent <name>...] [--json]
3
+ * [--since <ts>] [--until <ts>] [--limit <n>]
4
+ *
5
+ * Shows an interleaved chronological replay of events across multiple agents.
6
+ * Like reading a combined log — all agents' events merged by timestamp.
7
+ */
8
+
9
+ import { join } from "node:path";
10
+ import { loadConfig } from "../config.ts";
11
+ import { ValidationError } from "../errors.ts";
12
+ import { createEventStore } from "../events/store.ts";
13
+ import { color } from "../logging/color.ts";
14
+ import type { EventType, StoredEvent } from "../types.ts";
15
+
16
+ /** Labels and colors for each event type. */
17
+ const EVENT_LABELS: Record<EventType, { label: string; color: string }> = {
18
+ tool_start: { label: "TOOL START", color: color.blue },
19
+ tool_end: { label: "TOOL END ", color: color.blue },
20
+ session_start: { label: "SESSION +", color: color.green },
21
+ session_end: { label: "SESSION -", color: color.yellow },
22
+ mail_sent: { label: "MAIL SENT ", color: color.cyan },
23
+ mail_received: { label: "MAIL RECV ", color: color.cyan },
24
+ spawn: { label: "SPAWN ", color: color.magenta },
25
+ error: { label: "ERROR ", color: color.red },
26
+ custom: { label: "CUSTOM ", color: color.gray },
27
+ };
28
+
29
+ /** Colors assigned to agents in order of first appearance. */
30
+ const AGENT_COLORS = [color.blue, color.green, color.yellow, color.cyan, color.magenta] as const;
31
+
32
+ /**
33
+ * Parse a named flag value from args.
34
+ */
35
+ function getFlag(args: string[], flag: string): string | undefined {
36
+ const idx = args.indexOf(flag);
37
+ if (idx === -1 || idx + 1 >= args.length) {
38
+ return undefined;
39
+ }
40
+ return args[idx + 1];
41
+ }
42
+
43
+ /**
44
+ * Parse all occurrences of a named flag from args.
45
+ * Returns an array of values (e.g., --agent a --agent b => ["a", "b"]).
46
+ */
47
+ function getAllFlags(args: string[], flag: string): string[] {
48
+ const values: string[] = [];
49
+ for (let i = 0; i < args.length; i++) {
50
+ if (args[i] === flag && i + 1 < args.length) {
51
+ const value = args[i + 1];
52
+ if (value !== undefined) {
53
+ values.push(value);
54
+ }
55
+ i++; // skip the value
56
+ }
57
+ }
58
+ return values;
59
+ }
60
+
61
+ function hasFlag(args: string[], flag: string): boolean {
62
+ return args.includes(flag);
63
+ }
64
+
65
+ /**
66
+ * Format a relative time string from a timestamp.
67
+ * Returns strings like "2m ago", "1h ago", "3d ago".
68
+ */
69
+ function formatRelativeTime(timestamp: string): string {
70
+ const eventTime = new Date(timestamp).getTime();
71
+ const now = Date.now();
72
+ const diffMs = now - eventTime;
73
+
74
+ if (diffMs < 0) return "just now";
75
+
76
+ const seconds = Math.floor(diffMs / 1000);
77
+ if (seconds < 60) return `${seconds}s ago`;
78
+
79
+ const minutes = Math.floor(seconds / 60);
80
+ if (minutes < 60) return `${minutes}m ago`;
81
+
82
+ const hours = Math.floor(minutes / 60);
83
+ if (hours < 24) return `${hours}h ago`;
84
+
85
+ const days = Math.floor(hours / 24);
86
+ return `${days}d ago`;
87
+ }
88
+
89
+ /**
90
+ * Format an absolute time from an ISO timestamp.
91
+ * Returns "HH:MM:SS" portion.
92
+ */
93
+ function formatAbsoluteTime(timestamp: string): string {
94
+ const match = /T(\d{2}:\d{2}:\d{2})/.exec(timestamp);
95
+ if (match?.[1]) {
96
+ return match[1];
97
+ }
98
+ return timestamp;
99
+ }
100
+
101
+ /**
102
+ * Format the date portion of an ISO timestamp.
103
+ * Returns "YYYY-MM-DD".
104
+ */
105
+ function formatDate(timestamp: string): string {
106
+ const match = /^(\d{4}-\d{2}-\d{2})/.exec(timestamp);
107
+ if (match?.[1]) {
108
+ return match[1];
109
+ }
110
+ return "";
111
+ }
112
+
113
+ /**
114
+ * Build a detail string for a timeline event based on its type and fields.
115
+ */
116
+ function buildEventDetail(event: StoredEvent): string {
117
+ const parts: string[] = [];
118
+
119
+ if (event.toolName) {
120
+ parts.push(`tool=${event.toolName}`);
121
+ }
122
+
123
+ if (event.toolDurationMs !== null) {
124
+ parts.push(`duration=${event.toolDurationMs}ms`);
125
+ }
126
+
127
+ if (event.data) {
128
+ try {
129
+ const parsed: unknown = JSON.parse(event.data);
130
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
131
+ const data = parsed as Record<string, unknown>;
132
+ for (const [key, value] of Object.entries(data)) {
133
+ if (value !== null && value !== undefined) {
134
+ const strValue = typeof value === "string" ? value : JSON.stringify(value);
135
+ // Truncate long values
136
+ const truncated = strValue.length > 80 ? `${strValue.slice(0, 77)}...` : strValue;
137
+ parts.push(`${key}=${truncated}`);
138
+ }
139
+ }
140
+ }
141
+ } catch {
142
+ // data is not valid JSON; show it raw if short enough
143
+ if (event.data.length <= 80) {
144
+ parts.push(event.data);
145
+ }
146
+ }
147
+ }
148
+
149
+ return parts.join(" ");
150
+ }
151
+
152
+ /**
153
+ * Assign a stable color to each agent based on order of first appearance.
154
+ */
155
+ function buildAgentColorMap(events: StoredEvent[]): Map<string, string> {
156
+ const colorMap = new Map<string, string>();
157
+ for (const event of events) {
158
+ if (!colorMap.has(event.agentName)) {
159
+ const colorIndex = colorMap.size % AGENT_COLORS.length;
160
+ const color = AGENT_COLORS[colorIndex];
161
+ if (color !== undefined) {
162
+ colorMap.set(event.agentName, color);
163
+ }
164
+ }
165
+ }
166
+ return colorMap;
167
+ }
168
+
169
+ /**
170
+ * Print events as an interleaved timeline with ANSI colors and agent labels.
171
+ */
172
+ function printReplay(events: StoredEvent[], useAbsoluteTime: boolean): void {
173
+ const w = process.stdout.write.bind(process.stdout);
174
+
175
+ w(`${color.bold}Replay${color.reset}\n`);
176
+ w(`${"=".repeat(70)}\n`);
177
+
178
+ if (events.length === 0) {
179
+ w(`${color.dim}No events found.${color.reset}\n`);
180
+ return;
181
+ }
182
+
183
+ w(`${color.dim}${events.length} event${events.length === 1 ? "" : "s"}${color.reset}\n\n`);
184
+
185
+ const colorMap = buildAgentColorMap(events);
186
+ let lastDate = "";
187
+
188
+ for (const event of events) {
189
+ // Print date separator when the date changes
190
+ const date = formatDate(event.createdAt);
191
+ if (date && date !== lastDate) {
192
+ if (lastDate !== "") {
193
+ w("\n");
194
+ }
195
+ w(`${color.dim}--- ${date} ---${color.reset}\n`);
196
+ lastDate = date;
197
+ }
198
+
199
+ const timeStr = useAbsoluteTime
200
+ ? formatAbsoluteTime(event.createdAt)
201
+ : formatRelativeTime(event.createdAt);
202
+
203
+ const eventInfo = EVENT_LABELS[event.eventType] ?? {
204
+ label: event.eventType.padEnd(10),
205
+ color: color.gray,
206
+ };
207
+
208
+ const levelColor =
209
+ event.level === "error" ? color.red : event.level === "warn" ? color.yellow : "";
210
+ const levelReset = levelColor ? color.reset : "";
211
+
212
+ const detail = buildEventDetail(event);
213
+ const detailSuffix = detail ? ` ${color.dim}${detail}${color.reset}` : "";
214
+
215
+ const agentColor = colorMap.get(event.agentName) ?? color.gray;
216
+ const agentLabel = ` ${agentColor}[${event.agentName}]${color.reset}`;
217
+
218
+ w(
219
+ `${color.dim}${timeStr.padStart(10)}${color.reset} ` +
220
+ `${levelColor}${eventInfo.color}${color.bold}${eventInfo.label}${color.reset}${levelReset}` +
221
+ `${agentLabel}${detailSuffix}\n`,
222
+ );
223
+ }
224
+ }
225
+
226
+ const REPLAY_HELP = `overstory replay -- Interleaved chronological replay across agents
227
+
228
+ Usage: overstory replay [options]
229
+
230
+ Options:
231
+ --run <id> Filter events by run ID
232
+ --agent <name> Filter by agent name (can appear multiple times)
233
+ --since <timestamp> Start time filter (ISO 8601)
234
+ --until <timestamp> End time filter (ISO 8601)
235
+ --limit <n> Max events to show (default: 200)
236
+ --json Output as JSON array of StoredEvent objects
237
+ --help, -h Show this help
238
+
239
+ If --run is specified, shows all events from that run.
240
+ If --agent is specified, shows events from those agents merged chronologically.
241
+ If neither is specified, tries to read the current run from .overstory/current-run.txt.
242
+ Falls back to a 24-hour timeline of all events.`;
243
+
244
+ /**
245
+ * Entry point for `overstory replay [--run <id>] [--agent <name>...] [--json]`.
246
+ */
247
+ export async function replayCommand(args: string[]): Promise<void> {
248
+ if (args.includes("--help") || args.includes("-h")) {
249
+ process.stdout.write(`${REPLAY_HELP}\n`);
250
+ return;
251
+ }
252
+
253
+ const json = hasFlag(args, "--json");
254
+ const runId = getFlag(args, "--run");
255
+ const agentNames = getAllFlags(args, "--agent");
256
+ const sinceStr = getFlag(args, "--since");
257
+ const untilStr = getFlag(args, "--until");
258
+ const limitStr = getFlag(args, "--limit");
259
+ const limit = limitStr ? Number.parseInt(limitStr, 10) : 200;
260
+
261
+ if (Number.isNaN(limit) || limit < 1) {
262
+ throw new ValidationError("--limit must be a positive integer", {
263
+ field: "limit",
264
+ value: limitStr,
265
+ });
266
+ }
267
+
268
+ // Validate timestamps if provided
269
+ if (sinceStr !== undefined && Number.isNaN(new Date(sinceStr).getTime())) {
270
+ throw new ValidationError("--since must be a valid ISO 8601 timestamp", {
271
+ field: "since",
272
+ value: sinceStr,
273
+ });
274
+ }
275
+ if (untilStr !== undefined && Number.isNaN(new Date(untilStr).getTime())) {
276
+ throw new ValidationError("--until must be a valid ISO 8601 timestamp", {
277
+ field: "until",
278
+ value: untilStr,
279
+ });
280
+ }
281
+
282
+ const cwd = process.cwd();
283
+ const config = await loadConfig(cwd);
284
+ const overstoryDir = join(config.project.root, ".overstory");
285
+
286
+ // Open event store
287
+ const eventsDbPath = join(overstoryDir, "events.db");
288
+ const eventsFile = Bun.file(eventsDbPath);
289
+ if (!(await eventsFile.exists())) {
290
+ if (json) {
291
+ process.stdout.write("[]\n");
292
+ } else {
293
+ process.stdout.write("No events data yet.\n");
294
+ }
295
+ return;
296
+ }
297
+
298
+ const eventStore = createEventStore(eventsDbPath);
299
+
300
+ try {
301
+ let events: StoredEvent[];
302
+ const queryOpts = { since: sinceStr, until: untilStr, limit };
303
+
304
+ if (runId) {
305
+ // Query by run ID
306
+ events = eventStore.getByRun(runId, queryOpts);
307
+ } else if (agentNames.length > 0) {
308
+ // Query each agent and merge
309
+ const allEvents: StoredEvent[] = [];
310
+ for (const name of agentNames) {
311
+ const agentEvents = eventStore.getByAgent(name, {
312
+ since: sinceStr,
313
+ until: untilStr,
314
+ });
315
+ allEvents.push(...agentEvents);
316
+ }
317
+ // Sort by createdAt chronologically
318
+ allEvents.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
319
+ // Apply limit after merge
320
+ events = allEvents.slice(0, limit);
321
+ } else {
322
+ // Default: try current-run.txt, then fall back to 24h timeline
323
+ const currentRunPath = join(overstoryDir, "current-run.txt");
324
+ const currentRunFile = Bun.file(currentRunPath);
325
+ if (await currentRunFile.exists()) {
326
+ const currentRunId = (await currentRunFile.text()).trim();
327
+ if (currentRunId) {
328
+ events = eventStore.getByRun(currentRunId, queryOpts);
329
+ } else {
330
+ // Empty file, fall back to timeline
331
+ const since24h = sinceStr ?? new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
332
+ events = eventStore.getTimeline({
333
+ since: since24h,
334
+ until: untilStr,
335
+ limit,
336
+ });
337
+ }
338
+ } else {
339
+ // No current run file, fall back to 24h timeline
340
+ const since24h = sinceStr ?? new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
341
+ events = eventStore.getTimeline({
342
+ since: since24h,
343
+ until: untilStr,
344
+ limit,
345
+ });
346
+ }
347
+ }
348
+
349
+ if (json) {
350
+ process.stdout.write(`${JSON.stringify(events)}\n`);
351
+ return;
352
+ }
353
+
354
+ // Use absolute time if --since is specified, relative otherwise
355
+ const useAbsoluteTime = sinceStr !== undefined;
356
+ printReplay(events, useAbsoluteTime);
357
+ } finally {
358
+ eventStore.close();
359
+ }
360
+ }