@tangle-network/traces 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 (3) hide show
  1. package/README.md +73 -0
  2. package/dist/cli.js +1920 -0
  3. package/package.json +36 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1920 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { stat as stat11, writeFile as writeFile2 } from "fs/promises";
5
+
6
+ // src/analyze.ts
7
+ import { mkdtemp, writeFile } from "fs/promises";
8
+ import { tmpdir } from "os";
9
+ import { join } from "path";
10
+ import { buildDefaultAnalystRegistry } from "@tangle-network/agent-eval/analyst";
11
+ import { OtlpFileTraceStore } from "@tangle-network/agent-eval/traces";
12
+
13
+ // src/otlp.ts
14
+ function span(input) {
15
+ const attributes = {
16
+ "openinference.span.kind": input.kind
17
+ };
18
+ if (input.service != null) attributes["service.name"] = input.service;
19
+ if (input.agent != null) attributes["agent.name"] = input.agent;
20
+ if (input.model != null) attributes["llm.model_name"] = input.model;
21
+ if (input.tool != null) attributes["tool.name"] = input.tool;
22
+ if (input.inputTokens != null) attributes["llm.input_tokens"] = input.inputTokens;
23
+ if (input.outputTokens != null) attributes["llm.output_tokens"] = input.outputTokens;
24
+ if (input.step != null) attributes.step = input.step;
25
+ if (input.content != null && input.content.length > 0) attributes["content"] = input.content;
26
+ if (input.extra) Object.assign(attributes, input.extra);
27
+ const status = { code: input.status ?? "OK" };
28
+ if (input.statusMessage && input.statusMessage.length > 0) status.message = input.statusMessage;
29
+ return {
30
+ trace_id: input.traceId,
31
+ span_id: input.spanId,
32
+ parent_span_id: input.parentSpanId ?? null,
33
+ name: input.name,
34
+ start_time: input.startTime,
35
+ end_time: input.endTime ?? input.startTime,
36
+ status,
37
+ attributes
38
+ };
39
+ }
40
+ function serializeSpans(spans) {
41
+ if (spans.length === 0) return "";
42
+ return `${spans.map((s) => JSON.stringify(s)).join("\n")}
43
+ `;
44
+ }
45
+
46
+ // src/analyze.ts
47
+ async function writeOtlp(spans, outPath) {
48
+ const path = outPath ?? join(await mkdtemp(join(tmpdir(), "cli-bridge-traces-")), "spans.otlp.jsonl");
49
+ await writeFile(path, serializeSpans(spans), "utf8");
50
+ return path;
51
+ }
52
+ var DETERMINISTIC_VIEW_CEILING = 256 * 1024 * 1024;
53
+ async function analyzeSpans(spans, opts = {}) {
54
+ if (spans.length === 0) throw new Error("analyzeSpans: no spans to analyze");
55
+ const otlpPath = await writeOtlp(spans, opts.otlpOutPath);
56
+ const runId = opts.runId ?? `cli-bridge-${Date.now()}`;
57
+ const detStore = new OtlpFileTraceStore({ path: otlpPath, perCallByteCeiling: DETERMINISTIC_VIEW_CEILING });
58
+ await detStore.ensureIndexed();
59
+ const detRegistry = buildDefaultAnalystRegistry({ registry: { log: opts.log } });
60
+ const result = await detRegistry.run(runId, { traceStore: detStore });
61
+ if (opts.ai) {
62
+ const agStore = new OtlpFileTraceStore({ path: otlpPath });
63
+ await agStore.ensureIndexed();
64
+ const agRegistry = buildDefaultAnalystRegistry({
65
+ ai: opts.ai,
66
+ model: opts.model,
67
+ includeBehavioral: false,
68
+ registry: { log: opts.log }
69
+ });
70
+ const agResult = await agRegistry.run(runId, { traceStore: agStore }, {
71
+ budget: opts.budgetUsd != null ? { totalUsd: opts.budgetUsd } : void 0
72
+ });
73
+ result.findings.push(...agResult.findings);
74
+ result.per_analyst.push(...agResult.per_analyst);
75
+ result.total_cost_usd += agResult.total_cost_usd;
76
+ }
77
+ return { otlpPath, result };
78
+ }
79
+
80
+ // src/pipelines.ts
81
+ import { computeToolUseMetrics } from "@tangle-network/agent-eval";
82
+ import { stuckLoopView } from "@tangle-network/agent-eval/pipelines";
83
+
84
+ // src/runtime-store.ts
85
+ import { InMemoryTraceStore } from "@tangle-network/agent-eval";
86
+ var KIND = {
87
+ AGENT: "agent",
88
+ LLM: "llm",
89
+ TOOL: "tool",
90
+ CHAIN: "custom",
91
+ SPAN: "custom"
92
+ };
93
+ function ms(iso2) {
94
+ const n = Date.parse(iso2);
95
+ return Number.isNaN(n) ? 0 : n;
96
+ }
97
+ async function toRuntimeStore(spans) {
98
+ const store = new InMemoryTraceStore();
99
+ const runIds = /* @__PURE__ */ new Set();
100
+ for (const s of spans) {
101
+ if (!runIds.has(s.trace_id)) {
102
+ runIds.add(s.trace_id);
103
+ await store.appendRun({ runId: s.trace_id, scenarioId: "session" });
104
+ }
105
+ const kind = KIND[s.attributes["openinference.span.kind"]] ?? "custom";
106
+ const base = {
107
+ spanId: s.span_id,
108
+ parentSpanId: s.parent_span_id ?? void 0,
109
+ runId: s.trace_id,
110
+ name: s.name,
111
+ startedAt: ms(s.start_time),
112
+ endedAt: ms(s.end_time),
113
+ status: s.status.code === "ERROR" ? "error" : "ok",
114
+ error: s.status.message,
115
+ attributes: s.attributes
116
+ };
117
+ const span2 = kind === "tool" ? {
118
+ ...base,
119
+ kind: "tool",
120
+ toolName: String(s.attributes["tool.name"] ?? "tool"),
121
+ args: s.attributes.content ?? ""
122
+ } : { ...base, kind };
123
+ await store.appendSpan(span2);
124
+ }
125
+ return { store, runIds: [...runIds] };
126
+ }
127
+
128
+ // src/pipelines.ts
129
+ async function runPipelines(spans, opts = {}) {
130
+ const { store, runIds } = await toRuntimeStore(spans);
131
+ const stuckLoops = await stuckLoopView(store, { minOccurrences: opts.minLoopOccurrences ?? 3 });
132
+ const toolUse = await Promise.all(runIds.map((runId) => computeToolUseMetrics(store, runId)));
133
+ return { stuckLoops, toolUse };
134
+ }
135
+
136
+ // src/adapters/amp.ts
137
+ import { readdir, readFile, stat } from "fs/promises";
138
+ import { homedir } from "os";
139
+ import { basename, join as join2 } from "path";
140
+ var SERVICE = "amp";
141
+ var AmpAdapter = class {
142
+ harness = "amp";
143
+ threadsRoot() {
144
+ const base = process.env.XDG_DATA_HOME ?? join2(homedir(), ".local", "share");
145
+ return join2(base, "amp", "threads");
146
+ }
147
+ async locate(opts = {}) {
148
+ let files;
149
+ try {
150
+ files = await readdir(this.threadsRoot());
151
+ } catch {
152
+ return [];
153
+ }
154
+ const refs = [];
155
+ for (const f of files) {
156
+ if (!f.startsWith("T-") || !f.endsWith(".json")) continue;
157
+ const path = join2(this.threadsRoot(), f);
158
+ let st;
159
+ try {
160
+ st = await stat(path);
161
+ } catch {
162
+ continue;
163
+ }
164
+ if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
165
+ if (opts.cwd) continue;
166
+ refs.push({ harness: this.harness, sessionId: basename(f, ".json"), path, cwd: null, mtimeMs: st.mtimeMs });
167
+ }
168
+ return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
169
+ }
170
+ async parse(ref) {
171
+ let thread;
172
+ try {
173
+ thread = JSON.parse(await readFile(ref.path, "utf8"));
174
+ } catch {
175
+ return [];
176
+ }
177
+ const traceId = thread.id ?? ref.sessionId;
178
+ const rootId = `root:${traceId}`;
179
+ const start = new Date(thread.created ?? 0).toISOString();
180
+ const messages = thread.messages ?? [];
181
+ const spans = [
182
+ span({ traceId, spanId: rootId, parentSpanId: null, name: "session", kind: "AGENT", startTime: start, service: SERVICE, agent: SERVICE })
183
+ ];
184
+ const toolByUseId = /* @__PURE__ */ new Map();
185
+ let step = 0;
186
+ for (const m of messages) {
187
+ const mid = m.messageId ?? step;
188
+ const llmId = `llm:${mid}`;
189
+ const u = m.usage;
190
+ const inputTokens2 = u ? (u.inputTokens ?? 0) + (u.cacheReadInputTokens ?? 0) + (u.cacheCreationInputTokens ?? 0) || null : null;
191
+ spans.push(
192
+ span({
193
+ traceId,
194
+ spanId: llmId,
195
+ parentSpanId: rootId,
196
+ name: `message.${m.role ?? "unknown"}`,
197
+ kind: "LLM",
198
+ startTime: start,
199
+ service: SERVICE,
200
+ agent: SERVICE,
201
+ model: u?.model ?? null,
202
+ inputTokens: inputTokens2,
203
+ outputTokens: u?.outputTokens ?? null,
204
+ step
205
+ })
206
+ );
207
+ step += 1;
208
+ for (const b of m.content ?? []) {
209
+ if (b.type === "tool_use" && b.name) {
210
+ const t = span({
211
+ traceId,
212
+ spanId: `tool:${b.id ?? `${mid}:${step}`}`,
213
+ parentSpanId: llmId,
214
+ name: `tool.${b.name}`,
215
+ kind: "TOOL",
216
+ startTime: start,
217
+ service: SERVICE,
218
+ agent: SERVICE,
219
+ tool: b.name,
220
+ step,
221
+ content: b.input != null ? JSON.stringify(b.input) : null
222
+ });
223
+ spans.push(t);
224
+ if (b.id) toolByUseId.set(b.id, t);
225
+ step += 1;
226
+ } else if (b.type === "tool_result" && b.tool_use_id) {
227
+ const t = toolByUseId.get(b.tool_use_id);
228
+ if (t) t.status = b.is_error === true ? { code: "ERROR", message: "tool reported error" } : { code: "OK" };
229
+ }
230
+ }
231
+ }
232
+ return spans;
233
+ }
234
+ };
235
+
236
+ // src/adapters/claude.ts
237
+ import { readdir as readdir2, readFile as readFile2, stat as stat2 } from "fs/promises";
238
+ import { homedir as homedir2 } from "os";
239
+ import { basename as basename2, join as join3 } from "path";
240
+ var SERVICE2 = "claude-code";
241
+ function parseLines(raw) {
242
+ const out = [];
243
+ for (const line of raw.split("\n")) {
244
+ if (!line) continue;
245
+ try {
246
+ out.push(JSON.parse(line));
247
+ } catch {
248
+ }
249
+ }
250
+ return out;
251
+ }
252
+ function inputTokens(u) {
253
+ if (!u) return null;
254
+ const v = (u.input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
255
+ return v > 0 ? v : null;
256
+ }
257
+ function asBlocks(content) {
258
+ return Array.isArray(content) ? content : [];
259
+ }
260
+ function stringifyToolResult(content) {
261
+ if (typeof content === "string") return content;
262
+ if (Array.isArray(content)) {
263
+ return content.map((c) => c && typeof c === "object" && "text" in c ? String(c.text ?? "") : "").join("");
264
+ }
265
+ return "";
266
+ }
267
+ function parseClaudeStream(events, ctx) {
268
+ const spans = [];
269
+ const toolSpanByUseId = /* @__PURE__ */ new Map();
270
+ let step = ctx.startStep;
271
+ for (const ev of events) {
272
+ const ts = ev.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
273
+ const uid = ev.uuid ?? `${ctx.idPrefix}step${step}`;
274
+ if (ev.type === "assistant" && ev.message) {
275
+ const llmId = `${ctx.idPrefix}${uid}`;
276
+ spans.push(
277
+ span({
278
+ traceId: ctx.traceId,
279
+ spanId: llmId,
280
+ parentSpanId: ctx.rootParent,
281
+ name: "llm.turn",
282
+ kind: "LLM",
283
+ startTime: ts,
284
+ service: SERVICE2,
285
+ agent: ctx.agent,
286
+ model: ev.message.model ?? null,
287
+ inputTokens: inputTokens(ev.message.usage),
288
+ outputTokens: ev.message.usage?.output_tokens ?? null,
289
+ step
290
+ })
291
+ );
292
+ step += 1;
293
+ let toolIdx = 0;
294
+ for (const block of asBlocks(ev.message.content)) {
295
+ if (block.type !== "tool_use" || !block.name) continue;
296
+ const toolSpan = span({
297
+ traceId: ctx.traceId,
298
+ spanId: `${ctx.idPrefix}${uid}:tool:${toolIdx}`,
299
+ parentSpanId: llmId,
300
+ name: `tool.${block.name}`,
301
+ kind: "TOOL",
302
+ startTime: ts,
303
+ service: SERVICE2,
304
+ agent: ctx.agent,
305
+ tool: block.name,
306
+ step,
307
+ content: block.input != null ? JSON.stringify(block.input) : null
308
+ });
309
+ spans.push(toolSpan);
310
+ if (block.id) toolSpanByUseId.set(block.id, toolSpan);
311
+ toolIdx += 1;
312
+ step += 1;
313
+ }
314
+ } else if (ev.type === "user" && ev.message) {
315
+ for (const block of asBlocks(ev.message.content)) {
316
+ if (block.type !== "tool_result" || !block.tool_use_id) continue;
317
+ backfillResult(toolSpanByUseId.get(block.tool_use_id), ts, block.is_error === true, stringifyToolResult(block.content));
318
+ }
319
+ } else if (ev.type === "attachment" && ev.attachment?.type === "tool_result" && ev.attachment.toolUseID) {
320
+ const err = typeof ev.attachment.exitCode === "number" && ev.attachment.exitCode !== 0;
321
+ backfillResult(toolSpanByUseId.get(ev.attachment.toolUseID), ts, err, ev.attachment.stderr ?? "");
322
+ }
323
+ }
324
+ return { spans, toolSpanByUseId, nextStep: step };
325
+ }
326
+ function backfillResult(s, endTime, isError, message) {
327
+ if (!s) return;
328
+ s.end_time = endTime;
329
+ const code = isError ? "ERROR" : "OK";
330
+ s.status = { code };
331
+ if (isError && message) s.status.message = message.slice(0, 500);
332
+ }
333
+ var ClaudeAdapter = class {
334
+ harness = "claude-code";
335
+ aliases = ["claude", "claudish", "openclaw", "nanoclaw"];
336
+ root() {
337
+ return join3(homedir2(), ".claude", "projects");
338
+ }
339
+ async locate(opts = {}) {
340
+ const root = this.root();
341
+ let projectDirs;
342
+ try {
343
+ projectDirs = await readdir2(root);
344
+ } catch {
345
+ return [];
346
+ }
347
+ const refs = [];
348
+ for (const dir of projectDirs) {
349
+ const dirPath = join3(root, dir);
350
+ let files;
351
+ try {
352
+ files = await readdir2(dirPath);
353
+ } catch {
354
+ continue;
355
+ }
356
+ for (const f of files) {
357
+ if (!f.endsWith(".jsonl")) continue;
358
+ const path = join3(dirPath, f);
359
+ let st;
360
+ try {
361
+ st = await stat2(path);
362
+ } catch {
363
+ continue;
364
+ }
365
+ if (!st.isFile()) continue;
366
+ if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
367
+ const cwd = `/${dir.replace(/^-/, "").replace(/-/g, "/")}`;
368
+ if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
369
+ refs.push({ harness: this.harness, sessionId: basename2(f, ".jsonl"), path, cwd, mtimeMs: st.mtimeMs });
370
+ }
371
+ }
372
+ return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
373
+ }
374
+ async parse(ref) {
375
+ const raw = await readFile2(ref.path, "utf8");
376
+ const events = parseLines(raw);
377
+ const traceId = events.find((e) => e.sessionId)?.sessionId ?? ref.sessionId;
378
+ const rootId = `root:${traceId}`;
379
+ const spans = [
380
+ span({
381
+ traceId,
382
+ spanId: rootId,
383
+ parentSpanId: null,
384
+ name: "session",
385
+ kind: "AGENT",
386
+ startTime: events[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
387
+ endTime: events.at(-1)?.timestamp,
388
+ service: SERVICE2,
389
+ agent: SERVICE2
390
+ })
391
+ ];
392
+ const main2 = parseClaudeStream(events, {
393
+ traceId,
394
+ agent: SERVICE2,
395
+ startStep: 0,
396
+ idPrefix: "",
397
+ rootParent: rootId
398
+ });
399
+ spans.push(...main2.spans);
400
+ await this.foldSubagents(ref, traceId, main2, spans);
401
+ return spans;
402
+ }
403
+ /** Parse `<session>/subagents/agent-*.jsonl`, parenting each under its Agent call. */
404
+ async foldSubagents(ref, traceId, main2, out) {
405
+ const subDir = join3(ref.path.replace(/\.jsonl$/, ""), "subagents");
406
+ let files;
407
+ try {
408
+ files = await readdir2(subDir);
409
+ } catch {
410
+ return;
411
+ }
412
+ let step = main2.nextStep;
413
+ for (const f of files) {
414
+ if (!f.endsWith(".jsonl")) continue;
415
+ const hash = basename2(f, ".jsonl");
416
+ let meta = {};
417
+ try {
418
+ meta = JSON.parse(await readFile2(join3(subDir, `${hash}.meta.json`), "utf8"));
419
+ } catch {
420
+ }
421
+ const parent = meta.toolUseId && main2.toolSpanByUseId.get(meta.toolUseId)?.span_id || `root:${traceId}`;
422
+ let raw;
423
+ try {
424
+ raw = await readFile2(join3(subDir, f), "utf8");
425
+ } catch {
426
+ continue;
427
+ }
428
+ const parsed = parseClaudeStream(parseLines(raw), {
429
+ traceId,
430
+ agent: meta.agentType ? `subagent:${meta.agentType}` : "subagent",
431
+ startStep: step,
432
+ idPrefix: `${hash}:`,
433
+ rootParent: parent
434
+ });
435
+ out.push(...parsed.spans);
436
+ step = parsed.nextStep;
437
+ }
438
+ }
439
+ };
440
+
441
+ // src/adapters/codex.ts
442
+ import { readdir as readdir3, readFile as readFile3, stat as stat3 } from "fs/promises";
443
+ import { homedir as homedir3 } from "os";
444
+ import { basename as basename3, join as join4 } from "path";
445
+ var SERVICE3 = "codex";
446
+ function parseLines2(raw) {
447
+ const out = [];
448
+ for (const line of raw.split("\n")) {
449
+ if (!line) continue;
450
+ try {
451
+ out.push(JSON.parse(line));
452
+ } catch {
453
+ }
454
+ }
455
+ return out;
456
+ }
457
+ function contentToString(content) {
458
+ if (typeof content === "string") return content;
459
+ if (Array.isArray(content)) {
460
+ return content.map((c) => c && typeof c === "object" && "text" in c ? String(c.text ?? "") : "").join("");
461
+ }
462
+ return "";
463
+ }
464
+ function outputIsError(output) {
465
+ const s = typeof output === "string" ? output : JSON.stringify(output ?? "");
466
+ const error = /"success"\s*:\s*false|exit(?:_| )code["\s:]+[1-9]|\bcommand failed\b|\bENOENT\b|\berror:/i.test(s);
467
+ return { error, message: error ? s.slice(0, 500) : "" };
468
+ }
469
+ async function* walkRollouts(root) {
470
+ let years;
471
+ try {
472
+ years = await readdir3(root);
473
+ } catch {
474
+ return;
475
+ }
476
+ for (const y of years) {
477
+ const yp = join4(root, y);
478
+ let months;
479
+ try {
480
+ months = await readdir3(yp);
481
+ } catch {
482
+ continue;
483
+ }
484
+ for (const m of months) {
485
+ const mp = join4(yp, m);
486
+ let days;
487
+ try {
488
+ days = await readdir3(mp);
489
+ } catch {
490
+ continue;
491
+ }
492
+ for (const d of days) {
493
+ const dp = join4(mp, d);
494
+ let files;
495
+ try {
496
+ files = await readdir3(dp);
497
+ } catch {
498
+ continue;
499
+ }
500
+ for (const f of files) {
501
+ if (f.startsWith("rollout-") && f.endsWith(".jsonl")) yield join4(dp, f);
502
+ }
503
+ }
504
+ }
505
+ }
506
+ }
507
+ var CodexAdapter = class {
508
+ harness = "codex";
509
+ aliases = ["codex-acp"];
510
+ root() {
511
+ return process.env.CODEX_HOME ? join4(process.env.CODEX_HOME, "sessions") : join4(homedir3(), ".codex", "sessions");
512
+ }
513
+ async locate(opts = {}) {
514
+ const refs = [];
515
+ for await (const path of walkRollouts(this.root())) {
516
+ let st;
517
+ try {
518
+ st = await stat3(path);
519
+ } catch {
520
+ continue;
521
+ }
522
+ if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
523
+ const head = (await readFile3(path, "utf8").catch(() => "")).split("\n", 1)[0] ?? "";
524
+ let cwd = null;
525
+ let id = basename3(path).replace(/^rollout-[\dT-]+-/, "").replace(/\.jsonl$/, "");
526
+ try {
527
+ const meta = JSON.parse(head);
528
+ if (meta.payload?.cwd) cwd = meta.payload.cwd;
529
+ if (meta.payload?.id) id = meta.payload.id;
530
+ } catch {
531
+ }
532
+ if (opts.cwd && (!cwd || !cwd.startsWith(opts.cwd))) continue;
533
+ refs.push({ harness: this.harness, sessionId: id, path, cwd, mtimeMs: st.mtimeMs });
534
+ }
535
+ return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
536
+ }
537
+ async parse(ref) {
538
+ const lines = parseLines2(await readFile3(ref.path, "utf8"));
539
+ const meta = lines.find((l) => l.type === "session_meta");
540
+ const traceId = meta?.payload?.id ?? ref.sessionId;
541
+ const model = lines.find((l) => l.type === "turn_context")?.payload?.model ?? null;
542
+ const rootId = `root:${traceId}`;
543
+ const spans = [
544
+ span({
545
+ traceId,
546
+ spanId: rootId,
547
+ parentSpanId: null,
548
+ name: "session",
549
+ kind: "AGENT",
550
+ startTime: meta?.timestamp ?? lines[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
551
+ endTime: lines.at(-1)?.timestamp,
552
+ service: SERVICE3,
553
+ agent: SERVICE3,
554
+ model
555
+ })
556
+ ];
557
+ const toolByCallId = /* @__PURE__ */ new Map();
558
+ let step = 0;
559
+ let lastLlm = rootId;
560
+ for (const l of lines) {
561
+ const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
562
+ if (l.type === "event_msg" && l.payload?.type === "token_count") {
563
+ const u = l.payload.info?.last_token_usage;
564
+ if (u && (u.input_tokens || u.output_tokens)) {
565
+ const id = `llm:${step}`;
566
+ spans.push(
567
+ span({
568
+ traceId,
569
+ spanId: id,
570
+ parentSpanId: rootId,
571
+ name: "llm.turn",
572
+ kind: "LLM",
573
+ startTime: ts,
574
+ service: SERVICE3,
575
+ agent: SERVICE3,
576
+ model,
577
+ inputTokens: u.input_tokens ?? null,
578
+ outputTokens: u.output_tokens ?? null,
579
+ step
580
+ })
581
+ );
582
+ lastLlm = id;
583
+ step += 1;
584
+ }
585
+ } else if (l.type === "response_item" && l.payload?.type === "function_call") {
586
+ const name = l.payload.name ?? "tool";
587
+ const callId = l.payload.call_id ?? `${step}`;
588
+ const toolSpan = span({
589
+ traceId,
590
+ spanId: `tool:${callId}`,
591
+ parentSpanId: lastLlm,
592
+ name: `tool.${name}`,
593
+ kind: "TOOL",
594
+ startTime: ts,
595
+ service: SERVICE3,
596
+ agent: SERVICE3,
597
+ tool: name,
598
+ step,
599
+ content: l.payload.arguments ?? null
600
+ });
601
+ spans.push(toolSpan);
602
+ toolByCallId.set(callId, toolSpan);
603
+ step += 1;
604
+ } else if (l.type === "response_item" && l.payload?.type === "function_call_output") {
605
+ const t = toolByCallId.get(l.payload.call_id ?? "");
606
+ if (t) {
607
+ const { error, message } = outputIsError(l.payload.output);
608
+ t.end_time = ts;
609
+ t.status = error ? { code: "ERROR", message } : { code: "OK" };
610
+ }
611
+ } else if (l.type === "response_item" && l.payload?.type === "message") {
612
+ const text = contentToString(l.payload.content);
613
+ if (text) {
614
+ spans.push(
615
+ span({
616
+ traceId,
617
+ spanId: `msg:${step}`,
618
+ parentSpanId: rootId,
619
+ name: `message.${l.payload.role ?? "unknown"}`,
620
+ kind: "CHAIN",
621
+ startTime: ts,
622
+ service: SERVICE3,
623
+ agent: SERVICE3,
624
+ step,
625
+ content: text.slice(0, 8e3)
626
+ })
627
+ );
628
+ step += 1;
629
+ }
630
+ }
631
+ }
632
+ return spans;
633
+ }
634
+ };
635
+
636
+ // src/adapters/copilot.ts
637
+ import { readdir as readdir4, readFile as readFile4, stat as stat4 } from "fs/promises";
638
+ import { homedir as homedir4 } from "os";
639
+ import { join as join5 } from "path";
640
+ var SERVICE4 = "github-copilot";
641
+ function parseLines3(raw) {
642
+ const out = [];
643
+ for (const line of raw.split("\n")) {
644
+ if (!line.trim()) continue;
645
+ try {
646
+ out.push(JSON.parse(line));
647
+ } catch {
648
+ }
649
+ }
650
+ return out;
651
+ }
652
+ var CopilotAdapter = class {
653
+ harness = "github-copilot";
654
+ aliases = ["copilot"];
655
+ root() {
656
+ return join5(process.env.COPILOT_HOME ?? join5(homedir4(), ".copilot"), "session-state");
657
+ }
658
+ async locate(opts = {}) {
659
+ let dirs;
660
+ try {
661
+ dirs = await readdir4(this.root());
662
+ } catch {
663
+ return [];
664
+ }
665
+ const refs = [];
666
+ for (const id of dirs) {
667
+ const path = join5(this.root(), id, "events.jsonl");
668
+ let st;
669
+ try {
670
+ st = await stat4(path);
671
+ } catch {
672
+ continue;
673
+ }
674
+ if (!st.isFile()) continue;
675
+ if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
676
+ if (opts.cwd) continue;
677
+ refs.push({ harness: this.harness, sessionId: id, path, cwd: null, mtimeMs: st.mtimeMs });
678
+ }
679
+ return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
680
+ }
681
+ async parse(ref) {
682
+ const events = parseLines3(await readFile4(ref.path, "utf8"));
683
+ const traceId = ref.sessionId;
684
+ const rootId = `root:${traceId}`;
685
+ const spans = [
686
+ span({
687
+ traceId,
688
+ spanId: rootId,
689
+ parentSpanId: null,
690
+ name: "session",
691
+ kind: "AGENT",
692
+ startTime: events[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
693
+ endTime: events.at(-1)?.timestamp,
694
+ service: SERVICE4,
695
+ agent: SERVICE4
696
+ })
697
+ ];
698
+ const toolByCallId = /* @__PURE__ */ new Map();
699
+ let step = 0;
700
+ let lastLlm = rootId;
701
+ let pendingInputTokens = null;
702
+ for (const ev of events) {
703
+ const ts = ev.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
704
+ const d = ev.data ?? {};
705
+ if (ev.type === "assistant.usage") {
706
+ pendingInputTokens = d.inputTokens ?? null;
707
+ continue;
708
+ }
709
+ if (ev.type === "assistant.message") {
710
+ const llmId = `llm:${d.messageId ?? ev.id ?? step}`;
711
+ spans.push(
712
+ span({
713
+ traceId,
714
+ spanId: llmId,
715
+ parentSpanId: rootId,
716
+ name: "message.assistant",
717
+ kind: "LLM",
718
+ startTime: ts,
719
+ service: SERVICE4,
720
+ agent: SERVICE4,
721
+ model: d.model ?? null,
722
+ inputTokens: pendingInputTokens,
723
+ outputTokens: d.outputTokens ?? null,
724
+ step,
725
+ content: typeof d.content === "string" ? d.content.slice(0, 8e3) : null
726
+ })
727
+ );
728
+ lastLlm = llmId;
729
+ pendingInputTokens = null;
730
+ step += 1;
731
+ } else if (ev.type === "tool.execution_start" && d.toolCallId) {
732
+ const name = d.toolName ?? "tool";
733
+ const t = span({
734
+ traceId,
735
+ spanId: `tool:${d.toolCallId}`,
736
+ parentSpanId: lastLlm,
737
+ name: `tool.${name}`,
738
+ kind: "TOOL",
739
+ startTime: ts,
740
+ service: SERVICE4,
741
+ agent: SERVICE4,
742
+ tool: name,
743
+ step,
744
+ content: d.arguments != null ? JSON.stringify(d.arguments) : null
745
+ });
746
+ spans.push(t);
747
+ toolByCallId.set(d.toolCallId, t);
748
+ step += 1;
749
+ } else if (ev.type === "tool.execution_complete" && d.toolCallId) {
750
+ const t = toolByCallId.get(d.toolCallId);
751
+ if (t) {
752
+ t.end_time = ts;
753
+ const err = d.success === false;
754
+ t.status = err ? { code: "ERROR", message: (d.error?.message ?? "").slice(0, 500) } : { code: "OK" };
755
+ }
756
+ }
757
+ }
758
+ return spans;
759
+ }
760
+ };
761
+
762
+ // src/adapters/factory.ts
763
+ import { readdir as readdir5, readFile as readFile5, stat as stat5 } from "fs/promises";
764
+ import { homedir as homedir5 } from "os";
765
+ import { basename as basename4, join as join6 } from "path";
766
+ var SERVICE5 = "factory";
767
+ function parseLines4(raw) {
768
+ const out = [];
769
+ for (const line of raw.split("\n")) {
770
+ if (!line) continue;
771
+ try {
772
+ out.push(JSON.parse(line));
773
+ } catch {
774
+ }
775
+ }
776
+ return out;
777
+ }
778
+ var FactoryAdapter = class {
779
+ harness = "factory";
780
+ aliases = ["factory-droids", "droid"];
781
+ root() {
782
+ return join6(homedir5(), ".factory", "sessions");
783
+ }
784
+ async locate(opts = {}) {
785
+ const root = this.root();
786
+ let dirs;
787
+ try {
788
+ dirs = await readdir5(root);
789
+ } catch {
790
+ return [];
791
+ }
792
+ const refs = [];
793
+ for (const dir of dirs) {
794
+ const dp = join6(root, dir);
795
+ let files;
796
+ try {
797
+ files = await readdir5(dp);
798
+ } catch {
799
+ continue;
800
+ }
801
+ const cwd = dir.replace(/-/g, "/");
802
+ if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
803
+ for (const f of files) {
804
+ if (!f.endsWith(".jsonl")) continue;
805
+ const path = join6(dp, f);
806
+ let st;
807
+ try {
808
+ st = await stat5(path);
809
+ } catch {
810
+ continue;
811
+ }
812
+ if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
813
+ refs.push({ harness: this.harness, sessionId: basename4(f, ".jsonl"), path, cwd, mtimeMs: st.mtimeMs });
814
+ }
815
+ }
816
+ return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
817
+ }
818
+ async parse(ref) {
819
+ const lines = parseLines4(await readFile5(ref.path, "utf8"));
820
+ const start = lines.find((l) => l.type === "session_start");
821
+ const traceId = start?.id ?? ref.sessionId;
822
+ const rootId = `root:${traceId}`;
823
+ let settings = {};
824
+ try {
825
+ settings = JSON.parse(await readFile5(ref.path.replace(/\.jsonl$/, ".settings.json"), "utf8"));
826
+ } catch {
827
+ }
828
+ const spans = [
829
+ span({
830
+ traceId,
831
+ spanId: rootId,
832
+ parentSpanId: null,
833
+ name: "session",
834
+ kind: "AGENT",
835
+ startTime: start?.timestamp ?? lines[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
836
+ endTime: lines.at(-1)?.timestamp,
837
+ service: SERVICE5,
838
+ agent: SERVICE5,
839
+ model: settings.model ?? null,
840
+ extra: settings.tokenUsage ? {
841
+ "session.input_tokens": settings.tokenUsage.inputTokens ?? 0,
842
+ "session.output_tokens": settings.tokenUsage.outputTokens ?? 0
843
+ } : void 0
844
+ })
845
+ ];
846
+ const toolByUseId = /* @__PURE__ */ new Map();
847
+ let step = 0;
848
+ let lastLlm = rootId;
849
+ for (const l of lines) {
850
+ if (l.type !== "message" || !l.message) continue;
851
+ const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
852
+ const role = l.message.role;
853
+ const llmId = `llm:${l.id ?? step}`;
854
+ const text = (l.message.content ?? []).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
855
+ spans.push(
856
+ span({
857
+ traceId,
858
+ spanId: llmId,
859
+ parentSpanId: rootId,
860
+ name: `message.${role ?? "unknown"}`,
861
+ kind: "LLM",
862
+ startTime: ts,
863
+ service: SERVICE5,
864
+ agent: SERVICE5,
865
+ model: settings.model ?? null,
866
+ step,
867
+ content: text ? text.slice(0, 8e3) : null
868
+ })
869
+ );
870
+ lastLlm = llmId;
871
+ step += 1;
872
+ for (const b of l.message.content ?? []) {
873
+ if (b.type === "tool_use" && b.name) {
874
+ const t = span({
875
+ traceId,
876
+ spanId: `tool:${b.id ?? `${l.id}:${step}`}`,
877
+ parentSpanId: lastLlm,
878
+ name: `tool.${b.name}`,
879
+ kind: "TOOL",
880
+ startTime: ts,
881
+ service: SERVICE5,
882
+ agent: SERVICE5,
883
+ tool: b.name,
884
+ step,
885
+ content: b.input != null ? JSON.stringify(b.input) : null
886
+ });
887
+ spans.push(t);
888
+ if (b.id) toolByUseId.set(b.id, t);
889
+ step += 1;
890
+ } else if (b.type === "tool_result" && b.tool_use_id) {
891
+ const t = toolByUseId.get(b.tool_use_id);
892
+ if (t) t.status = b.is_error === true ? { code: "ERROR", message: "tool reported error" } : { code: "OK" };
893
+ }
894
+ }
895
+ }
896
+ return spans;
897
+ }
898
+ };
899
+
900
+ // src/adapters/forge.ts
901
+ import { readdir as readdir6, readFile as readFile6, stat as stat6 } from "fs/promises";
902
+ import { homedir as homedir6 } from "os";
903
+ import { basename as basename5, join as join7 } from "path";
904
+ var SERVICE6 = "forge";
905
+ function tokens(t) {
906
+ if (!t) return null;
907
+ return t.actual ?? t.approx ?? null;
908
+ }
909
+ var ForgeAdapter = class {
910
+ harness = "forge";
911
+ aliases = ["forgecode"];
912
+ /** Directories scanned for `*-dump.json` exports. */
913
+ dumpDirs() {
914
+ return [process.cwd(), join7(homedir6(), ".forge")];
915
+ }
916
+ async locate(opts = {}) {
917
+ const refs = [];
918
+ const dirs = opts.cwd ? [opts.cwd] : this.dumpDirs();
919
+ for (const dir of dirs) {
920
+ let files;
921
+ try {
922
+ files = await readdir6(dir);
923
+ } catch {
924
+ continue;
925
+ }
926
+ for (const f of files) {
927
+ if (!f.endsWith("-dump.json")) continue;
928
+ const path = join7(dir, f);
929
+ let st;
930
+ try {
931
+ st = await stat6(path);
932
+ } catch {
933
+ continue;
934
+ }
935
+ if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
936
+ refs.push({ harness: this.harness, sessionId: basename5(f, ".json"), path, cwd: dir, mtimeMs: st.mtimeMs });
937
+ }
938
+ }
939
+ return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
940
+ }
941
+ async parse(ref) {
942
+ let dump;
943
+ try {
944
+ dump = JSON.parse(await readFile6(ref.path, "utf8"));
945
+ } catch {
946
+ return [];
947
+ }
948
+ const ctx = dump.context ?? dump;
949
+ const traceId = ctx.conversation_id ?? ref.sessionId;
950
+ const rootId = `root:${traceId}`;
951
+ const messages = ctx.messages ?? [];
952
+ const spans = [
953
+ span({ traceId, spanId: rootId, parentSpanId: null, name: "session", kind: "AGENT", startTime: (/* @__PURE__ */ new Date(0)).toISOString(), service: SERVICE6, agent: SERVICE6 })
954
+ ];
955
+ const toolByCallId = /* @__PURE__ */ new Map();
956
+ let step = 0;
957
+ let lastLlm = rootId;
958
+ for (const raw of messages) {
959
+ const entry = raw.message ?? raw;
960
+ const ts = (/* @__PURE__ */ new Date(0)).toISOString();
961
+ if (entry.text) {
962
+ const llmId = `llm:${step}`;
963
+ spans.push(
964
+ span({
965
+ traceId,
966
+ spanId: llmId,
967
+ parentSpanId: rootId,
968
+ name: `message.${entry.text.role ?? "unknown"}`,
969
+ kind: "LLM",
970
+ startTime: ts,
971
+ service: SERVICE6,
972
+ agent: SERVICE6,
973
+ model: entry.text.model ?? null,
974
+ inputTokens: tokens(raw.usage?.prompt_tokens),
975
+ outputTokens: tokens(raw.usage?.completion_tokens),
976
+ step,
977
+ content: entry.text.content ? entry.text.content.slice(0, 8e3) : null
978
+ })
979
+ );
980
+ lastLlm = llmId;
981
+ step += 1;
982
+ for (const tc of entry.text.tool_calls ?? []) {
983
+ const id = tc.call_id ?? `${step}`;
984
+ const t = span({
985
+ traceId,
986
+ spanId: `tool:${id}`,
987
+ parentSpanId: llmId,
988
+ name: `tool.${tc.name ?? "tool"}`,
989
+ kind: "TOOL",
990
+ startTime: ts,
991
+ service: SERVICE6,
992
+ agent: SERVICE6,
993
+ tool: tc.name ?? "tool",
994
+ step,
995
+ content: tc.arguments != null ? JSON.stringify(tc.arguments) : null
996
+ });
997
+ spans.push(t);
998
+ toolByCallId.set(id, t);
999
+ step += 1;
1000
+ }
1001
+ } else if (entry.tool) {
1002
+ const t = toolByCallId.get(entry.tool.call_id ?? "");
1003
+ if (t) t.status = entry.tool.output?.is_error ? { code: "ERROR", message: "tool reported error" } : { code: "OK" };
1004
+ }
1005
+ }
1006
+ return spans;
1007
+ }
1008
+ };
1009
+
1010
+ // src/adapters/gemini.ts
1011
+ import { readdir as readdir7, readFile as readFile7, stat as stat7 } from "fs/promises";
1012
+ import { homedir as homedir7 } from "os";
1013
+ import { join as join8 } from "path";
1014
+ function iso(ts, fallback) {
1015
+ if (typeof ts === "number") return new Date(ts).toISOString();
1016
+ if (typeof ts === "string" && ts.length > 0) return ts;
1017
+ return fallback;
1018
+ }
1019
+ function toolStatusError(status) {
1020
+ return status != null && /error|fail|cancel/i.test(status);
1021
+ }
1022
+ var GeminiFamilyAdapter = class {
1023
+ harness;
1024
+ aliases;
1025
+ service;
1026
+ homeDirName;
1027
+ constructor(cfg) {
1028
+ this.harness = cfg.harness;
1029
+ this.aliases = cfg.aliases;
1030
+ this.service = cfg.service;
1031
+ this.homeDirName = cfg.homeDirName;
1032
+ }
1033
+ chatsRoot() {
1034
+ return join8(homedir7(), this.homeDirName, "tmp");
1035
+ }
1036
+ async locate(opts = {}) {
1037
+ const root = this.chatsRoot();
1038
+ let projectDirs;
1039
+ try {
1040
+ projectDirs = await readdir7(root);
1041
+ } catch {
1042
+ return [];
1043
+ }
1044
+ const refs = [];
1045
+ for (const pd of projectDirs) {
1046
+ const chatsDir = join8(root, pd, "chats");
1047
+ let files;
1048
+ try {
1049
+ files = await readdir7(chatsDir);
1050
+ } catch {
1051
+ continue;
1052
+ }
1053
+ for (const f of files) {
1054
+ if (!f.startsWith("session-") || !f.endsWith(".json")) continue;
1055
+ const path = join8(chatsDir, f);
1056
+ let st;
1057
+ try {
1058
+ st = await stat7(path);
1059
+ } catch {
1060
+ continue;
1061
+ }
1062
+ if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
1063
+ if (opts.cwd) continue;
1064
+ refs.push({ harness: this.harness, sessionId: f.replace(/\.json$/, ""), path, cwd: null, mtimeMs: st.mtimeMs });
1065
+ }
1066
+ }
1067
+ return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
1068
+ }
1069
+ async parse(ref) {
1070
+ let session;
1071
+ try {
1072
+ session = JSON.parse(await readFile7(ref.path, "utf8"));
1073
+ } catch {
1074
+ return [];
1075
+ }
1076
+ const traceId = session.sessionId ?? ref.sessionId;
1077
+ const messages = session.messages ?? [];
1078
+ const rootId = `root:${traceId}`;
1079
+ const start = iso(session.startTime ?? messages[0]?.timestamp, (/* @__PURE__ */ new Date(0)).toISOString());
1080
+ const spans = [
1081
+ span({
1082
+ traceId,
1083
+ spanId: rootId,
1084
+ parentSpanId: null,
1085
+ name: "session",
1086
+ kind: "AGENT",
1087
+ startTime: start,
1088
+ endTime: iso(messages.at(-1)?.timestamp, start),
1089
+ service: this.service,
1090
+ agent: this.service
1091
+ })
1092
+ ];
1093
+ let step = 0;
1094
+ for (const m of messages) {
1095
+ const mid = m.id ?? `m${step}`;
1096
+ const ts = iso(m.timestamp, start);
1097
+ const llmId = `llm:${mid}`;
1098
+ const content = typeof m.content === "string" ? m.content : JSON.stringify(m.content ?? "");
1099
+ spans.push(
1100
+ span({
1101
+ traceId,
1102
+ spanId: llmId,
1103
+ parentSpanId: rootId,
1104
+ name: `message.${m.type ?? "unknown"}`,
1105
+ kind: "LLM",
1106
+ startTime: ts,
1107
+ service: this.service,
1108
+ agent: this.service,
1109
+ model: m.model ?? null,
1110
+ inputTokens: m.tokens?.input ?? null,
1111
+ outputTokens: m.tokens?.output ?? null,
1112
+ step,
1113
+ content: content ? content.slice(0, 8e3) : null
1114
+ })
1115
+ );
1116
+ step += 1;
1117
+ for (const tc of m.toolCalls ?? []) {
1118
+ const err = toolStatusError(tc.status);
1119
+ spans.push(
1120
+ span({
1121
+ traceId,
1122
+ spanId: `tool:${tc.id ?? `${mid}:${step}`}`,
1123
+ parentSpanId: llmId,
1124
+ name: `tool.${tc.name ?? "tool"}`,
1125
+ kind: "TOOL",
1126
+ startTime: ts,
1127
+ status: err ? "ERROR" : "OK",
1128
+ statusMessage: err ? `status=${tc.status}` : void 0,
1129
+ service: this.service,
1130
+ agent: this.service,
1131
+ tool: tc.name ?? "tool",
1132
+ step,
1133
+ content: tc.args != null ? JSON.stringify(tc.args) : null
1134
+ })
1135
+ );
1136
+ step += 1;
1137
+ }
1138
+ }
1139
+ return spans;
1140
+ }
1141
+ };
1142
+ var GeminiAdapter = class extends GeminiFamilyAdapter {
1143
+ constructor() {
1144
+ super({ harness: "gemini", service: "gemini", homeDirName: ".gemini", aliases: ["gemini-cli"] });
1145
+ }
1146
+ };
1147
+
1148
+ // src/adapters/opencode.ts
1149
+ import { readdir as readdir8, readFile as readFile8, stat as stat8 } from "fs/promises";
1150
+ import { homedir as homedir8 } from "os";
1151
+ import { join as join9 } from "path";
1152
+ var SERVICE7 = "opencode";
1153
+ function isoOf(ms2) {
1154
+ return new Date(ms2 ?? 0).toISOString();
1155
+ }
1156
+ var OpencodeAdapter = class {
1157
+ harness = "opencode";
1158
+ storage() {
1159
+ const base = process.env.XDG_DATA_HOME ?? join9(homedir8(), ".local", "share");
1160
+ return join9(base, "opencode", "storage");
1161
+ }
1162
+ async locate(opts = {}) {
1163
+ const msgRoot = join9(this.storage(), "message");
1164
+ let sessionDirs;
1165
+ try {
1166
+ sessionDirs = await readdir8(msgRoot);
1167
+ } catch {
1168
+ return [];
1169
+ }
1170
+ const refs = [];
1171
+ for (const sid of sessionDirs) {
1172
+ const dir = join9(msgRoot, sid);
1173
+ let st;
1174
+ try {
1175
+ st = await stat8(dir);
1176
+ } catch {
1177
+ continue;
1178
+ }
1179
+ if (!st.isDirectory()) continue;
1180
+ if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
1181
+ if (opts.cwd) continue;
1182
+ refs.push({ harness: this.harness, sessionId: sid, path: dir, cwd: null, mtimeMs: st.mtimeMs });
1183
+ }
1184
+ return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
1185
+ }
1186
+ async parse(ref) {
1187
+ const traceId = ref.sessionId;
1188
+ const rootId = `root:${traceId}`;
1189
+ const partRoot = join9(this.storage(), "part");
1190
+ let msgFiles;
1191
+ try {
1192
+ msgFiles = (await readdir8(ref.path)).filter((f) => f.endsWith(".json"));
1193
+ } catch {
1194
+ return [];
1195
+ }
1196
+ const messages = [];
1197
+ for (const f of msgFiles) {
1198
+ try {
1199
+ messages.push({ msg: JSON.parse(await readFile8(join9(ref.path, f), "utf8")), file: f });
1200
+ } catch {
1201
+ }
1202
+ }
1203
+ messages.sort((a, b) => (a.msg.time?.created ?? 0) - (b.msg.time?.created ?? 0));
1204
+ const spans = [
1205
+ span({
1206
+ traceId,
1207
+ spanId: rootId,
1208
+ parentSpanId: null,
1209
+ name: "session",
1210
+ kind: "AGENT",
1211
+ startTime: isoOf(messages[0]?.msg.time?.created),
1212
+ endTime: isoOf(messages.at(-1)?.msg.time?.completed ?? messages.at(-1)?.msg.time?.created),
1213
+ service: SERVICE7,
1214
+ agent: SERVICE7
1215
+ })
1216
+ ];
1217
+ let step = 0;
1218
+ for (const { msg } of messages) {
1219
+ const mid = msg.id ?? `m${step}`;
1220
+ const llmId = `llm:${mid}`;
1221
+ spans.push(
1222
+ span({
1223
+ traceId,
1224
+ spanId: llmId,
1225
+ parentSpanId: rootId,
1226
+ name: `message.${msg.role ?? "unknown"}`,
1227
+ kind: "LLM",
1228
+ startTime: isoOf(msg.time?.created),
1229
+ endTime: isoOf(msg.time?.completed ?? msg.time?.created),
1230
+ service: SERVICE7,
1231
+ agent: SERVICE7,
1232
+ model: msg.modelID ?? null,
1233
+ inputTokens: msg.tokens?.input ?? null,
1234
+ outputTokens: msg.tokens?.output ?? null,
1235
+ step
1236
+ })
1237
+ );
1238
+ step += 1;
1239
+ const pdir = join9(partRoot, mid);
1240
+ let partFiles;
1241
+ try {
1242
+ partFiles = (await readdir8(pdir)).filter((f) => f.endsWith(".json"));
1243
+ } catch {
1244
+ continue;
1245
+ }
1246
+ for (const pf of partFiles) {
1247
+ let part;
1248
+ try {
1249
+ part = JSON.parse(await readFile8(join9(pdir, pf), "utf8"));
1250
+ } catch {
1251
+ continue;
1252
+ }
1253
+ if (part.type !== "tool" || !part.tool) continue;
1254
+ const err = part.state?.status === "error";
1255
+ spans.push(
1256
+ span({
1257
+ traceId,
1258
+ spanId: `tool:${part.callID ?? `${mid}:${step}`}`,
1259
+ parentSpanId: llmId,
1260
+ name: `tool.${part.tool}`,
1261
+ kind: "TOOL",
1262
+ startTime: isoOf(msg.time?.created),
1263
+ endTime: isoOf(msg.time?.completed ?? msg.time?.created),
1264
+ status: err ? "ERROR" : "OK",
1265
+ statusMessage: err ? (part.state?.error ?? "").slice(0, 500) : void 0,
1266
+ service: SERVICE7,
1267
+ agent: SERVICE7,
1268
+ tool: part.tool,
1269
+ step,
1270
+ content: part.state?.input != null ? JSON.stringify(part.state.input) : null
1271
+ })
1272
+ );
1273
+ step += 1;
1274
+ }
1275
+ }
1276
+ return spans;
1277
+ }
1278
+ };
1279
+
1280
+ // src/adapters/pi.ts
1281
+ import { readdir as readdir9, readFile as readFile9, stat as stat9 } from "fs/promises";
1282
+ import { homedir as homedir9 } from "os";
1283
+ import { basename as basename6, join as join10 } from "path";
1284
+ var SERVICE8 = "pi";
1285
+ function parseLines5(raw) {
1286
+ const out = [];
1287
+ for (const line of raw.split("\n")) {
1288
+ if (!line) continue;
1289
+ try {
1290
+ out.push(JSON.parse(line));
1291
+ } catch {
1292
+ }
1293
+ }
1294
+ return out;
1295
+ }
1296
+ function isToolBlock(b) {
1297
+ return typeof b.type === "string" && /tool[_-]?call|tool[_-]?use|^tool$/i.test(b.type);
1298
+ }
1299
+ function isToolResultBlock(b) {
1300
+ return typeof b.type === "string" && /tool[_-]?result|tool[_-]?output/i.test(b.type);
1301
+ }
1302
+ function textOf(content) {
1303
+ if (!content) return "";
1304
+ return content.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
1305
+ }
1306
+ var PiAdapter = class {
1307
+ harness = "pi";
1308
+ root() {
1309
+ return join10(homedir9(), ".pi", "agent", "sessions");
1310
+ }
1311
+ async locate(opts = {}) {
1312
+ const root = this.root();
1313
+ let dirs;
1314
+ try {
1315
+ dirs = await readdir9(root);
1316
+ } catch {
1317
+ return [];
1318
+ }
1319
+ const refs = [];
1320
+ for (const dir of dirs) {
1321
+ const dp = join10(root, dir);
1322
+ let files;
1323
+ try {
1324
+ files = await readdir9(dp);
1325
+ } catch {
1326
+ continue;
1327
+ }
1328
+ const cwd = `/${dir.replace(/^-+/, "").replace(/-+$/, "").replace(/-/g, "/")}`;
1329
+ if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
1330
+ for (const f of files) {
1331
+ if (!f.endsWith(".jsonl")) continue;
1332
+ const path = join10(dp, f);
1333
+ let st;
1334
+ try {
1335
+ st = await stat9(path);
1336
+ } catch {
1337
+ continue;
1338
+ }
1339
+ if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
1340
+ const id = basename6(f, ".jsonl").replace(/^[\dTZ.-]+_/, "");
1341
+ refs.push({ harness: this.harness, sessionId: id, path, cwd, mtimeMs: st.mtimeMs });
1342
+ }
1343
+ }
1344
+ return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
1345
+ }
1346
+ async parse(ref) {
1347
+ const lines = parseLines5(await readFile9(ref.path, "utf8"));
1348
+ const sessionLine = lines.find((l) => l.type === "session");
1349
+ const traceId = sessionLine?.id ?? ref.sessionId;
1350
+ const rootId = `root:${traceId}`;
1351
+ const spans = [
1352
+ span({
1353
+ traceId,
1354
+ spanId: rootId,
1355
+ parentSpanId: null,
1356
+ name: "session",
1357
+ kind: "AGENT",
1358
+ startTime: sessionLine?.timestamp ?? lines[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
1359
+ endTime: lines.at(-1)?.timestamp,
1360
+ service: SERVICE8,
1361
+ agent: SERVICE8
1362
+ })
1363
+ ];
1364
+ const toolByCallId = /* @__PURE__ */ new Map();
1365
+ let step = 0;
1366
+ for (const l of lines) {
1367
+ if (l.type !== "message" || !l.message) continue;
1368
+ const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
1369
+ const mid = l.id ?? `m${step}`;
1370
+ const msg = l.message;
1371
+ const llmId = `llm:${mid}`;
1372
+ const errored = !!msg.errorMessage;
1373
+ spans.push(
1374
+ span({
1375
+ traceId,
1376
+ spanId: llmId,
1377
+ parentSpanId: rootId,
1378
+ name: `message.${msg.role ?? "unknown"}`,
1379
+ kind: "LLM",
1380
+ startTime: ts,
1381
+ status: errored ? "ERROR" : "OK",
1382
+ statusMessage: errored ? msg.errorMessage.slice(0, 500) : void 0,
1383
+ service: SERVICE8,
1384
+ agent: SERVICE8,
1385
+ model: msg.model ?? null,
1386
+ inputTokens: msg.usage?.input ?? null,
1387
+ outputTokens: msg.usage?.output ?? null,
1388
+ step,
1389
+ content: textOf(msg.content).slice(0, 8e3) || null
1390
+ })
1391
+ );
1392
+ step += 1;
1393
+ for (const b of msg.content ?? []) {
1394
+ if (isToolBlock(b)) {
1395
+ const name = b.toolName ?? b.name ?? "tool";
1396
+ const callId = b.id ?? b.callId ?? b.toolCallId ?? `${mid}:${step}`;
1397
+ const toolSpan = span({
1398
+ traceId,
1399
+ spanId: `tool:${callId}`,
1400
+ parentSpanId: llmId,
1401
+ name: `tool.${name}`,
1402
+ kind: "TOOL",
1403
+ startTime: ts,
1404
+ service: SERVICE8,
1405
+ agent: SERVICE8,
1406
+ tool: name,
1407
+ step,
1408
+ content: (b.input ?? b.args) != null ? JSON.stringify(b.input ?? b.args) : null
1409
+ });
1410
+ spans.push(toolSpan);
1411
+ toolByCallId.set(callId, toolSpan);
1412
+ step += 1;
1413
+ } else if (isToolResultBlock(b)) {
1414
+ const callId = b.toolCallId ?? b.callId ?? b.id ?? "";
1415
+ const t = toolByCallId.get(callId);
1416
+ if (t) {
1417
+ const err = b.isError === true || b.is_error === true;
1418
+ t.end_time = ts;
1419
+ t.status = err ? { code: "ERROR", message: "tool result reported error" } : { code: "OK" };
1420
+ }
1421
+ }
1422
+ }
1423
+ }
1424
+ return spans;
1425
+ }
1426
+ };
1427
+
1428
+ // src/adapters/qwen.ts
1429
+ import { readdir as readdir10, readFile as readFile10, stat as stat10 } from "fs/promises";
1430
+ import { homedir as homedir10 } from "os";
1431
+ import { basename as basename7, join as join11 } from "path";
1432
+ var SERVICE9 = "qwen";
1433
+ function parseLines6(raw) {
1434
+ const out = [];
1435
+ for (const line of raw.split("\n")) {
1436
+ if (!line) continue;
1437
+ try {
1438
+ out.push(JSON.parse(line));
1439
+ } catch {
1440
+ }
1441
+ }
1442
+ return out;
1443
+ }
1444
+ function textOf2(parts) {
1445
+ if (!parts) return "";
1446
+ return parts.filter((p) => typeof p.text === "string").map((p) => p.text).join("");
1447
+ }
1448
+ var QwenAdapter = class {
1449
+ harness = "qwen";
1450
+ aliases = ["qwen-code"];
1451
+ root() {
1452
+ return join11(homedir10(), ".qwen", "projects");
1453
+ }
1454
+ async locate(opts = {}) {
1455
+ const root = this.root();
1456
+ let projectDirs;
1457
+ try {
1458
+ projectDirs = await readdir10(root);
1459
+ } catch {
1460
+ return [];
1461
+ }
1462
+ const refs = [];
1463
+ for (const pd of projectDirs) {
1464
+ const chatsDir = join11(root, pd, "chats");
1465
+ let files;
1466
+ try {
1467
+ files = await readdir10(chatsDir);
1468
+ } catch {
1469
+ continue;
1470
+ }
1471
+ if (opts.cwd) continue;
1472
+ for (const f of files) {
1473
+ if (!f.endsWith(".jsonl")) continue;
1474
+ const path = join11(chatsDir, f);
1475
+ let st;
1476
+ try {
1477
+ st = await stat10(path);
1478
+ } catch {
1479
+ continue;
1480
+ }
1481
+ if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
1482
+ refs.push({ harness: this.harness, sessionId: basename7(f, ".jsonl"), path, cwd: null, mtimeMs: st.mtimeMs });
1483
+ }
1484
+ }
1485
+ return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
1486
+ }
1487
+ async parse(ref) {
1488
+ const records = parseLines6(await readFile10(ref.path, "utf8"));
1489
+ const traceId = records.find((r) => r.sessionId)?.sessionId ?? ref.sessionId;
1490
+ const rootId = `root:${traceId}`;
1491
+ const spans = [
1492
+ span({
1493
+ traceId,
1494
+ spanId: rootId,
1495
+ parentSpanId: null,
1496
+ name: "session",
1497
+ kind: "AGENT",
1498
+ startTime: records[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
1499
+ endTime: records.at(-1)?.timestamp,
1500
+ service: SERVICE9,
1501
+ agent: SERVICE9
1502
+ })
1503
+ ];
1504
+ const openToolsByName = /* @__PURE__ */ new Map();
1505
+ let step = 0;
1506
+ let lastLlm = rootId;
1507
+ for (const r of records) {
1508
+ const ts = r.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
1509
+ const role = r.message?.role;
1510
+ if (r.type === "assistant" || role === "model") {
1511
+ const llmId = `llm:${r.uuid ?? step}`;
1512
+ spans.push(
1513
+ span({
1514
+ traceId,
1515
+ spanId: llmId,
1516
+ parentSpanId: rootId,
1517
+ name: "llm.turn",
1518
+ kind: "LLM",
1519
+ startTime: ts,
1520
+ service: SERVICE9,
1521
+ agent: SERVICE9,
1522
+ model: r.model ?? null,
1523
+ inputTokens: r.usageMetadata?.promptTokenCount ?? null,
1524
+ outputTokens: r.usageMetadata?.candidatesTokenCount ?? null,
1525
+ step,
1526
+ content: textOf2(r.message?.parts).slice(0, 8e3) || null
1527
+ })
1528
+ );
1529
+ lastLlm = llmId;
1530
+ step += 1;
1531
+ for (const p of r.message?.parts ?? []) {
1532
+ if (!p.functionCall?.name) continue;
1533
+ const name = p.functionCall.name;
1534
+ const t = span({
1535
+ traceId,
1536
+ spanId: `tool:${name}:${step}`,
1537
+ parentSpanId: llmId,
1538
+ name: `tool.${name}`,
1539
+ kind: "TOOL",
1540
+ startTime: ts,
1541
+ service: SERVICE9,
1542
+ agent: SERVICE9,
1543
+ tool: name,
1544
+ step,
1545
+ content: p.functionCall.args != null ? JSON.stringify(p.functionCall.args) : null
1546
+ });
1547
+ spans.push(t);
1548
+ const q = openToolsByName.get(name) ?? [];
1549
+ q.push(t);
1550
+ openToolsByName.set(name, q);
1551
+ step += 1;
1552
+ }
1553
+ } else if (r.type === "tool_result") {
1554
+ const err = r.toolCallResult?.status === "error" || r.toolCallResult?.error != null;
1555
+ for (const p of r.message?.parts ?? []) {
1556
+ const name = p.functionResponse?.name;
1557
+ if (!name) continue;
1558
+ const t = openToolsByName.get(name)?.shift();
1559
+ if (t) {
1560
+ t.end_time = ts;
1561
+ t.status = err ? { code: "ERROR", message: "tool result reported error" } : { code: "OK" };
1562
+ }
1563
+ }
1564
+ }
1565
+ }
1566
+ return spans;
1567
+ }
1568
+ };
1569
+
1570
+ // src/registry.ts
1571
+ var ADAPTERS = [
1572
+ new ClaudeAdapter(),
1573
+ new CodexAdapter(),
1574
+ new OpencodeAdapter(),
1575
+ new GeminiAdapter(),
1576
+ new QwenAdapter(),
1577
+ new FactoryAdapter(),
1578
+ new PiAdapter(),
1579
+ new AmpAdapter(),
1580
+ new CopilotAdapter(),
1581
+ new ForgeAdapter()
1582
+ ];
1583
+ var BY_NAME = /* @__PURE__ */ new Map();
1584
+ for (const a of ADAPTERS) {
1585
+ BY_NAME.set(a.harness, a);
1586
+ for (const alias of a.aliases ?? []) BY_NAME.set(alias, a);
1587
+ }
1588
+ function listAdapters() {
1589
+ return ADAPTERS;
1590
+ }
1591
+ function resolveAdapter(harness) {
1592
+ return BY_NAME.get(harness) ?? BY_NAME.get(harness.toLowerCase());
1593
+ }
1594
+ function knownHarnesses() {
1595
+ return [...BY_NAME.keys()].sort();
1596
+ }
1597
+
1598
+ // src/report.ts
1599
+ var SEVERITY_RANK = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
1600
+ var SEVERITY_BADGE = {
1601
+ critical: "\u{1F534} CRITICAL",
1602
+ high: "\u{1F7E0} HIGH",
1603
+ medium: "\u{1F7E1} MEDIUM",
1604
+ low: "\u{1F535} LOW",
1605
+ info: "\u2139\uFE0F INFO"
1606
+ };
1607
+ function renderReport(result, meta) {
1608
+ const lines = [];
1609
+ const findings = [...result.findings].sort(
1610
+ (a, b) => (SEVERITY_RANK[a.severity] ?? 9) - (SEVERITY_RANK[b.severity] ?? 9)
1611
+ );
1612
+ lines.push(`# Trace analysis \u2014 ${meta.harness}`);
1613
+ lines.push("");
1614
+ lines.push(
1615
+ `${meta.sessionCount} session(s), ${meta.spanCount} spans \u2192 **${findings.length} findings** across ${result.per_analyst.length} analyst(s). Cost: $${result.total_cost_usd.toFixed(4)}.`
1616
+ );
1617
+ lines.push("");
1618
+ lines.push("| Analyst | Status | Findings | Latency |");
1619
+ lines.push("|---|---|---|---|");
1620
+ for (const s of result.per_analyst) {
1621
+ lines.push(`| \`${s.analyst_id}\` | ${s.status} | ${s.findings_count} | ${s.latency_ms}ms |`);
1622
+ }
1623
+ lines.push("");
1624
+ if (findings.length === 0) {
1625
+ lines.push("_No findings \u2014 no behavioral inefficiencies or failure modes detected._");
1626
+ } else {
1627
+ const byArea = /* @__PURE__ */ new Map();
1628
+ for (const f of findings) {
1629
+ const arr = byArea.get(f.area) ?? [];
1630
+ arr.push(f);
1631
+ byArea.set(f.area, arr);
1632
+ }
1633
+ for (const [area, arr] of byArea) {
1634
+ lines.push(`## ${area} (${arr.length})`);
1635
+ lines.push("");
1636
+ for (const f of arr) {
1637
+ lines.push(`### ${SEVERITY_BADGE[f.severity] ?? f.severity} \u2014 ${f.claim}`);
1638
+ lines.push("");
1639
+ if (f.subject) lines.push(`- **Subject:** \`${f.subject}\``);
1640
+ lines.push(`- **Confidence:** ${f.confidence}`);
1641
+ if (f.recommended_action) lines.push(`- **Fix:** ${f.recommended_action}`);
1642
+ for (const ev of f.evidence_refs.slice(0, 3)) {
1643
+ const excerpt = ev.excerpt ? ` \u2014 \`${ev.excerpt.slice(0, 200)}\`` : "";
1644
+ lines.push(`- **Evidence:** ${ev.kind} ${ev.uri}${excerpt}`);
1645
+ }
1646
+ lines.push("");
1647
+ }
1648
+ }
1649
+ }
1650
+ lines.push("---");
1651
+ lines.push(`OTLP artifact: \`${meta.otlpPath}\` \u2014 also runnable with \`halo ${meta.otlpPath} -p "diagnose"\`.`);
1652
+ lines.push("");
1653
+ return lines.join("\n");
1654
+ }
1655
+ function renderPipelines(pr) {
1656
+ const lines = ["## loops & waste (deterministic)", ""];
1657
+ if (pr.stuckLoops.findings.length === 0) {
1658
+ lines.push("- **Stuck loops:** none (no tool called \u22653\xD7 with identical args).");
1659
+ } else {
1660
+ lines.push(`- **Stuck loops:** ${pr.stuckLoops.findings.length} (${(pr.stuckLoops.affectedRunRatio * 100).toFixed(0)}% of runs affected)`);
1661
+ for (const f of pr.stuckLoops.findings.sort((a, b) => b.occurrences - a.occurrences).slice(0, 10)) {
1662
+ lines.push(` - \u{1F501} \`${f.toolName}\` \xD7${f.occurrences} with identical args over ${(f.windowMs / 1e3).toFixed(1)}s`);
1663
+ }
1664
+ }
1665
+ for (const m of pr.toolUse) {
1666
+ if (m.totalCalls === 0) continue;
1667
+ lines.push(
1668
+ `- **Tool use** (${m.totalCalls} calls): ${(m.duplicateRate * 100).toFixed(0)}% duplicate, ${(m.retryRate * 100).toFixed(0)}% retry, ${(m.errorRate * 100).toFixed(0)}% error`
1669
+ );
1670
+ }
1671
+ lines.push("");
1672
+ return lines.join("\n");
1673
+ }
1674
+
1675
+ // src/cli.ts
1676
+ function parseArgs(argv) {
1677
+ const a = {
1678
+ command: argv[0] ?? "help",
1679
+ harness: "claude-code",
1680
+ all: false,
1681
+ last: 0,
1682
+ llm: false,
1683
+ interval: 5,
1684
+ window: 30,
1685
+ minLoop: 3
1686
+ };
1687
+ for (let i = 1; i < argv.length; i++) {
1688
+ const arg = argv[i];
1689
+ const next = () => argv[++i];
1690
+ switch (arg) {
1691
+ case "--harness":
1692
+ a.harness = next() ?? a.harness;
1693
+ break;
1694
+ case "--all":
1695
+ a.all = true;
1696
+ break;
1697
+ case "--last":
1698
+ a.last = Number(next());
1699
+ break;
1700
+ case "--session":
1701
+ a.session = next();
1702
+ break;
1703
+ case "--cwd":
1704
+ a.cwd = next();
1705
+ break;
1706
+ case "--since":
1707
+ a.since = next();
1708
+ break;
1709
+ case "--out":
1710
+ a.out = next();
1711
+ break;
1712
+ case "--otlp":
1713
+ a.otlp = next();
1714
+ break;
1715
+ case "--llm":
1716
+ a.llm = true;
1717
+ break;
1718
+ case "--budget":
1719
+ a.budget = Number(next());
1720
+ break;
1721
+ case "--interval":
1722
+ a.interval = Number(next());
1723
+ break;
1724
+ case "--window":
1725
+ a.window = Number(next());
1726
+ break;
1727
+ case "--min-loop":
1728
+ a.minLoop = Number(next());
1729
+ break;
1730
+ default:
1731
+ if (arg?.startsWith("--")) throw new Error(`unknown flag: ${arg}`);
1732
+ }
1733
+ }
1734
+ return a;
1735
+ }
1736
+ function adaptersFor(args) {
1737
+ if (args.all) return [...listAdapters()];
1738
+ const adapter = resolveAdapter(args.harness);
1739
+ if (!adapter) {
1740
+ throw new Error(`unknown harness "${args.harness}". Known: ${knownHarnesses().join(", ")}`);
1741
+ }
1742
+ return [adapter];
1743
+ }
1744
+ async function discover(args) {
1745
+ const sinceMs = args.since ? Date.parse(args.since) : void 0;
1746
+ const out = [];
1747
+ for (const adapter of adaptersFor(args)) {
1748
+ let refs = await adapter.locate({ cwd: args.cwd, sinceMs });
1749
+ if (args.last > 0) refs = refs.slice(0, args.last);
1750
+ out.push({ adapter, refs });
1751
+ }
1752
+ return out;
1753
+ }
1754
+ async function buildAxService() {
1755
+ const apiKey = process.env.OPENAI_API_KEY;
1756
+ if (!apiKey) {
1757
+ throw new Error("--llm needs OPENAI_API_KEY for the agentic analyst kinds (deterministic analysis needs no key)");
1758
+ }
1759
+ const { AxAI } = await import("@ax-llm/ax");
1760
+ return new AxAI({ name: "openai", apiKey });
1761
+ }
1762
+ async function cmdList(args) {
1763
+ const groups = await discover({ ...args, last: args.last || 20 });
1764
+ for (const { adapter, refs } of groups) {
1765
+ console.log(`
1766
+ ${adapter.harness} \u2014 ${refs.length} session(s)`);
1767
+ for (const r of refs) {
1768
+ console.log(` ${new Date(r.mtimeMs).toISOString()} ${r.sessionId} ${r.cwd ?? ""}`);
1769
+ }
1770
+ }
1771
+ }
1772
+ async function collectSpans(args) {
1773
+ if (args.session) {
1774
+ const adapter = resolveAdapter(args.harness);
1775
+ if (!adapter) throw new Error(`unknown harness "${args.harness}"`);
1776
+ const st = await stat11(args.session);
1777
+ const ref = {
1778
+ harness: adapter.harness,
1779
+ sessionId: args.session,
1780
+ path: args.session,
1781
+ cwd: null,
1782
+ mtimeMs: st.mtimeMs
1783
+ };
1784
+ return { spans: await adapter.parse(ref), harness: adapter.harness, sessionCount: 1 };
1785
+ }
1786
+ const groups = await discover({ ...args, last: args.last || 1 });
1787
+ const spans = [];
1788
+ let sessionCount = 0;
1789
+ const harnesses = [];
1790
+ for (const { adapter, refs } of groups) {
1791
+ if (refs.length > 0) harnesses.push(adapter.harness);
1792
+ for (const ref of refs) {
1793
+ spans.push(...await adapter.parse(ref));
1794
+ sessionCount += 1;
1795
+ }
1796
+ }
1797
+ return { spans, harness: harnesses.join("+") || args.harness, sessionCount };
1798
+ }
1799
+ async function cmdConvert(args) {
1800
+ const { spans } = await collectSpans(args);
1801
+ if (spans.length === 0) throw new Error("no spans found for the given selection");
1802
+ const path = await writeOtlp(spans, args.otlp);
1803
+ console.log(`wrote ${spans.length} spans \u2192 ${path}`);
1804
+ }
1805
+ async function cmdAnalyze(args) {
1806
+ const { spans, harness, sessionCount } = await collectSpans(args);
1807
+ if (spans.length === 0) throw new Error("no spans found for the given selection");
1808
+ const ai = args.llm ? await buildAxService() : void 0;
1809
+ const { otlpPath, result } = await analyzeSpans(spans, {
1810
+ ai,
1811
+ budgetUsd: args.budget,
1812
+ otlpOutPath: args.otlp,
1813
+ log: (msg) => process.stderr.write(`${msg}
1814
+ `)
1815
+ });
1816
+ const pipelines = await runPipelines(spans, { minLoopOccurrences: args.minLoop });
1817
+ const report = `${renderReport(result, { harness, sessionCount, spanCount: spans.length, otlpPath })}
1818
+ ${renderPipelines(pipelines)}`;
1819
+ if (args.out) {
1820
+ await writeFile2(args.out, report, "utf8");
1821
+ console.log(`report \u2192 ${args.out} (${result.findings.length} findings, ${pipelines.stuckLoops.findings.length} loops, OTLP: ${otlpPath})`);
1822
+ } else {
1823
+ console.log(report);
1824
+ }
1825
+ }
1826
+ function loopKey(sessionId, toolName, argHash) {
1827
+ return `${sessionId}|${toolName}|${argHash}`;
1828
+ }
1829
+ var sleep = (ms2) => new Promise((r) => setTimeout(r, ms2));
1830
+ async function cmdWatch(args) {
1831
+ if (!args.all && !args.cwd) args.all = true;
1832
+ const seen = /* @__PURE__ */ new Map();
1833
+ const intervalMs = Math.max(1, args.interval) * 1e3;
1834
+ process.stderr.write(
1835
+ `traces watch \u2014 observing ${args.all ? "all harnesses" : args.harness}, sessions active in the last ${args.window}m, every ${args.interval}s. Read-only; Ctrl-C to stop.
1836
+ `
1837
+ );
1838
+ for (; ; ) {
1839
+ const sinceMs = Date.now() - args.window * 6e4;
1840
+ for (const adapter of adaptersFor(args)) {
1841
+ let refs;
1842
+ try {
1843
+ refs = await adapter.locate({ cwd: args.cwd, sinceMs });
1844
+ } catch {
1845
+ continue;
1846
+ }
1847
+ for (const ref of refs) {
1848
+ let report;
1849
+ try {
1850
+ const spans = await adapter.parse(ref);
1851
+ if (spans.length === 0) continue;
1852
+ report = await runPipelines(spans, { minLoopOccurrences: args.minLoop });
1853
+ } catch {
1854
+ continue;
1855
+ }
1856
+ for (const f of report.stuckLoops.findings) {
1857
+ const key = loopKey(ref.sessionId, f.toolName, f.argHash);
1858
+ const prior = seen.get(key) ?? 0;
1859
+ if (f.occurrences > prior) {
1860
+ seen.set(key, f.occurrences);
1861
+ const ts = (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
1862
+ process.stdout.write(
1863
+ `\u26A0\uFE0F ${ts} [${adapter.harness}] ${ref.sessionId.slice(0, 8)} \u2014 \`${f.toolName}\` repeated \xD7${f.occurrences} with identical args (${(f.windowMs / 1e3).toFixed(0)}s)${ref.cwd ? ` \xB7 ${ref.cwd}` : ""}
1864
+ `
1865
+ );
1866
+ }
1867
+ }
1868
+ }
1869
+ }
1870
+ await sleep(intervalMs);
1871
+ }
1872
+ }
1873
+ function usage() {
1874
+ console.log(`traces \u2014 analyze & observe coding-agent sessions
1875
+
1876
+ Commands:
1877
+ list List discovered sessions
1878
+ analyze Run analyst suite + loop/waste pipelines, write a markdown report
1879
+ convert Emit OTLP-JSONL only (also feeds HALO)
1880
+ watch Online observer: tail active sessions, notify on stuck loops (read-only)
1881
+
1882
+ Options:
1883
+ --harness <id> Harness or alias (default: claude-code). Known: ${knownHarnesses().join(", ")}
1884
+ --all Sweep every known harness
1885
+ --last <n> Most-recent N sessions
1886
+ --session <path> Analyze one explicit session file
1887
+ --cwd <dir> Filter sessions by working directory
1888
+ --since <iso> Only sessions modified since this time
1889
+ --out <path> Write report to a file
1890
+ --otlp <path> OTLP artifact path
1891
+ --llm Enable agentic RLM analysts (needs OPENAI_API_KEY)
1892
+ --budget <usd> USD cap for agentic analysts
1893
+ --interval <s> watch: poll interval seconds (default 5)
1894
+ --window <m> watch: only sessions active in the last N minutes (default 30)
1895
+ --min-loop <n> Min identical repeated calls to flag a loop (default 3)`);
1896
+ }
1897
+ async function main() {
1898
+ const args = parseArgs(process.argv.slice(2));
1899
+ switch (args.command) {
1900
+ case "list":
1901
+ await cmdList(args);
1902
+ break;
1903
+ case "analyze":
1904
+ await cmdAnalyze(args);
1905
+ break;
1906
+ case "convert":
1907
+ await cmdConvert(args);
1908
+ break;
1909
+ case "watch":
1910
+ await cmdWatch(args);
1911
+ break;
1912
+ default:
1913
+ usage();
1914
+ }
1915
+ }
1916
+ main().catch((err) => {
1917
+ process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
1918
+ `);
1919
+ process.exitCode = 1;
1920
+ });