@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.
- package/LICENSE +21 -0
- package/README.md +381 -0
- package/agents/builder.md +137 -0
- package/agents/coordinator.md +263 -0
- package/agents/lead.md +301 -0
- package/agents/merger.md +160 -0
- package/agents/monitor.md +214 -0
- package/agents/reviewer.md +140 -0
- package/agents/scout.md +119 -0
- package/agents/supervisor.md +423 -0
- package/package.json +47 -0
- package/src/agents/checkpoint.test.ts +88 -0
- package/src/agents/checkpoint.ts +101 -0
- package/src/agents/hooks-deployer.test.ts +2040 -0
- package/src/agents/hooks-deployer.ts +607 -0
- package/src/agents/identity.test.ts +603 -0
- package/src/agents/identity.ts +384 -0
- package/src/agents/lifecycle.test.ts +196 -0
- package/src/agents/lifecycle.ts +183 -0
- package/src/agents/manifest.test.ts +746 -0
- package/src/agents/manifest.ts +354 -0
- package/src/agents/overlay.test.ts +676 -0
- package/src/agents/overlay.ts +308 -0
- package/src/beads/client.test.ts +217 -0
- package/src/beads/client.ts +202 -0
- package/src/beads/molecules.test.ts +338 -0
- package/src/beads/molecules.ts +198 -0
- package/src/commands/agents.test.ts +322 -0
- package/src/commands/agents.ts +287 -0
- package/src/commands/clean.test.ts +670 -0
- package/src/commands/clean.ts +618 -0
- package/src/commands/completions.test.ts +342 -0
- package/src/commands/completions.ts +887 -0
- package/src/commands/coordinator.test.ts +1530 -0
- package/src/commands/coordinator.ts +733 -0
- package/src/commands/costs.test.ts +1119 -0
- package/src/commands/costs.ts +564 -0
- package/src/commands/dashboard.test.ts +308 -0
- package/src/commands/dashboard.ts +838 -0
- package/src/commands/doctor.test.ts +294 -0
- package/src/commands/doctor.ts +213 -0
- package/src/commands/errors.test.ts +647 -0
- package/src/commands/errors.ts +248 -0
- package/src/commands/feed.test.ts +578 -0
- package/src/commands/feed.ts +361 -0
- package/src/commands/group.test.ts +262 -0
- package/src/commands/group.ts +511 -0
- package/src/commands/hooks.test.ts +458 -0
- package/src/commands/hooks.ts +253 -0
- package/src/commands/init.test.ts +347 -0
- package/src/commands/init.ts +650 -0
- package/src/commands/inspect.test.ts +670 -0
- package/src/commands/inspect.ts +431 -0
- package/src/commands/log.test.ts +1454 -0
- package/src/commands/log.ts +724 -0
- package/src/commands/logs.test.ts +379 -0
- package/src/commands/logs.ts +546 -0
- package/src/commands/mail.test.ts +1270 -0
- package/src/commands/mail.ts +771 -0
- package/src/commands/merge.test.ts +670 -0
- package/src/commands/merge.ts +355 -0
- package/src/commands/metrics.test.ts +444 -0
- package/src/commands/metrics.ts +143 -0
- package/src/commands/monitor.test.ts +191 -0
- package/src/commands/monitor.ts +390 -0
- package/src/commands/nudge.test.ts +230 -0
- package/src/commands/nudge.ts +372 -0
- package/src/commands/prime.test.ts +470 -0
- package/src/commands/prime.ts +381 -0
- package/src/commands/replay.test.ts +741 -0
- package/src/commands/replay.ts +360 -0
- package/src/commands/run.test.ts +431 -0
- package/src/commands/run.ts +351 -0
- package/src/commands/sling.test.ts +657 -0
- package/src/commands/sling.ts +661 -0
- package/src/commands/spec.test.ts +203 -0
- package/src/commands/spec.ts +168 -0
- package/src/commands/status.test.ts +430 -0
- package/src/commands/status.ts +398 -0
- package/src/commands/stop.test.ts +420 -0
- package/src/commands/stop.ts +151 -0
- package/src/commands/supervisor.test.ts +187 -0
- package/src/commands/supervisor.ts +535 -0
- package/src/commands/trace.test.ts +745 -0
- package/src/commands/trace.ts +325 -0
- package/src/commands/watch.test.ts +145 -0
- package/src/commands/watch.ts +247 -0
- package/src/commands/worktree.test.ts +786 -0
- package/src/commands/worktree.ts +311 -0
- package/src/config.test.ts +822 -0
- package/src/config.ts +829 -0
- package/src/doctor/agents.test.ts +454 -0
- package/src/doctor/agents.ts +396 -0
- package/src/doctor/config-check.test.ts +190 -0
- package/src/doctor/config-check.ts +183 -0
- package/src/doctor/consistency.test.ts +651 -0
- package/src/doctor/consistency.ts +294 -0
- package/src/doctor/databases.test.ts +290 -0
- package/src/doctor/databases.ts +218 -0
- package/src/doctor/dependencies.test.ts +184 -0
- package/src/doctor/dependencies.ts +175 -0
- package/src/doctor/logs.test.ts +251 -0
- package/src/doctor/logs.ts +295 -0
- package/src/doctor/merge-queue.test.ts +216 -0
- package/src/doctor/merge-queue.ts +144 -0
- package/src/doctor/structure.test.ts +291 -0
- package/src/doctor/structure.ts +198 -0
- package/src/doctor/types.ts +37 -0
- package/src/doctor/version.test.ts +136 -0
- package/src/doctor/version.ts +129 -0
- package/src/e2e/init-sling-lifecycle.test.ts +277 -0
- package/src/errors.ts +217 -0
- package/src/events/store.test.ts +660 -0
- package/src/events/store.ts +369 -0
- package/src/events/tool-filter.test.ts +330 -0
- package/src/events/tool-filter.ts +126 -0
- package/src/index.ts +316 -0
- package/src/insights/analyzer.test.ts +466 -0
- package/src/insights/analyzer.ts +203 -0
- package/src/logging/color.test.ts +142 -0
- package/src/logging/color.ts +71 -0
- package/src/logging/logger.test.ts +813 -0
- package/src/logging/logger.ts +266 -0
- package/src/logging/reporter.test.ts +259 -0
- package/src/logging/reporter.ts +109 -0
- package/src/logging/sanitizer.test.ts +190 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/mail/broadcast.test.ts +203 -0
- package/src/mail/broadcast.ts +92 -0
- package/src/mail/client.test.ts +773 -0
- package/src/mail/client.ts +223 -0
- package/src/mail/store.test.ts +705 -0
- package/src/mail/store.ts +387 -0
- package/src/merge/queue.test.ts +359 -0
- package/src/merge/queue.ts +231 -0
- package/src/merge/resolver.test.ts +1345 -0
- package/src/merge/resolver.ts +645 -0
- package/src/metrics/store.test.ts +667 -0
- package/src/metrics/store.ts +445 -0
- package/src/metrics/summary.test.ts +398 -0
- package/src/metrics/summary.ts +178 -0
- package/src/metrics/transcript.test.ts +356 -0
- package/src/metrics/transcript.ts +175 -0
- package/src/mulch/client.test.ts +671 -0
- package/src/mulch/client.ts +332 -0
- package/src/sessions/compat.test.ts +280 -0
- package/src/sessions/compat.ts +104 -0
- package/src/sessions/store.test.ts +873 -0
- package/src/sessions/store.ts +494 -0
- package/src/test-helpers.test.ts +124 -0
- package/src/test-helpers.ts +126 -0
- package/src/tracker/beads.ts +56 -0
- package/src/tracker/factory.test.ts +80 -0
- package/src/tracker/factory.ts +64 -0
- package/src/tracker/seeds.ts +182 -0
- package/src/tracker/types.ts +52 -0
- package/src/types.ts +724 -0
- package/src/watchdog/daemon.test.ts +1975 -0
- package/src/watchdog/daemon.ts +671 -0
- package/src/watchdog/health.test.ts +431 -0
- package/src/watchdog/health.ts +264 -0
- package/src/watchdog/triage.test.ts +164 -0
- package/src/watchdog/triage.ts +179 -0
- package/src/worktree/manager.test.ts +439 -0
- package/src/worktree/manager.ts +198 -0
- package/src/worktree/tmux.test.ts +1009 -0
- package/src/worktree/tmux.ts +509 -0
- package/templates/CLAUDE.md.tmpl +89 -0
- package/templates/hooks.json.tmpl +105 -0
- 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
|
+
}
|