@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,431 @@
1
+ /**
2
+ * CLI command: overstory inspect <agent-name>
3
+ *
4
+ * Deep per-agent inspection aggregating data from EventStore, SessionStore,
5
+ * MetricsStore, and tmux capture-pane.
6
+ */
7
+
8
+ import { join } from "node:path";
9
+ import { loadConfig } from "../config.ts";
10
+ import { ValidationError } from "../errors.ts";
11
+ import { createEventStore } from "../events/store.ts";
12
+ import { color } from "../logging/color.ts";
13
+ import { createMetricsStore } from "../metrics/store.ts";
14
+ import { openSessionStore } from "../sessions/compat.ts";
15
+ import type { AgentSession, StoredEvent, ToolStats } from "../types.ts";
16
+
17
+ /**
18
+ * Parse a named flag value from args.
19
+ */
20
+ function getFlag(args: string[], flag: string): string | undefined {
21
+ const idx = args.indexOf(flag);
22
+ if (idx === -1 || idx + 1 >= args.length) {
23
+ return undefined;
24
+ }
25
+ return args[idx + 1];
26
+ }
27
+
28
+ function hasFlag(args: string[], flag: string): boolean {
29
+ return args.includes(flag);
30
+ }
31
+
32
+ /**
33
+ * Format a duration in ms to a human-readable string.
34
+ */
35
+ function formatDuration(ms: number): string {
36
+ const seconds = Math.floor(ms / 1000);
37
+ if (seconds < 60) return `${seconds}s`;
38
+ const minutes = Math.floor(seconds / 60);
39
+ const remainSec = seconds % 60;
40
+ if (minutes < 60) return `${minutes}m ${remainSec}s`;
41
+ const hours = Math.floor(minutes / 60);
42
+ const remainMin = minutes % 60;
43
+ return `${hours}h ${remainMin}m`;
44
+ }
45
+
46
+ /**
47
+ * Get colored state icon based on agent state.
48
+ */
49
+ function getStateIcon(state: AgentSession["state"]): string {
50
+ switch (state) {
51
+ case "booting":
52
+ return `${color.yellow}⏳${color.reset}`; // Yellow hourglass
53
+ case "working":
54
+ return `${color.green}●${color.reset}`; // Green circle
55
+ case "stalled":
56
+ return `${color.yellow}⚠${color.reset}`; // Yellow warning
57
+ case "completed":
58
+ return `${color.blue}✓${color.reset}`; // Blue checkmark
59
+ case "zombie":
60
+ return `${color.red}☠${color.reset}`; // Red skull
61
+ default:
62
+ return "?";
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Extract current file from most recent Edit/Write/Read tool_start event.
68
+ */
69
+ function extractCurrentFile(events: StoredEvent[]): string | null {
70
+ // Scan backwards for tool_start events with Edit/Write/Read
71
+ const fileTools = ["Edit", "Write", "Read"];
72
+ for (let i = events.length - 1; i >= 0; i--) {
73
+ const event = events[i];
74
+ if (
75
+ event &&
76
+ event.eventType === "tool_start" &&
77
+ event.toolName &&
78
+ fileTools.includes(event.toolName) &&
79
+ event.toolArgs
80
+ ) {
81
+ try {
82
+ const args = JSON.parse(event.toolArgs) as Record<string, unknown>;
83
+ const filePath = (args.file_path as string) ?? (args.path as string);
84
+ if (filePath) {
85
+ return filePath;
86
+ }
87
+ } catch {
88
+ // Failed to parse JSON, continue
89
+ }
90
+ }
91
+ }
92
+ return null;
93
+ }
94
+
95
+ /**
96
+ * Summarize tool arguments for display (truncate long values).
97
+ */
98
+ function summarizeArgs(toolArgs: string | null): string {
99
+ if (!toolArgs) return "";
100
+ try {
101
+ const parsed = JSON.parse(toolArgs) as Record<string, unknown>;
102
+ const entries = Object.entries(parsed)
103
+ .map(([key, value]) => {
104
+ const str = String(value);
105
+ return `${key}=${str.length > 40 ? `${str.slice(0, 37)}...` : str}`;
106
+ })
107
+ .join(", ");
108
+ return entries.length > 100 ? `${entries.slice(0, 97)}...` : entries;
109
+ } catch {
110
+ return toolArgs.length > 100 ? `${toolArgs.slice(0, 97)}...` : toolArgs;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Capture tmux pane output.
116
+ */
117
+ async function captureTmux(sessionName: string, lines: number): Promise<string | null> {
118
+ try {
119
+ const proc = Bun.spawn(["tmux", "capture-pane", "-t", sessionName, "-p", "-S", `-${lines}`], {
120
+ stdout: "pipe",
121
+ stderr: "pipe",
122
+ });
123
+ const exitCode = await proc.exited;
124
+ if (exitCode !== 0) {
125
+ return null;
126
+ }
127
+ const output = await new Response(proc.stdout).text();
128
+ return output.trim();
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
133
+
134
+ export interface InspectData {
135
+ session: AgentSession;
136
+ timeSinceLastActivity: number;
137
+ recentToolCalls: Array<{
138
+ toolName: string;
139
+ args: string;
140
+ durationMs: number | null;
141
+ timestamp: string;
142
+ }>;
143
+ currentFile: string | null;
144
+ toolStats: ToolStats[];
145
+ tokenUsage: {
146
+ inputTokens: number;
147
+ outputTokens: number;
148
+ cacheReadTokens: number;
149
+ cacheCreationTokens: number;
150
+ estimatedCostUsd: number | null;
151
+ modelUsed: string | null;
152
+ } | null;
153
+ tmuxOutput: string | null;
154
+ }
155
+
156
+ /**
157
+ * Gather all inspection data for an agent.
158
+ */
159
+ export async function gatherInspectData(
160
+ root: string,
161
+ agentName: string,
162
+ opts: {
163
+ limit?: number;
164
+ noTmux?: boolean;
165
+ tmuxLines?: number;
166
+ } = {},
167
+ ): Promise<InspectData> {
168
+ const overstoryDir = join(root, ".overstory");
169
+ const { store } = openSessionStore(overstoryDir);
170
+
171
+ let session: AgentSession | null = null;
172
+ try {
173
+ session = store.getByName(agentName);
174
+ if (!session) {
175
+ throw new ValidationError(`Agent not found: ${agentName}`, {
176
+ field: "agent-name",
177
+ value: agentName,
178
+ });
179
+ }
180
+
181
+ const now = Date.now();
182
+ const timeSinceLastActivity = now - new Date(session.lastActivity).getTime();
183
+
184
+ // EventStore: recent tool calls and tool stats
185
+ let recentToolCalls: InspectData["recentToolCalls"] = [];
186
+ let currentFile: string | null = null;
187
+ let toolStats: ToolStats[] = [];
188
+
189
+ const eventsDbPath = join(overstoryDir, "events.db");
190
+ const eventsFile = Bun.file(eventsDbPath);
191
+ if (await eventsFile.exists()) {
192
+ const eventStore = createEventStore(eventsDbPath);
193
+ try {
194
+ // Get recent events for this agent
195
+ const events = eventStore.getByAgent(agentName, { limit: 200 });
196
+
197
+ // Extract current file from most recent Edit/Write/Read tool_start
198
+ currentFile = extractCurrentFile(events);
199
+
200
+ // Filter to tool_start events for recent tool calls display
201
+ const toolStartEvents = events.filter((e) => e.eventType === "tool_start");
202
+ const limit = opts.limit ?? 20;
203
+ recentToolCalls = toolStartEvents.slice(0, limit).map((event) => ({
204
+ toolName: event.toolName ?? "unknown",
205
+ args: summarizeArgs(event.toolArgs),
206
+ durationMs: event.toolDurationMs,
207
+ timestamp: event.createdAt,
208
+ }));
209
+
210
+ // Tool usage statistics
211
+ toolStats = eventStore.getToolStats({ agentName });
212
+ } finally {
213
+ eventStore.close();
214
+ }
215
+ }
216
+
217
+ // MetricsStore: token usage
218
+ let tokenUsage: InspectData["tokenUsage"] = null;
219
+ const metricsDbPath = join(overstoryDir, "metrics.db");
220
+ const metricsFile = Bun.file(metricsDbPath);
221
+ if (await metricsFile.exists()) {
222
+ const metricsStore = createMetricsStore(metricsDbPath);
223
+ try {
224
+ const sessions = metricsStore.getSessionsByAgent(agentName);
225
+ const mostRecent = sessions[0];
226
+ if (mostRecent) {
227
+ tokenUsage = {
228
+ inputTokens: mostRecent.inputTokens,
229
+ outputTokens: mostRecent.outputTokens,
230
+ cacheReadTokens: mostRecent.cacheReadTokens,
231
+ cacheCreationTokens: mostRecent.cacheCreationTokens,
232
+ estimatedCostUsd: mostRecent.estimatedCostUsd,
233
+ modelUsed: mostRecent.modelUsed,
234
+ };
235
+ }
236
+ } finally {
237
+ metricsStore.close();
238
+ }
239
+ }
240
+
241
+ // tmux capture
242
+ let tmuxOutput: string | null = null;
243
+ if (!opts.noTmux && session.tmuxSession) {
244
+ const lines = opts.tmuxLines ?? 30;
245
+ tmuxOutput = await captureTmux(session.tmuxSession, lines);
246
+ }
247
+
248
+ return {
249
+ session,
250
+ timeSinceLastActivity,
251
+ recentToolCalls,
252
+ currentFile,
253
+ toolStats,
254
+ tokenUsage,
255
+ tmuxOutput,
256
+ };
257
+ } finally {
258
+ store.close();
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Print inspection data in human-readable format.
264
+ */
265
+ export function printInspectData(data: InspectData): void {
266
+ const w = process.stdout.write.bind(process.stdout);
267
+ const { session } = data;
268
+
269
+ w(`\n🔍 Agent Inspection: ${session.agentName}\n`);
270
+ w(`${"═".repeat(80)}\n\n`);
271
+
272
+ // Agent state and metadata
273
+ const stateIcon = getStateIcon(session.state);
274
+ w(`${stateIcon} State: ${session.state}\n`);
275
+ w(`⏱ Last activity: ${formatDuration(data.timeSinceLastActivity)} ago\n`);
276
+ w(`🎯 Task: ${session.beadId}\n`);
277
+ w(`🔧 Capability: ${session.capability}\n`);
278
+ w(`🌿 Branch: ${session.branchName}\n`);
279
+ if (session.parentAgent) {
280
+ w(`👤 Parent: ${session.parentAgent} (depth: ${session.depth})\n`);
281
+ }
282
+ w(`📅 Started: ${session.startedAt}\n`);
283
+ w(`💻 Tmux: ${session.tmuxSession}\n`);
284
+ w("\n");
285
+
286
+ // Current file
287
+ if (data.currentFile) {
288
+ w(`📝 Current file: ${data.currentFile}\n\n`);
289
+ }
290
+
291
+ // Token usage
292
+ if (data.tokenUsage) {
293
+ w("💰 Token Usage\n");
294
+ w(`${"─".repeat(80)}\n`);
295
+ w(` Input: ${data.tokenUsage.inputTokens.toLocaleString()}\n`);
296
+ w(` Output: ${data.tokenUsage.outputTokens.toLocaleString()}\n`);
297
+ w(` Cache read: ${data.tokenUsage.cacheReadTokens.toLocaleString()}\n`);
298
+ w(` Cache created: ${data.tokenUsage.cacheCreationTokens.toLocaleString()}\n`);
299
+ if (data.tokenUsage.estimatedCostUsd !== null) {
300
+ w(` Estimated cost: $${data.tokenUsage.estimatedCostUsd.toFixed(4)}\n`);
301
+ }
302
+ if (data.tokenUsage.modelUsed) {
303
+ w(` Model: ${data.tokenUsage.modelUsed}\n`);
304
+ }
305
+ w("\n");
306
+ }
307
+
308
+ // Tool usage statistics (top 10)
309
+ if (data.toolStats.length > 0) {
310
+ w("🛠 Tool Usage (Top 10)\n");
311
+ w(`${"─".repeat(80)}\n`);
312
+ const top10 = data.toolStats.slice(0, 10);
313
+ for (const stat of top10) {
314
+ const avgMs = stat.avgDurationMs.toFixed(0);
315
+ w(` ${stat.toolName.padEnd(20)} ${String(stat.count).padStart(6)} calls `);
316
+ w(`avg: ${String(avgMs).padStart(6)}ms max: ${stat.maxDurationMs}ms\n`);
317
+ }
318
+ w("\n");
319
+ }
320
+
321
+ // Recent tool calls
322
+ if (data.recentToolCalls.length > 0) {
323
+ w(`📊 Recent Tool Calls (last ${data.recentToolCalls.length})\n`);
324
+ w(`${"─".repeat(80)}\n`);
325
+ for (const call of data.recentToolCalls) {
326
+ const time = new Date(call.timestamp).toLocaleTimeString();
327
+ const duration = call.durationMs !== null ? `${call.durationMs}ms` : "pending";
328
+ w(` [${time}] ${call.toolName.padEnd(15)} ${duration.padStart(10)}`);
329
+ if (call.args) {
330
+ w(` ${call.args}`);
331
+ }
332
+ w("\n");
333
+ }
334
+ w("\n");
335
+ }
336
+
337
+ // tmux output
338
+ if (data.tmuxOutput) {
339
+ w("📺 Live Tmux Output\n");
340
+ w(`${"─".repeat(80)}\n`);
341
+ w(`${data.tmuxOutput}\n`);
342
+ w(`${"─".repeat(80)}\n`);
343
+ }
344
+ }
345
+
346
+ const INSPECT_HELP = `overstory inspect <agent-name> — Deep inspection of a single agent
347
+
348
+ Usage: overstory inspect <agent-name> [options]
349
+
350
+ Options:
351
+ --json Output as JSON
352
+ --follow Poll and refresh (clears screen, re-gathers, re-prints)
353
+ --interval <ms> Polling interval for --follow in milliseconds (default: 3000, min: 500)
354
+ --limit <n> Number of recent tool calls to show (default: 20)
355
+ --no-tmux Skip tmux capture-pane
356
+ --help, -h Show this help
357
+
358
+ Examples:
359
+ overstory inspect builder-1
360
+ overstory inspect scout-alpha --json
361
+ overstory inspect builder-1 --follow --interval 2000`;
362
+
363
+ /**
364
+ * Entry point for `overstory inspect <agent-name>`.
365
+ */
366
+ export async function inspectCommand(args: string[]): Promise<void> {
367
+ if (args.includes("--help") || args.includes("-h")) {
368
+ process.stdout.write(`${INSPECT_HELP}\n`);
369
+ return;
370
+ }
371
+
372
+ const agentName = args[0];
373
+ if (!agentName) {
374
+ throw new ValidationError("Agent name is required", {
375
+ field: "agent-name",
376
+ });
377
+ }
378
+
379
+ const json = hasFlag(args, "--json");
380
+ const follow = hasFlag(args, "--follow");
381
+ const noTmux = hasFlag(args, "--no-tmux");
382
+
383
+ const intervalStr = getFlag(args, "--interval");
384
+ const interval = intervalStr ? Number.parseInt(intervalStr, 10) : 3000;
385
+ if (Number.isNaN(interval) || interval < 500) {
386
+ throw new ValidationError("--interval must be a number >= 500 (milliseconds)", {
387
+ field: "interval",
388
+ value: intervalStr,
389
+ });
390
+ }
391
+
392
+ const limitStr = getFlag(args, "--limit");
393
+ const limit = limitStr ? Number.parseInt(limitStr, 10) : 20;
394
+ if (Number.isNaN(limit) || limit < 1) {
395
+ throw new ValidationError("--limit must be a number >= 1", {
396
+ field: "limit",
397
+ value: limitStr,
398
+ });
399
+ }
400
+
401
+ const cwd = process.cwd();
402
+ const config = await loadConfig(cwd);
403
+ const root = config.project.root;
404
+
405
+ if (follow) {
406
+ // Polling loop
407
+ while (true) {
408
+ // Clear screen
409
+ process.stdout.write("\x1b[2J\x1b[H");
410
+ const data = await gatherInspectData(root, agentName, {
411
+ limit,
412
+ noTmux,
413
+ tmuxLines: 30,
414
+ });
415
+ if (json) {
416
+ process.stdout.write(`${JSON.stringify(data, null, "\t")}\n`);
417
+ } else {
418
+ printInspectData(data);
419
+ }
420
+ await Bun.sleep(interval);
421
+ }
422
+ } else {
423
+ // Single snapshot
424
+ const data = await gatherInspectData(root, agentName, { limit, noTmux, tmuxLines: 30 });
425
+ if (json) {
426
+ process.stdout.write(`${JSON.stringify(data, null, "\t")}\n`);
427
+ } else {
428
+ printInspectData(data);
429
+ }
430
+ }
431
+ }