@katyella/legio 0.1.0

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 (219) hide show
  1. package/CHANGELOG.md +422 -0
  2. package/LICENSE +21 -0
  3. package/README.md +555 -0
  4. package/agents/builder.md +141 -0
  5. package/agents/coordinator.md +351 -0
  6. package/agents/cto.md +196 -0
  7. package/agents/gateway.md +276 -0
  8. package/agents/lead.md +281 -0
  9. package/agents/merger.md +156 -0
  10. package/agents/monitor.md +212 -0
  11. package/agents/reviewer.md +142 -0
  12. package/agents/scout.md +131 -0
  13. package/agents/supervisor.md +416 -0
  14. package/bin/legio.mjs +38 -0
  15. package/package.json +77 -0
  16. package/src/agents/checkpoint.test.ts +88 -0
  17. package/src/agents/checkpoint.ts +102 -0
  18. package/src/agents/hooks-deployer.test.ts +1820 -0
  19. package/src/agents/hooks-deployer.ts +574 -0
  20. package/src/agents/identity.test.ts +614 -0
  21. package/src/agents/identity.ts +385 -0
  22. package/src/agents/lifecycle.test.ts +202 -0
  23. package/src/agents/lifecycle.ts +184 -0
  24. package/src/agents/manifest.test.ts +558 -0
  25. package/src/agents/manifest.ts +297 -0
  26. package/src/agents/overlay.test.ts +592 -0
  27. package/src/agents/overlay.ts +316 -0
  28. package/src/beads/client.test.ts +210 -0
  29. package/src/beads/client.ts +227 -0
  30. package/src/beads/molecules.test.ts +320 -0
  31. package/src/beads/molecules.ts +209 -0
  32. package/src/commands/agents.test.ts +325 -0
  33. package/src/commands/agents.ts +286 -0
  34. package/src/commands/clean.test.ts +730 -0
  35. package/src/commands/clean.ts +653 -0
  36. package/src/commands/completions.test.ts +346 -0
  37. package/src/commands/completions.ts +950 -0
  38. package/src/commands/coordinator.test.ts +1524 -0
  39. package/src/commands/coordinator.ts +880 -0
  40. package/src/commands/costs.test.ts +1015 -0
  41. package/src/commands/costs.ts +473 -0
  42. package/src/commands/dashboard.test.ts +94 -0
  43. package/src/commands/dashboard.ts +607 -0
  44. package/src/commands/doctor.test.ts +295 -0
  45. package/src/commands/doctor.ts +213 -0
  46. package/src/commands/down.test.ts +308 -0
  47. package/src/commands/down.ts +124 -0
  48. package/src/commands/errors.test.ts +648 -0
  49. package/src/commands/errors.ts +255 -0
  50. package/src/commands/feed.test.ts +579 -0
  51. package/src/commands/feed.ts +368 -0
  52. package/src/commands/gateway.test.ts +698 -0
  53. package/src/commands/gateway.ts +419 -0
  54. package/src/commands/group.test.ts +262 -0
  55. package/src/commands/group.ts +539 -0
  56. package/src/commands/hooks.test.ts +292 -0
  57. package/src/commands/hooks.ts +210 -0
  58. package/src/commands/init.test.ts +211 -0
  59. package/src/commands/init.ts +622 -0
  60. package/src/commands/inspect.test.ts +670 -0
  61. package/src/commands/inspect.ts +455 -0
  62. package/src/commands/log.test.ts +1556 -0
  63. package/src/commands/log.ts +752 -0
  64. package/src/commands/logs.test.ts +379 -0
  65. package/src/commands/logs.ts +544 -0
  66. package/src/commands/mail.test.ts +1726 -0
  67. package/src/commands/mail.ts +926 -0
  68. package/src/commands/merge.test.ts +676 -0
  69. package/src/commands/merge.ts +374 -0
  70. package/src/commands/metrics.test.ts +444 -0
  71. package/src/commands/metrics.ts +150 -0
  72. package/src/commands/monitor.test.ts +151 -0
  73. package/src/commands/monitor.ts +394 -0
  74. package/src/commands/nudge.test.ts +230 -0
  75. package/src/commands/nudge.ts +373 -0
  76. package/src/commands/prime.test.ts +467 -0
  77. package/src/commands/prime.ts +386 -0
  78. package/src/commands/replay.test.ts +742 -0
  79. package/src/commands/replay.ts +367 -0
  80. package/src/commands/run.test.ts +443 -0
  81. package/src/commands/run.ts +365 -0
  82. package/src/commands/server.test.ts +626 -0
  83. package/src/commands/server.ts +298 -0
  84. package/src/commands/sling.test.ts +810 -0
  85. package/src/commands/sling.ts +700 -0
  86. package/src/commands/spec.test.ts +206 -0
  87. package/src/commands/spec.ts +171 -0
  88. package/src/commands/status.test.ts +276 -0
  89. package/src/commands/status.ts +339 -0
  90. package/src/commands/stop.test.ts +357 -0
  91. package/src/commands/stop.ts +119 -0
  92. package/src/commands/supervisor.test.ts +186 -0
  93. package/src/commands/supervisor.ts +544 -0
  94. package/src/commands/trace.test.ts +746 -0
  95. package/src/commands/trace.ts +332 -0
  96. package/src/commands/up.test.ts +597 -0
  97. package/src/commands/up.ts +275 -0
  98. package/src/commands/watch.test.ts +152 -0
  99. package/src/commands/watch.ts +238 -0
  100. package/src/commands/worktree.test.ts +648 -0
  101. package/src/commands/worktree.ts +266 -0
  102. package/src/config.test.ts +496 -0
  103. package/src/config.ts +616 -0
  104. package/src/doctor/agents.test.ts +448 -0
  105. package/src/doctor/agents.ts +396 -0
  106. package/src/doctor/config-check.test.ts +184 -0
  107. package/src/doctor/config-check.ts +185 -0
  108. package/src/doctor/consistency.test.ts +645 -0
  109. package/src/doctor/consistency.ts +294 -0
  110. package/src/doctor/databases.test.ts +284 -0
  111. package/src/doctor/databases.ts +211 -0
  112. package/src/doctor/dependencies.test.ts +150 -0
  113. package/src/doctor/dependencies.ts +179 -0
  114. package/src/doctor/logs.test.ts +244 -0
  115. package/src/doctor/logs.ts +295 -0
  116. package/src/doctor/merge-queue.test.ts +210 -0
  117. package/src/doctor/merge-queue.ts +144 -0
  118. package/src/doctor/structure.test.ts +285 -0
  119. package/src/doctor/structure.ts +195 -0
  120. package/src/doctor/types.ts +37 -0
  121. package/src/doctor/version.test.ts +130 -0
  122. package/src/doctor/version.ts +131 -0
  123. package/src/e2e/chat-flow.test.ts +346 -0
  124. package/src/e2e/init-sling-lifecycle.test.ts +288 -0
  125. package/src/errors.test.ts +21 -0
  126. package/src/errors.ts +246 -0
  127. package/src/events/store.test.ts +660 -0
  128. package/src/events/store.ts +344 -0
  129. package/src/events/tool-filter.test.ts +330 -0
  130. package/src/events/tool-filter.ts +126 -0
  131. package/src/global-setup.ts +14 -0
  132. package/src/index.ts +339 -0
  133. package/src/insights/analyzer.test.ts +466 -0
  134. package/src/insights/analyzer.ts +203 -0
  135. package/src/logging/color.test.ts +118 -0
  136. package/src/logging/color.ts +71 -0
  137. package/src/logging/logger.test.ts +812 -0
  138. package/src/logging/logger.ts +266 -0
  139. package/src/logging/reporter.test.ts +258 -0
  140. package/src/logging/reporter.ts +109 -0
  141. package/src/logging/sanitizer.test.ts +190 -0
  142. package/src/logging/sanitizer.ts +57 -0
  143. package/src/mail/broadcast.test.ts +203 -0
  144. package/src/mail/broadcast.ts +92 -0
  145. package/src/mail/client.test.ts +873 -0
  146. package/src/mail/client.ts +236 -0
  147. package/src/mail/store.test.ts +815 -0
  148. package/src/mail/store.ts +402 -0
  149. package/src/merge/queue.test.ts +449 -0
  150. package/src/merge/queue.ts +262 -0
  151. package/src/merge/resolver.test.ts +1453 -0
  152. package/src/merge/resolver.ts +759 -0
  153. package/src/metrics/store.test.ts +1167 -0
  154. package/src/metrics/store.ts +511 -0
  155. package/src/metrics/summary.test.ts +397 -0
  156. package/src/metrics/summary.ts +178 -0
  157. package/src/metrics/transcript.test.ts +643 -0
  158. package/src/metrics/transcript.ts +351 -0
  159. package/src/mulch/client.test.ts +547 -0
  160. package/src/mulch/client.ts +416 -0
  161. package/src/server/audit-store.test.ts +384 -0
  162. package/src/server/audit-store.ts +257 -0
  163. package/src/server/headless.test.ts +180 -0
  164. package/src/server/headless.ts +151 -0
  165. package/src/server/index.test.ts +241 -0
  166. package/src/server/index.ts +317 -0
  167. package/src/server/public/app.js +187 -0
  168. package/src/server/public/apple-touch-icon.png +0 -0
  169. package/src/server/public/components/agent-badge.js +37 -0
  170. package/src/server/public/components/data-table.js +114 -0
  171. package/src/server/public/components/gateway-chat.js +256 -0
  172. package/src/server/public/components/issue-card.js +96 -0
  173. package/src/server/public/components/layout.js +88 -0
  174. package/src/server/public/components/message-bubble.js +120 -0
  175. package/src/server/public/components/stat-card.js +26 -0
  176. package/src/server/public/components/terminal-panel.js +140 -0
  177. package/src/server/public/favicon-16.png +0 -0
  178. package/src/server/public/favicon-32.png +0 -0
  179. package/src/server/public/favicon.ico +0 -0
  180. package/src/server/public/favicon.png +0 -0
  181. package/src/server/public/index.html +64 -0
  182. package/src/server/public/lib/api.js +35 -0
  183. package/src/server/public/lib/markdown.js +8 -0
  184. package/src/server/public/lib/preact-setup.js +8 -0
  185. package/src/server/public/lib/state.js +99 -0
  186. package/src/server/public/lib/utils.js +309 -0
  187. package/src/server/public/lib/ws.js +79 -0
  188. package/src/server/public/views/chat.js +983 -0
  189. package/src/server/public/views/costs.js +692 -0
  190. package/src/server/public/views/dashboard.js +781 -0
  191. package/src/server/public/views/gateway-chat.js +622 -0
  192. package/src/server/public/views/inspect.js +399 -0
  193. package/src/server/public/views/issues.js +470 -0
  194. package/src/server/public/views/setup.js +94 -0
  195. package/src/server/public/views/task-detail.js +422 -0
  196. package/src/server/routes.test.ts +3816 -0
  197. package/src/server/routes.ts +1964 -0
  198. package/src/server/websocket.test.ts +288 -0
  199. package/src/server/websocket.ts +196 -0
  200. package/src/sessions/compat.test.ts +109 -0
  201. package/src/sessions/compat.ts +17 -0
  202. package/src/sessions/store.test.ts +969 -0
  203. package/src/sessions/store.ts +480 -0
  204. package/src/test-helpers.test.ts +97 -0
  205. package/src/test-helpers.ts +143 -0
  206. package/src/types.ts +708 -0
  207. package/src/watchdog/daemon.test.ts +1233 -0
  208. package/src/watchdog/daemon.ts +533 -0
  209. package/src/watchdog/health.test.ts +371 -0
  210. package/src/watchdog/health.ts +248 -0
  211. package/src/watchdog/triage.test.ts +162 -0
  212. package/src/watchdog/triage.ts +193 -0
  213. package/src/worktree/manager.test.ts +444 -0
  214. package/src/worktree/manager.ts +224 -0
  215. package/src/worktree/tmux.test.ts +1238 -0
  216. package/src/worktree/tmux.ts +644 -0
  217. package/templates/CLAUDE.md.tmpl +89 -0
  218. package/templates/hooks.json.tmpl +132 -0
  219. package/templates/overlay.md.tmpl +79 -0
@@ -0,0 +1,455 @@
1
+ /**
2
+ * CLI command: legio inspect <agent-name>
3
+ *
4
+ * Deep per-agent inspection aggregating data from EventStore, SessionStore,
5
+ * MetricsStore, and tmux capture-pane.
6
+ */
7
+
8
+ import { spawn } from "node:child_process";
9
+ import { access } from "node:fs/promises";
10
+ import { join } from "node:path";
11
+ import { loadConfig } from "../config.ts";
12
+ import { ValidationError } from "../errors.ts";
13
+ import { createEventStore } from "../events/store.ts";
14
+ import { color } from "../logging/color.ts";
15
+ import { createMetricsStore } from "../metrics/store.ts";
16
+ import { openSessionStore } from "../sessions/compat.ts";
17
+ import type { AgentSession, StoredEvent, ToolStats } from "../types.ts";
18
+ import { readTerminalLog } from "../worktree/tmux.ts";
19
+
20
+ /**
21
+ * Parse a named flag value from args.
22
+ */
23
+ function getFlag(args: string[], flag: string): string | undefined {
24
+ const idx = args.indexOf(flag);
25
+ if (idx === -1 || idx + 1 >= args.length) {
26
+ return undefined;
27
+ }
28
+ return args[idx + 1];
29
+ }
30
+
31
+ function hasFlag(args: string[], flag: string): boolean {
32
+ return args.includes(flag);
33
+ }
34
+
35
+ /**
36
+ * Format a duration in ms to a human-readable string.
37
+ */
38
+ function formatDuration(ms: number): string {
39
+ const seconds = Math.floor(ms / 1000);
40
+ if (seconds < 60) return `${seconds}s`;
41
+ const minutes = Math.floor(seconds / 60);
42
+ const remainSec = seconds % 60;
43
+ if (minutes < 60) return `${minutes}m ${remainSec}s`;
44
+ const hours = Math.floor(minutes / 60);
45
+ const remainMin = minutes % 60;
46
+ return `${hours}h ${remainMin}m`;
47
+ }
48
+
49
+ /**
50
+ * Get colored state icon based on agent state.
51
+ */
52
+ function getStateIcon(state: AgentSession["state"]): string {
53
+ switch (state) {
54
+ case "booting":
55
+ return `${color.yellow}⏳${color.reset}`; // Yellow hourglass
56
+ case "working":
57
+ return `${color.green}●${color.reset}`; // Green circle
58
+ case "completed":
59
+ return `${color.blue}✓${color.reset}`; // Blue checkmark
60
+ case "zombie":
61
+ return `${color.red}☠${color.reset}`; // Red skull
62
+ default:
63
+ return "?";
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Extract current file from most recent Edit/Write/Read tool_start event.
69
+ */
70
+ function extractCurrentFile(events: StoredEvent[]): string | null {
71
+ // Scan backwards for tool_start events with Edit/Write/Read
72
+ const fileTools = ["Edit", "Write", "Read"];
73
+ for (let i = events.length - 1; i >= 0; i--) {
74
+ const event = events[i];
75
+ if (
76
+ event &&
77
+ event.eventType === "tool_start" &&
78
+ event.toolName &&
79
+ fileTools.includes(event.toolName) &&
80
+ event.toolArgs
81
+ ) {
82
+ try {
83
+ const args = JSON.parse(event.toolArgs) as Record<string, unknown>;
84
+ const filePath = (args.file_path as string) ?? (args.path as string);
85
+ if (filePath) {
86
+ return filePath;
87
+ }
88
+ } catch {
89
+ // Failed to parse JSON, continue
90
+ }
91
+ }
92
+ }
93
+ return null;
94
+ }
95
+
96
+ /**
97
+ * Summarize tool arguments for display (truncate long values).
98
+ */
99
+ function summarizeArgs(toolArgs: string | null): string {
100
+ if (!toolArgs) return "";
101
+ try {
102
+ const parsed = JSON.parse(toolArgs) as Record<string, unknown>;
103
+ const entries = Object.entries(parsed)
104
+ .map(([key, value]) => {
105
+ const str = String(value);
106
+ return `${key}=${str.length > 40 ? `${str.slice(0, 37)}...` : str}`;
107
+ })
108
+ .join(", ");
109
+ return entries.length > 100 ? `${entries.slice(0, 97)}...` : entries;
110
+ } catch {
111
+ return toolArgs.length > 100 ? `${toolArgs.slice(0, 97)}...` : toolArgs;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Capture tmux pane output.
117
+ */
118
+ async function captureTmux(sessionName: string, lines: number): Promise<string | null> {
119
+ try {
120
+ const result = await new Promise<{ exitCode: number; stdout: string }>((resolve) => {
121
+ const proc = spawn("tmux", ["capture-pane", "-t", sessionName, "-p", "-S", `-${lines}`], {
122
+ stdio: ["ignore", "pipe", "pipe"],
123
+ });
124
+ const stdoutChunks: Buffer[] = [];
125
+ proc.stdout?.on("data", (chunk: Buffer) => stdoutChunks.push(chunk));
126
+ proc.on("close", (code: number | null) => {
127
+ resolve({ exitCode: code ?? 1, stdout: Buffer.concat(stdoutChunks).toString("utf-8") });
128
+ });
129
+ });
130
+ if (result.exitCode !== 0) {
131
+ return null;
132
+ }
133
+ return result.stdout.trim();
134
+ } catch {
135
+ return null;
136
+ }
137
+ }
138
+
139
+ export interface InspectData {
140
+ session: AgentSession;
141
+ timeSinceLastActivity: number;
142
+ recentToolCalls: Array<{
143
+ toolName: string;
144
+ args: string;
145
+ durationMs: number | null;
146
+ timestamp: string;
147
+ }>;
148
+ currentFile: string | null;
149
+ toolStats: ToolStats[];
150
+ tokenUsage: {
151
+ inputTokens: number;
152
+ outputTokens: number;
153
+ cacheReadTokens: number;
154
+ cacheCreationTokens: number;
155
+ estimatedCostUsd: number | null;
156
+ modelUsed: string | null;
157
+ } | null;
158
+ tmuxOutput: string | null;
159
+ }
160
+
161
+ /**
162
+ * Gather all inspection data for an agent.
163
+ */
164
+ export async function gatherInspectData(
165
+ root: string,
166
+ agentName: string,
167
+ opts: {
168
+ limit?: number;
169
+ noTmux?: boolean;
170
+ tmuxLines?: number;
171
+ } = {},
172
+ ): Promise<InspectData> {
173
+ const legioDir = join(root, ".legio");
174
+ const { store } = openSessionStore(legioDir);
175
+
176
+ let session: AgentSession | null = null;
177
+ try {
178
+ session = store.getByName(agentName);
179
+ if (!session) {
180
+ throw new ValidationError(`Agent not found: ${agentName}`, {
181
+ field: "agent-name",
182
+ value: agentName,
183
+ });
184
+ }
185
+
186
+ const now = Date.now();
187
+ const timeSinceLastActivity = now - new Date(session.lastActivity).getTime();
188
+
189
+ // EventStore: recent tool calls and tool stats
190
+ let recentToolCalls: InspectData["recentToolCalls"] = [];
191
+ let currentFile: string | null = null;
192
+ let toolStats: ToolStats[] = [];
193
+
194
+ const eventsDbPath = join(legioDir, "events.db");
195
+ let eventsDbExists = false;
196
+ try {
197
+ await access(eventsDbPath);
198
+ eventsDbExists = true;
199
+ } catch {
200
+ /* not found */
201
+ }
202
+ if (eventsDbExists) {
203
+ const eventStore = createEventStore(eventsDbPath);
204
+ try {
205
+ // Get recent events for this agent
206
+ const events = eventStore.getByAgent(agentName, { limit: 200 });
207
+
208
+ // Extract current file from most recent Edit/Write/Read tool_start
209
+ currentFile = extractCurrentFile(events);
210
+
211
+ // Filter to tool_start events for recent tool calls display
212
+ const toolStartEvents = events.filter((e) => e.eventType === "tool_start");
213
+ const limit = opts.limit ?? 20;
214
+ recentToolCalls = toolStartEvents.slice(0, limit).map((event) => ({
215
+ toolName: event.toolName ?? "unknown",
216
+ args: summarizeArgs(event.toolArgs),
217
+ durationMs: event.toolDurationMs,
218
+ timestamp: event.createdAt,
219
+ }));
220
+
221
+ // Tool usage statistics
222
+ toolStats = eventStore.getToolStats({ agentName });
223
+ } finally {
224
+ eventStore.close();
225
+ }
226
+ }
227
+
228
+ // MetricsStore: token usage
229
+ let tokenUsage: InspectData["tokenUsage"] = null;
230
+ const metricsDbPath = join(legioDir, "metrics.db");
231
+ let metricsDbExists = false;
232
+ try {
233
+ await access(metricsDbPath);
234
+ metricsDbExists = true;
235
+ } catch {
236
+ /* not found */
237
+ }
238
+ if (metricsDbExists) {
239
+ const metricsStore = createMetricsStore(metricsDbPath);
240
+ try {
241
+ const sessions = metricsStore.getSessionsByAgent(agentName);
242
+ const mostRecent = sessions[0];
243
+ if (mostRecent) {
244
+ tokenUsage = {
245
+ inputTokens: mostRecent.inputTokens,
246
+ outputTokens: mostRecent.outputTokens,
247
+ cacheReadTokens: mostRecent.cacheReadTokens,
248
+ cacheCreationTokens: mostRecent.cacheCreationTokens,
249
+ estimatedCostUsd: mostRecent.estimatedCostUsd,
250
+ modelUsed: mostRecent.modelUsed,
251
+ };
252
+ }
253
+ } finally {
254
+ metricsStore.close();
255
+ }
256
+ }
257
+
258
+ // Terminal output: prefer terminal log file, fall back to capture-pane
259
+ let tmuxOutput: string | null = null;
260
+ if (!opts.noTmux) {
261
+ const lines = opts.tmuxLines ?? 30;
262
+ // Try terminal log file first (pipe-pane streaming)
263
+ if (session.terminalLogPath) {
264
+ tmuxOutput = await readTerminalLog(session.terminalLogPath, lines);
265
+ }
266
+ // Fall back to capture-pane if no log file exists
267
+ if (tmuxOutput === null && session.tmuxSession) {
268
+ tmuxOutput = await captureTmux(session.tmuxSession, lines);
269
+ }
270
+ }
271
+
272
+ return {
273
+ session,
274
+ timeSinceLastActivity,
275
+ recentToolCalls,
276
+ currentFile,
277
+ toolStats,
278
+ tokenUsage,
279
+ tmuxOutput,
280
+ };
281
+ } finally {
282
+ store.close();
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Print inspection data in human-readable format.
288
+ */
289
+ export function printInspectData(data: InspectData): void {
290
+ const w = process.stdout.write.bind(process.stdout);
291
+ const { session } = data;
292
+
293
+ w(`\n🔍 Agent Inspection: ${session.agentName}\n`);
294
+ w(`${"═".repeat(80)}\n\n`);
295
+
296
+ // Agent state and metadata
297
+ const stateIcon = getStateIcon(session.state);
298
+ w(`${stateIcon} State: ${session.state}\n`);
299
+ w(`⏱ Last activity: ${formatDuration(data.timeSinceLastActivity)} ago\n`);
300
+ w(`🎯 Task: ${session.beadId}\n`);
301
+ w(`🔧 Capability: ${session.capability}\n`);
302
+ w(`🌿 Branch: ${session.branchName}\n`);
303
+ if (session.parentAgent) {
304
+ w(`👤 Parent: ${session.parentAgent} (depth: ${session.depth})\n`);
305
+ }
306
+ w(`📅 Started: ${session.startedAt}\n`);
307
+ w(`💻 Tmux: ${session.tmuxSession}\n`);
308
+ w("\n");
309
+
310
+ // Current file
311
+ if (data.currentFile) {
312
+ w(`📝 Current file: ${data.currentFile}\n\n`);
313
+ }
314
+
315
+ // Token usage
316
+ if (data.tokenUsage) {
317
+ w("💰 Token Usage\n");
318
+ w(`${"─".repeat(80)}\n`);
319
+ w(` Input: ${data.tokenUsage.inputTokens.toLocaleString()}\n`);
320
+ w(` Output: ${data.tokenUsage.outputTokens.toLocaleString()}\n`);
321
+ w(` Cache read: ${data.tokenUsage.cacheReadTokens.toLocaleString()}\n`);
322
+ w(` Cache created: ${data.tokenUsage.cacheCreationTokens.toLocaleString()}\n`);
323
+ if (data.tokenUsage.estimatedCostUsd !== null) {
324
+ w(` Estimated cost: $${data.tokenUsage.estimatedCostUsd.toFixed(4)}\n`);
325
+ }
326
+ if (data.tokenUsage.modelUsed) {
327
+ w(` Model: ${data.tokenUsage.modelUsed}\n`);
328
+ }
329
+ w("\n");
330
+ }
331
+
332
+ // Tool usage statistics (top 10)
333
+ if (data.toolStats.length > 0) {
334
+ w("🛠 Tool Usage (Top 10)\n");
335
+ w(`${"─".repeat(80)}\n`);
336
+ const top10 = data.toolStats.slice(0, 10);
337
+ for (const stat of top10) {
338
+ const avgMs = stat.avgDurationMs.toFixed(0);
339
+ w(` ${stat.toolName.padEnd(20)} ${String(stat.count).padStart(6)} calls `);
340
+ w(`avg: ${String(avgMs).padStart(6)}ms max: ${stat.maxDurationMs}ms\n`);
341
+ }
342
+ w("\n");
343
+ }
344
+
345
+ // Recent tool calls
346
+ if (data.recentToolCalls.length > 0) {
347
+ w(`📊 Recent Tool Calls (last ${data.recentToolCalls.length})\n`);
348
+ w(`${"─".repeat(80)}\n`);
349
+ for (const call of data.recentToolCalls) {
350
+ const time = new Date(call.timestamp).toLocaleTimeString();
351
+ const duration = call.durationMs !== null ? `${call.durationMs}ms` : "pending";
352
+ w(` [${time}] ${call.toolName.padEnd(15)} ${duration.padStart(10)}`);
353
+ if (call.args) {
354
+ w(` ${call.args}`);
355
+ }
356
+ w("\n");
357
+ }
358
+ w("\n");
359
+ }
360
+
361
+ // tmux output
362
+ if (data.tmuxOutput) {
363
+ w("📺 Live Tmux Output\n");
364
+ w(`${"─".repeat(80)}\n`);
365
+ w(`${data.tmuxOutput}\n`);
366
+ w(`${"─".repeat(80)}\n`);
367
+ }
368
+ }
369
+
370
+ const INSPECT_HELP = `legio inspect <agent-name> — Deep inspection of a single agent
371
+
372
+ Usage: legio inspect <agent-name> [options]
373
+
374
+ Options:
375
+ --json Output as JSON
376
+ --follow Poll and refresh (clears screen, re-gathers, re-prints)
377
+ --interval <ms> Polling interval for --follow in milliseconds (default: 3000, min: 500)
378
+ --limit <n> Number of recent tool calls to show (default: 20)
379
+ --no-tmux Skip tmux capture-pane
380
+ --help, -h Show this help
381
+
382
+ Examples:
383
+ legio inspect builder-1
384
+ legio inspect scout-alpha --json
385
+ legio inspect builder-1 --follow --interval 2000`;
386
+
387
+ /**
388
+ * Entry point for `legio inspect <agent-name>`.
389
+ */
390
+ export async function inspectCommand(args: string[]): Promise<void> {
391
+ if (args.includes("--help") || args.includes("-h")) {
392
+ process.stdout.write(`${INSPECT_HELP}\n`);
393
+ return;
394
+ }
395
+
396
+ const agentName = args[0];
397
+ if (!agentName) {
398
+ throw new ValidationError("Agent name is required", {
399
+ field: "agent-name",
400
+ });
401
+ }
402
+
403
+ const json = hasFlag(args, "--json");
404
+ const follow = hasFlag(args, "--follow");
405
+ const noTmux = hasFlag(args, "--no-tmux");
406
+
407
+ const intervalStr = getFlag(args, "--interval");
408
+ const interval = intervalStr ? Number.parseInt(intervalStr, 10) : 3000;
409
+ if (Number.isNaN(interval) || interval < 500) {
410
+ throw new ValidationError("--interval must be a number >= 500 (milliseconds)", {
411
+ field: "interval",
412
+ value: intervalStr,
413
+ });
414
+ }
415
+
416
+ const limitStr = getFlag(args, "--limit");
417
+ const limit = limitStr ? Number.parseInt(limitStr, 10) : 20;
418
+ if (Number.isNaN(limit) || limit < 1) {
419
+ throw new ValidationError("--limit must be a number >= 1", {
420
+ field: "limit",
421
+ value: limitStr,
422
+ });
423
+ }
424
+
425
+ const cwd = process.cwd();
426
+ const config = await loadConfig(cwd);
427
+ const root = config.project.root;
428
+
429
+ if (follow) {
430
+ // Polling loop
431
+ while (true) {
432
+ // Clear screen
433
+ process.stdout.write("\x1b[2J\x1b[H");
434
+ const data = await gatherInspectData(root, agentName, {
435
+ limit,
436
+ noTmux,
437
+ tmuxLines: 30,
438
+ });
439
+ if (json) {
440
+ process.stdout.write(`${JSON.stringify(data, null, "\t")}\n`);
441
+ } else {
442
+ printInspectData(data);
443
+ }
444
+ await new Promise((resolve) => setTimeout(resolve, interval));
445
+ }
446
+ } else {
447
+ // Single snapshot
448
+ const data = await gatherInspectData(root, agentName, { limit, noTmux, tmuxLines: 30 });
449
+ if (json) {
450
+ process.stdout.write(`${JSON.stringify(data, null, "\t")}\n`);
451
+ } else {
452
+ printInspectData(data);
453
+ }
454
+ }
455
+ }