@tangle-network/traces 0.8.0 → 0.8.5

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.
@@ -1,3 +1,110 @@
1
+ // src/adoption.ts
2
+ import { readFile } from "fs/promises";
3
+ import { join } from "path";
4
+ function toolName(s) {
5
+ const n = s.attributes["tool.name"];
6
+ if (typeof n === "string") return n;
7
+ if (s.name.startsWith("tool.")) return s.name.slice("tool.".length);
8
+ return null;
9
+ }
10
+ function parseInput(s) {
11
+ const c = s.attributes.content;
12
+ if (typeof c !== "string") return {};
13
+ try {
14
+ const v = JSON.parse(c);
15
+ return v && typeof v === "object" ? v : {};
16
+ } catch {
17
+ return {};
18
+ }
19
+ }
20
+ function skillNameOf(input) {
21
+ const v = input.skill ?? input.command;
22
+ return typeof v === "string" && v.length > 0 ? v : "?";
23
+ }
24
+ function subagentTypeOf(input) {
25
+ const v = input.subagent_type;
26
+ return typeof v === "string" && v.length > 0 ? v : "?";
27
+ }
28
+ function countSkillRunsJsonl(raw) {
29
+ const out = {};
30
+ for (const line of raw.split("\n")) {
31
+ const t = line.trim();
32
+ if (!t) continue;
33
+ let o;
34
+ try {
35
+ o = JSON.parse(t);
36
+ } catch {
37
+ continue;
38
+ }
39
+ if (!o || typeof o !== "object") continue;
40
+ const rec = o;
41
+ const names = [];
42
+ if (Array.isArray(rec.skills)) {
43
+ for (const s of rec.skills) if (typeof s === "string" && s) names.push(s);
44
+ }
45
+ if (typeof rec.skill === "string" && rec.skill) names.push(rec.skill);
46
+ if (names.length === 0) names.push("?");
47
+ for (const n of names) out[n] = (out[n] ?? 0) + 1;
48
+ }
49
+ return out;
50
+ }
51
+ async function readLoopRuns(cwds) {
52
+ const counts = {};
53
+ let filesRead = 0;
54
+ const seen = /* @__PURE__ */ new Set();
55
+ for (const cwd of cwds) {
56
+ if (!cwd || seen.has(cwd)) continue;
57
+ seen.add(cwd);
58
+ const path = join(cwd, ".evolve", "skill-runs.jsonl");
59
+ let raw;
60
+ try {
61
+ raw = await readFile(path, "utf8");
62
+ } catch {
63
+ continue;
64
+ }
65
+ filesRead += 1;
66
+ for (const [k, v] of Object.entries(countSkillRunsJsonl(raw))) counts[k] = (counts[k] ?? 0) + v;
67
+ }
68
+ return { counts, filesRead };
69
+ }
70
+ async function analyzeAdoption(spans, opts = {}) {
71
+ const sessions = /* @__PURE__ */ new Set();
72
+ const sessionsWithSkill = /* @__PURE__ */ new Set();
73
+ const sessionsWithSubagent = /* @__PURE__ */ new Set();
74
+ const skillInvocations = {};
75
+ const subagentSpawns = {};
76
+ for (const s of spans) {
77
+ sessions.add(s.trace_id);
78
+ const tn = toolName(s);
79
+ if (tn === "Skill") {
80
+ const name = skillNameOf(parseInput(s));
81
+ skillInvocations[name] = (skillInvocations[name] ?? 0) + 1;
82
+ sessionsWithSkill.add(s.trace_id);
83
+ } else if (tn === "Task" || tn === "Agent") {
84
+ const type = subagentTypeOf(parseInput(s));
85
+ subagentSpawns[type] = (subagentSpawns[type] ?? 0) + 1;
86
+ sessionsWithSubagent.add(s.trace_id);
87
+ }
88
+ }
89
+ const { counts: loopDispatchedRuns, filesRead } = await readLoopRuns(opts.cwds ?? []);
90
+ const totalSkillInvocations = Object.values(skillInvocations).reduce((a, b) => a + b, 0);
91
+ const totalSubagentSpawns = Object.values(subagentSpawns).reduce((a, b) => a + b, 0);
92
+ const totalLoopDispatchedRuns = Object.values(loopDispatchedRuns).reduce((a, b) => a + b, 0);
93
+ return {
94
+ sessionCount: sessions.size,
95
+ sessionsWithSkill: sessionsWithSkill.size,
96
+ skillPenetration: sessions.size === 0 ? 0 : sessionsWithSkill.size / sessions.size,
97
+ skillInvocations,
98
+ totalSkillInvocations,
99
+ subagentSpawns,
100
+ totalSubagentSpawns,
101
+ sessionsWithSubagent: sessionsWithSubagent.size,
102
+ loopDispatchedRuns,
103
+ totalLoopDispatchedRuns,
104
+ skillRunFilesRead: filesRead
105
+ };
106
+ }
107
+
1
108
  // src/otlp.ts
2
109
  function span(input) {
3
110
  const attributes = {
@@ -30,6 +137,9 @@ function toOpenInferenceSpan(s) {
30
137
  const resourceAttrs = {};
31
138
  if (a["service.name"] != null) resourceAttrs["service.name"] = a["service.name"];
32
139
  if (a["agent.name"] != null) resourceAttrs["agent.name"] = a["agent.name"];
140
+ for (const k of ["tangle.subject.key", "git.repository", "git.branch", "git.commit", "tangle.cwd"]) {
141
+ if (a[k] != null) resourceAttrs[k] = a[k];
142
+ }
33
143
  return {
34
144
  trace_id: s.trace_id,
35
145
  span_id: s.span_id,
@@ -50,11 +160,11 @@ function serializeSpans(spans) {
50
160
  `;
51
161
  }
52
162
  async function writeOtlpFile(spans, outPath) {
53
- const { mkdtemp, writeFile: writeFile3 } = await import("fs/promises");
54
- const { tmpdir } = await import("os");
55
- const { join: join13 } = await import("path");
56
- const path = outPath ?? join13(await mkdtemp(join13(tmpdir(), "traces-")), "spans.otlp.jsonl");
57
- await writeFile3(path, serializeSpans(spans), "utf8");
163
+ const { mkdtemp: mkdtemp3, writeFile: writeFile4 } = await import("fs/promises");
164
+ const { tmpdir: tmpdir3 } = await import("os");
165
+ const { join: join16 } = await import("path");
166
+ const path = outPath ?? join16(await mkdtemp3(join16(tmpdir3(), "traces-")), "spans.otlp.jsonl");
167
+ await writeFile4(path, serializeSpans(spans), "utf8");
58
168
  return path;
59
169
  }
60
170
 
@@ -89,150 +199,37 @@ async function analyzeSpans(spans, opts = {}) {
89
199
  return { otlpPath, result };
90
200
  }
91
201
 
92
- // src/external.ts
93
- import { spawn } from "child_process";
94
- import { readFile, writeFile } from "fs/promises";
95
- function runCommand(command, args = [], opts = {}) {
96
- const timeoutMs = opts.timeoutMs ?? 12e4;
97
- const maxBuffer = opts.maxBuffer ?? 32 * 1024 * 1024;
98
- return new Promise((resolve, reject) => {
99
- let child;
100
- try {
101
- child = spawn(command, [...args], { signal: opts.signal });
102
- } catch (err2) {
103
- reject(err2 instanceof Error ? err2 : new Error(String(err2)));
104
- return;
105
- }
106
- let out = "";
107
- let err = "";
108
- let killed = false;
109
- const timer = setTimeout(() => {
110
- killed = true;
111
- child.kill("SIGKILL");
112
- reject(new Error(`${command}: timed out after ${timeoutMs}ms`));
113
- }, timeoutMs);
114
- const cap = (add, buf) => {
115
- const next = buf + add;
116
- if (next.length > maxBuffer) {
117
- killed = true;
118
- child.kill("SIGKILL");
119
- clearTimeout(timer);
120
- reject(new Error(`${command}: output exceeded ${maxBuffer} bytes`));
121
- }
122
- return next;
123
- };
124
- child.stdout?.on("data", (d) => {
125
- out = cap(String(d), out);
126
- });
127
- child.stderr?.on("data", (d) => {
128
- err = cap(String(d), err);
129
- });
130
- child.on("error", (e) => {
131
- clearTimeout(timer);
132
- if (!killed) reject(e);
133
- });
134
- child.on("close", (code) => {
135
- clearTimeout(timer);
136
- if (!killed) resolve({ stdout: out, stderr: err, code });
137
- });
138
- if (opts.input !== void 0) {
139
- child.stdin?.on("error", () => {
140
- });
141
- child.stdin?.end(opts.input);
142
- }
143
- });
144
- }
145
- function commandAnalyzer(spec) {
146
- return {
147
- name: spec.name,
148
- async analyze(otlpPath, opts = {}) {
149
- try {
150
- const res = await runCommand(spec.command, spec.args(otlpPath, opts.prompt), {
151
- signal: opts.signal,
152
- timeoutMs: spec.timeoutMs
153
- });
154
- if (res.code !== 0) {
155
- return { analyzer: spec.name, ok: false, output: res.stdout, error: res.stderr.trim() || `exit ${res.code}` };
156
- }
157
- return { analyzer: spec.name, ok: true, output: res.stdout.trim(), findings: spec.parse?.(res.stdout) };
158
- } catch (e) {
159
- return { analyzer: spec.name, ok: false, output: "", error: e instanceof Error ? e.message : String(e) };
160
- }
161
- }
162
- };
163
- }
164
- function toCanonicalOpenInference(otlpJsonl, opts = {}) {
165
- return otlpJsonl.split("\n").filter((l) => l.trim()).map((line) => {
166
- const s = JSON.parse(line);
167
- const a = s.attributes ?? {};
168
- const resourceAttrs = { "service.name": a["service.name"] ?? opts.service ?? "traces" };
169
- if (a["agent.name"]) resourceAttrs["agent.name"] = a["agent.name"];
170
- return JSON.stringify({
171
- trace_id: s.trace_id,
172
- span_id: s.span_id,
173
- parent_span_id: s.parent_span_id ?? "",
174
- trace_state: "",
175
- name: s.name,
176
- kind: a["openinference.span.kind"] ?? "CHAIN",
177
- start_time: s.start_time,
178
- end_time: s.end_time,
179
- status: { code: s.status?.code ?? "OK", message: s.status?.message ?? "" },
180
- resource: { attributes: resourceAttrs },
181
- scope: { name: opts.scopeName ?? "tangle-traces", version: "" },
182
- attributes: a
183
- });
184
- }).join("\n");
185
- }
186
- function haloAnalyzer(opts = {}) {
187
- const command = opts.command ?? "halo";
188
- const defaultPrompt = opts.defaultPrompt ?? "diagnose";
189
- return {
190
- name: "halo",
191
- async analyze(otlpPath, o = {}) {
192
- try {
193
- const canonical = toCanonicalOpenInference(await readFile(otlpPath, "utf8"));
194
- const canonPath = `${otlpPath}.halo.jsonl`;
195
- await writeFile(canonPath, canonical);
196
- const args = [canonPath, "-p", o.prompt ?? defaultPrompt];
197
- if (opts.model) args.push("-m", opts.model);
198
- const res = await runCommand(command, args, { signal: o.signal, timeoutMs: opts.timeoutMs });
199
- if (res.code !== 0) {
200
- return { analyzer: "halo", ok: false, output: res.stdout, error: res.stderr.trim() || `exit ${res.code}` };
201
- }
202
- return { analyzer: "halo", ok: true, output: res.stdout.trim() };
203
- } catch (e) {
204
- return { analyzer: "halo", ok: false, output: "", error: e instanceof Error ? e.message : String(e) };
205
- }
206
- }
207
- };
208
- }
209
- function runExternalAnalyzers(otlpPath, analyzers, opts = {}) {
210
- return Promise.all(analyzers.map((a) => a.analyze(otlpPath, opts)));
211
- }
212
- function commandRedactor(spec) {
213
- const name = spec.name ?? spec.command;
214
- return {
215
- name,
216
- async redactText(texts) {
217
- if (texts.length === 0) return [];
218
- const res = await runCommand(spec.command, spec.args ?? [], {
219
- input: JSON.stringify(texts),
220
- timeoutMs: spec.timeoutMs
221
- });
222
- if (res.code !== 0) throw new Error(`redactor ${name}: exit ${res.code}: ${res.stderr.trim()}`);
223
- let parsed;
224
- try {
225
- parsed = JSON.parse(res.stdout);
226
- } catch {
227
- throw new Error(`redactor ${name}: stdout was not JSON`);
228
- }
229
- if (!Array.isArray(parsed) || parsed.length !== texts.length) {
230
- throw new Error(`redactor ${name}: expected a JSON array of ${texts.length} strings`);
231
- }
232
- return parsed.map(String);
233
- }
234
- };
235
- }
202
+ // src/attributes.ts
203
+ var ATTR = {
204
+ /** = the trace/session id. Wire contract (dedup linkage). */
205
+ SESSION_ID: "tangle.sessionId",
206
+ /** "cli" marks a CLI upload so the platform can drop a re-upload of a session
207
+ * that also streamed live. Wire contract. */
208
+ INGEST_SOURCE: "tangle.ingest_source",
209
+ HARNESS: "tangle.harness",
210
+ CWD: "tangle.cwd",
211
+ /** THE per-session grouping key the spine derives subjects from
212
+ * (`deriveSubjectKey` reads `tangle.subject.key` first). Resolves to the git
213
+ * remote (host/owner/repo) when readable, else the cwd path basename. */
214
+ SUBJECT_KEY: "tangle.subject.key",
215
+ /** Normalized git remote (e.g. `github.com/tangle-network/agent-dev-container`). */
216
+ GIT_REPOSITORY: "git.repository",
217
+ /** Current branch at conversion time. */
218
+ GIT_BRANCH_NAME: "git.branch",
219
+ /** HEAD short sha at conversion time. */
220
+ GIT_COMMIT: "git.commit",
221
+ GIT_BRANCH: "tangle.git_branch",
222
+ HOST: "tangle.host",
223
+ /** Basename of the session file. (Renamed from the ambiguous `tangle.source`,
224
+ * which collided with `tangle.ingest_source`.) */
225
+ SESSION_FILE: "tangle.session_file",
226
+ UPLOADED_AT: "tangle.uploaded_at",
227
+ UPLOADER: "tangle.uploader",
228
+ REDACTION_VERSION: "redaction.version",
229
+ REDACTION_COUNT: "redaction.count"
230
+ };
231
+ var INGEST_SOURCE_CLI = "cli";
232
+ var DEFAULT_HARNESS = "claude-code";
236
233
 
237
234
  // src/time.ts
238
235
  function parseIsoToEpochMs(ts) {
@@ -310,6 +307,7 @@ function capText(raw) {
310
307
  if (t.length <= CONTENT_CAP) return t;
311
308
  return `${t.slice(0, CONTENT_CAP)}\u2026 [+${t.length - CONTENT_CAP} chars]`;
312
309
  }
310
+ var ACTOR_ATTR = "tangle.actor";
313
311
  function userPromptSpan(o) {
314
312
  return span({
315
313
  traceId: o.traceId,
@@ -321,14 +319,15 @@ function userPromptSpan(o) {
321
319
  service: o.service ?? null,
322
320
  agent: o.agent ?? null,
323
321
  step: o.step,
324
- content: o.content
322
+ content: o.content,
323
+ extra: { [ACTOR_ATTR]: o.actor ?? "human" }
325
324
  });
326
325
  }
327
326
 
328
327
  // src/adapters/amp.ts
329
328
  import { readdir, readFile as readFile2, stat } from "fs/promises";
330
329
  import { homedir } from "os";
331
- import { basename, join } from "path";
330
+ import { basename, join as join2 } from "path";
332
331
  var SERVICE = "amp";
333
332
  function textOf(content) {
334
333
  if (typeof content === "string") return content.slice(0, CONTENT_CAP);
@@ -339,8 +338,8 @@ function textOf(content) {
339
338
  var AmpAdapter = class {
340
339
  harness = "amp";
341
340
  threadsRoot() {
342
- const base = process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share");
343
- return join(base, "amp", "threads");
341
+ const base = process.env.XDG_DATA_HOME ?? join2(homedir(), ".local", "share");
342
+ return join2(base, "amp", "threads");
344
343
  }
345
344
  async locate(opts = {}) {
346
345
  let files;
@@ -352,7 +351,7 @@ var AmpAdapter = class {
352
351
  const refs = [];
353
352
  for (const f of files) {
354
353
  if (!f.startsWith("T-") || !f.endsWith(".json")) continue;
355
- const path = join(this.threadsRoot(), f);
354
+ const path = join2(this.threadsRoot(), f);
356
355
  let st;
357
356
  try {
358
357
  st = await stat(path);
@@ -451,10 +450,98 @@ var AmpAdapter = class {
451
450
  }
452
451
  };
453
452
 
453
+ // src/adapters/actor.ts
454
+ var CMD_MARKERS = [
455
+ "<command-name>",
456
+ "<local-command-caveat>",
457
+ "<command-message>",
458
+ "<command-args>",
459
+ "<command-stdout>"
460
+ ];
461
+ var INJECT_MARKERS = [
462
+ "<system-reminder>",
463
+ "Caveat: The messages below",
464
+ "<user-memory-input>",
465
+ // Harness-injected wrappers that arrive as `userType: external` user turns —
466
+ // they pass the structural human test but are the harness feeding the model,
467
+ // not a person. (Observed in Claude Code task/loop sessions.)
468
+ "<task-notification>",
469
+ "<task-prompt>",
470
+ "<local-command-stdout>",
471
+ "This session is being continued from a previous conversation",
472
+ // Slash-command skill bodies are expanded into a user turn (no <command-name>
473
+ // wrapper) — the skill prompt, not the human.
474
+ "Base directory for this skill:"
475
+ ];
476
+ var SYNTHETIC_MARKERS = [
477
+ "You are in a development environment with full file system access",
478
+ "## What the user asked for",
479
+ "Build the project described below",
480
+ "Filesystem sandboxing defines",
481
+ "<permissions instructions>",
482
+ "You are an autonomous",
483
+ "You are a coding agent",
484
+ "Your task is to build",
485
+ "## Deliverable",
486
+ "## Acceptance criteria",
487
+ "Return ONLY",
488
+ "respond with JSON",
489
+ "<environment_details>",
490
+ "<task>",
491
+ "Review this change before it is pushed",
492
+ "Complete a security review",
493
+ "Focus on correctness, regressions, security issues",
494
+ "Read-only critical audit",
495
+ "Staff-engineer code review"
496
+ ];
497
+ var AGENT_PROMPT = /^(you are\b|you're\b|read[- ]only\b|stop now\b|your task\b|your job\b|review the\b|audit\b|analyze the\b|return (only|a |the )|here is the\b|context:|task:|do not (revert|edit|touch)|work only in\b|you have been\b|act as\b|operate the\b|goal:|objective:)/i;
498
+ function textIsCmdOrInject(s) {
499
+ return CMD_MARKERS.some((m) => s.includes(m)) || INJECT_MARKERS.some((m) => s.includes(m));
500
+ }
501
+ function textIsSynthetic(s) {
502
+ const head = s.slice(0, 600);
503
+ return SYNTHETIC_MARKERS.some((m) => head.includes(m));
504
+ }
505
+ function looksLikeAgentPrompt(s) {
506
+ const t = s.trim();
507
+ if (t.length > 1500) return true;
508
+ return AGENT_PROMPT.test(t);
509
+ }
510
+ function claudeActor(args) {
511
+ if (args.isSidechain === true) return "subagent-spawn";
512
+ if (args.userType != null && args.userType !== "external") return "injected";
513
+ if (textIsCmdOrInject(args.text)) return "injected";
514
+ if (textIsSynthetic(args.text)) return "injected";
515
+ if (args.isFirstUserTurn && looksLikeAgentPrompt(args.text)) return "subagent-spawn";
516
+ return "human";
517
+ }
518
+ function codexActor(args) {
519
+ if (textIsCmdOrInject(args.text)) return "injected";
520
+ if (textIsSynthetic(args.text)) return "injected";
521
+ if (args.isFirstUserTurn && looksLikeAgentPrompt(args.text)) return "injected";
522
+ return "human";
523
+ }
524
+ var JARGON_COMPLAINT = /\b(jargon|plain english|plain language|eli5|explain like|in plain|what does that mean|what does .* mean|i don'?t (understand|get)|gibberish|english please|too technical|speak (plainly|english)|dumb(ed)? it down|simpler|simplify your|no buzzwords?)\b/;
525
+ var STRUCTURE_COMPLAINT = /\b(too long|tl;?dr|get to the point|bluf|lead with|bullet|bulletpoint|wall of text|too verbose|shorter|be concise|stop (yapping|rambling)|less words?|fewer words?|just (the|tell me)|cut the)\b/;
526
+ var CORRECTION = /(\bno[,. ]|^no\b|\bstop\b|\bdon'?t\b|you keep|i (said|told you|asked)|that'?s not (what|right|it)|not what i|didn'?t ask|incorrect|that'?s wrong|you'?re wrong|why are you|why did you|that'?s not the|missed the|not the point|ur wrong|thats not)/;
527
+ var FRUSTRATION = /(ugh|wtf|wth|come on|seriously|again\?|for the (last|nth) time|are you kidding|jesus|christ|frustrat|annoying|wasting|waste of)/;
528
+ var PRAISE = /\b(perfect|exactly|nailed it|love (it|this)|beautiful|great work|ship it|clean|nice work|excellent|that'?s it|yes!+|amazing|chef'?s kiss|brilliant)\b/;
529
+ function classifyReaction(text) {
530
+ const t = text.toLowerCase();
531
+ const hits = [];
532
+ if (JARGON_COMPLAINT.test(t)) hits.push("jargon");
533
+ if (STRUCTURE_COMPLAINT.test(t)) hits.push("structure");
534
+ if (CORRECTION.test(t)) hits.push("correction");
535
+ if (FRUSTRATION.test(t)) hits.push("frustration");
536
+ if (PRAISE.test(t)) hits.push("praise");
537
+ return hits;
538
+ }
539
+ var CORRECTIVE_REACTIONS = ["correction", "frustration", "jargon", "structure"];
540
+
454
541
  // src/adapters/claude.ts
455
542
  import { readdir as readdir2, readFile as readFile3, stat as stat2 } from "fs/promises";
456
543
  import { homedir as homedir2 } from "os";
457
- import { basename as basename2, join as join2 } from "path";
544
+ import { basename as basename2, join as join3 } from "path";
458
545
  var SERVICE2 = "claude-code";
459
546
  function parseLines(raw) {
460
547
  const out = [];
@@ -492,6 +579,7 @@ function parseClaudeStream(events, ctx) {
492
579
  const spans = [];
493
580
  const toolSpanByUseId = /* @__PURE__ */ new Map();
494
581
  let step = ctx.startStep;
582
+ let sawUserTurn = false;
495
583
  for (const ev of events) {
496
584
  const ts = ev.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
497
585
  const uid = ev.uuid ?? `${ctx.idPrefix}step${step}`;
@@ -539,6 +627,13 @@ function parseClaudeStream(events, ctx) {
539
627
  } else if (ev.type === "user" && ev.message) {
540
628
  const prompt = textOf2(ev.message.content);
541
629
  if (prompt) {
630
+ const actor = claudeActor({
631
+ text: prompt,
632
+ isSidechain: ev.isSidechain,
633
+ userType: ev.userType ?? null,
634
+ isFirstUserTurn: !sawUserTurn
635
+ });
636
+ sawUserTurn = true;
542
637
  spans.push(
543
638
  userPromptSpan({
544
639
  traceId: ctx.traceId,
@@ -548,7 +643,8 @@ function parseClaudeStream(events, ctx) {
548
643
  service: SERVICE2,
549
644
  agent: ctx.agent,
550
645
  step,
551
- content: prompt
646
+ content: prompt,
647
+ actor
552
648
  })
553
649
  );
554
650
  step += 1;
@@ -575,7 +671,7 @@ var ClaudeAdapter = class {
575
671
  harness = "claude-code";
576
672
  aliases = ["claude", "claudish", "openclaw", "nanoclaw"];
577
673
  root() {
578
- return join2(homedir2(), ".claude", "projects");
674
+ return join3(homedir2(), ".claude", "projects");
579
675
  }
580
676
  async locate(opts = {}) {
581
677
  const root = this.root();
@@ -587,7 +683,7 @@ var ClaudeAdapter = class {
587
683
  }
588
684
  const refs = [];
589
685
  for (const dir of projectDirs) {
590
- const dirPath = join2(root, dir);
686
+ const dirPath = join3(root, dir);
591
687
  let files;
592
688
  try {
593
689
  files = await readdir2(dirPath);
@@ -596,7 +692,7 @@ var ClaudeAdapter = class {
596
692
  }
597
693
  for (const f of files) {
598
694
  if (!f.endsWith(".jsonl")) continue;
599
- const path = join2(dirPath, f);
695
+ const path = join3(dirPath, f);
600
696
  let st;
601
697
  try {
602
698
  st = await stat2(path);
@@ -643,7 +739,7 @@ var ClaudeAdapter = class {
643
739
  }
644
740
  /** Parse `<session>/subagents/agent-*.jsonl`, parenting each under its Agent call. */
645
741
  async foldSubagents(ref, traceId, main, out) {
646
- const subDir = join2(ref.path.replace(/\.jsonl$/, ""), "subagents");
742
+ const subDir = join3(ref.path.replace(/\.jsonl$/, ""), "subagents");
647
743
  let files;
648
744
  try {
649
745
  files = await readdir2(subDir);
@@ -656,13 +752,13 @@ var ClaudeAdapter = class {
656
752
  const hash = basename2(f, ".jsonl");
657
753
  let meta = {};
658
754
  try {
659
- meta = JSON.parse(await readFile3(join2(subDir, `${hash}.meta.json`), "utf8"));
755
+ meta = JSON.parse(await readFile3(join3(subDir, `${hash}.meta.json`), "utf8"));
660
756
  } catch {
661
757
  }
662
758
  const parent = meta.toolUseId && main.toolSpanByUseId.get(meta.toolUseId)?.span_id || `root:${traceId}`;
663
759
  let raw;
664
760
  try {
665
- raw = await readFile3(join2(subDir, f), "utf8");
761
+ raw = await readFile3(join3(subDir, f), "utf8");
666
762
  } catch {
667
763
  continue;
668
764
  }
@@ -682,7 +778,7 @@ var ClaudeAdapter = class {
682
778
  // src/adapters/codex.ts
683
779
  import { readdir as readdir3, readFile as readFile4, stat as stat3 } from "fs/promises";
684
780
  import { homedir as homedir3 } from "os";
685
- import { basename as basename3, join as join3 } from "path";
781
+ import { basename as basename3, join as join4 } from "path";
686
782
  var SERVICE3 = "codex";
687
783
  function parseLines2(raw) {
688
784
  const out = [];
@@ -718,7 +814,7 @@ async function* walkRollouts(root) {
718
814
  return;
719
815
  }
720
816
  for (const y of years) {
721
- const yp = join3(root, y);
817
+ const yp = join4(root, y);
722
818
  let months;
723
819
  try {
724
820
  months = await readdir3(yp);
@@ -726,7 +822,7 @@ async function* walkRollouts(root) {
726
822
  continue;
727
823
  }
728
824
  for (const m of months) {
729
- const mp = join3(yp, m);
825
+ const mp = join4(yp, m);
730
826
  let days;
731
827
  try {
732
828
  days = await readdir3(mp);
@@ -734,7 +830,7 @@ async function* walkRollouts(root) {
734
830
  continue;
735
831
  }
736
832
  for (const d of days) {
737
- const dp = join3(mp, d);
833
+ const dp = join4(mp, d);
738
834
  let files;
739
835
  try {
740
836
  files = await readdir3(dp);
@@ -742,7 +838,7 @@ async function* walkRollouts(root) {
742
838
  continue;
743
839
  }
744
840
  for (const f of files) {
745
- if (f.startsWith("rollout-") && f.endsWith(".jsonl")) yield join3(dp, f);
841
+ if (f.startsWith("rollout-") && f.endsWith(".jsonl")) yield join4(dp, f);
746
842
  }
747
843
  }
748
844
  }
@@ -752,7 +848,7 @@ var CodexAdapter = class {
752
848
  harness = "codex";
753
849
  aliases = ["codex-acp"];
754
850
  root() {
755
- return process.env.CODEX_HOME ? join3(process.env.CODEX_HOME, "sessions") : join3(homedir3(), ".codex", "sessions");
851
+ return process.env.CODEX_HOME ? join4(process.env.CODEX_HOME, "sessions") : join4(homedir3(), ".codex", "sessions");
756
852
  }
757
853
  async locate(opts = {}) {
758
854
  const refs = [];
@@ -801,6 +897,7 @@ var CodexAdapter = class {
801
897
  const toolByCallId = /* @__PURE__ */ new Map();
802
898
  let step = 0;
803
899
  let lastLlm = rootId;
900
+ let sawUserTurn = false;
804
901
  for (const l of lines) {
805
902
  const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
806
903
  if (l.type === "event_msg" && l.payload?.type === "token_count") {
@@ -855,6 +952,8 @@ var CodexAdapter = class {
855
952
  } else if (l.type === "response_item" && l.payload?.type === "message" && l.payload.role === "user") {
856
953
  const prompt = textOf3(l.payload.content);
857
954
  if (prompt) {
955
+ const actor = codexActor({ text: prompt, isFirstUserTurn: !sawUserTurn });
956
+ sawUserTurn = true;
858
957
  spans.push(
859
958
  userPromptSpan({
860
959
  traceId,
@@ -864,7 +963,8 @@ var CodexAdapter = class {
864
963
  content: prompt,
865
964
  service: SERVICE3,
866
965
  agent: SERVICE3,
867
- step
966
+ step,
967
+ actor
868
968
  })
869
969
  );
870
970
  step += 1;
@@ -897,7 +997,7 @@ var CodexAdapter = class {
897
997
  // src/adapters/copilot.ts
898
998
  import { readdir as readdir4, readFile as readFile5, stat as stat4 } from "fs/promises";
899
999
  import { homedir as homedir4 } from "os";
900
- import { join as join4 } from "path";
1000
+ import { join as join5 } from "path";
901
1001
  var SERVICE4 = "github-copilot";
902
1002
  function parseLines3(raw) {
903
1003
  const out = [];
@@ -914,7 +1014,7 @@ var CopilotAdapter = class {
914
1014
  harness = "github-copilot";
915
1015
  aliases = ["copilot"];
916
1016
  root() {
917
- return join4(process.env.COPILOT_HOME ?? join4(homedir4(), ".copilot"), "session-state");
1017
+ return join5(process.env.COPILOT_HOME ?? join5(homedir4(), ".copilot"), "session-state");
918
1018
  }
919
1019
  async locate(opts = {}) {
920
1020
  let dirs;
@@ -925,7 +1025,7 @@ var CopilotAdapter = class {
925
1025
  }
926
1026
  const refs = [];
927
1027
  for (const id of dirs) {
928
- const path = join4(this.root(), id, "events.jsonl");
1028
+ const path = join5(this.root(), id, "events.jsonl");
929
1029
  let st;
930
1030
  try {
931
1031
  st = await stat4(path);
@@ -1023,7 +1123,7 @@ var CopilotAdapter = class {
1023
1123
  // src/adapters/factory.ts
1024
1124
  import { readdir as readdir5, readFile as readFile6, stat as stat5 } from "fs/promises";
1025
1125
  import { homedir as homedir5 } from "os";
1026
- import { basename as basename4, join as join5 } from "path";
1126
+ import { basename as basename4, join as join6 } from "path";
1027
1127
  var SERVICE5 = "factory";
1028
1128
  function textOf4(content) {
1029
1129
  return capText(
@@ -1045,7 +1145,7 @@ var FactoryAdapter = class {
1045
1145
  harness = "factory";
1046
1146
  aliases = ["factory-droids", "droid"];
1047
1147
  root() {
1048
- return join5(homedir5(), ".factory", "sessions");
1148
+ return join6(homedir5(), ".factory", "sessions");
1049
1149
  }
1050
1150
  async locate(opts = {}) {
1051
1151
  const root = this.root();
@@ -1057,7 +1157,7 @@ var FactoryAdapter = class {
1057
1157
  }
1058
1158
  const refs = [];
1059
1159
  for (const dir of dirs) {
1060
- const dp = join5(root, dir);
1160
+ const dp = join6(root, dir);
1061
1161
  let files;
1062
1162
  try {
1063
1163
  files = await readdir5(dp);
@@ -1068,7 +1168,7 @@ var FactoryAdapter = class {
1068
1168
  if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
1069
1169
  for (const f of files) {
1070
1170
  if (!f.endsWith(".jsonl")) continue;
1071
- const path = join5(dp, f);
1171
+ const path = join6(dp, f);
1072
1172
  let st;
1073
1173
  try {
1074
1174
  st = await stat5(path);
@@ -1184,7 +1284,7 @@ var FactoryAdapter = class {
1184
1284
  // src/adapters/forge.ts
1185
1285
  import { readdir as readdir6, readFile as readFile7, stat as stat6 } from "fs/promises";
1186
1286
  import { homedir as homedir6 } from "os";
1187
- import { basename as basename5, join as join6 } from "path";
1287
+ import { basename as basename5, join as join7 } from "path";
1188
1288
  var SERVICE6 = "forge";
1189
1289
  function tokens(t) {
1190
1290
  if (!t) return null;
@@ -1198,7 +1298,7 @@ var ForgeAdapter = class {
1198
1298
  aliases = ["forgecode"];
1199
1299
  /** Directories scanned for `*-dump.json` exports. */
1200
1300
  dumpDirs() {
1201
- return [process.cwd(), join6(homedir6(), ".forge")];
1301
+ return [process.cwd(), join7(homedir6(), ".forge")];
1202
1302
  }
1203
1303
  async locate(opts = {}) {
1204
1304
  const refs = [];
@@ -1212,7 +1312,7 @@ var ForgeAdapter = class {
1212
1312
  }
1213
1313
  for (const f of files) {
1214
1314
  if (!f.endsWith("-dump.json")) continue;
1215
- const path = join6(dir, f);
1315
+ const path = join7(dir, f);
1216
1316
  let st;
1217
1317
  try {
1218
1318
  st = await stat6(path);
@@ -1314,7 +1414,7 @@ var ForgeAdapter = class {
1314
1414
  // src/adapters/gemini.ts
1315
1415
  import { readdir as readdir7, readFile as readFile8, stat as stat7 } from "fs/promises";
1316
1416
  import { homedir as homedir7 } from "os";
1317
- import { join as join7 } from "path";
1417
+ import { join as join8 } from "path";
1318
1418
  function iso(ts, fallback) {
1319
1419
  if (typeof ts === "number") return new Date(ts).toISOString();
1320
1420
  if (typeof ts === "string" && ts.length > 0) return ts;
@@ -1339,7 +1439,7 @@ var GeminiFamilyAdapter = class {
1339
1439
  this.homeDirName = cfg.homeDirName;
1340
1440
  }
1341
1441
  chatsRoot() {
1342
- return join7(homedir7(), this.homeDirName, "tmp");
1442
+ return join8(homedir7(), this.homeDirName, "tmp");
1343
1443
  }
1344
1444
  async locate(opts = {}) {
1345
1445
  const root = this.chatsRoot();
@@ -1351,7 +1451,7 @@ var GeminiFamilyAdapter = class {
1351
1451
  }
1352
1452
  const refs = [];
1353
1453
  for (const pd of projectDirs) {
1354
- const chatsDir = join7(root, pd, "chats");
1454
+ const chatsDir = join8(root, pd, "chats");
1355
1455
  let files;
1356
1456
  try {
1357
1457
  files = await readdir7(chatsDir);
@@ -1360,7 +1460,7 @@ var GeminiFamilyAdapter = class {
1360
1460
  }
1361
1461
  for (const f of files) {
1362
1462
  if (!f.startsWith("session-") || !f.endsWith(".json")) continue;
1363
- const path = join7(chatsDir, f);
1463
+ const path = join8(chatsDir, f);
1364
1464
  let st;
1365
1465
  try {
1366
1466
  st = await stat7(path);
@@ -1473,7 +1573,7 @@ var GeminiAdapter = class extends GeminiFamilyAdapter {
1473
1573
  // src/adapters/opencode.ts
1474
1574
  import { readdir as readdir8, readFile as readFile9, stat as stat8 } from "fs/promises";
1475
1575
  import { homedir as homedir8 } from "os";
1476
- import { join as join8 } from "path";
1576
+ import { join as join9 } from "path";
1477
1577
  var SERVICE7 = "opencode";
1478
1578
  function isoOf(ms) {
1479
1579
  return new Date(ms ?? 0).toISOString();
@@ -1486,11 +1586,11 @@ function textOf7(parts) {
1486
1586
  var OpencodeAdapter = class {
1487
1587
  harness = "opencode";
1488
1588
  storage() {
1489
- const base = process.env.XDG_DATA_HOME ?? join8(homedir8(), ".local", "share");
1490
- return join8(base, "opencode", "storage");
1589
+ const base = process.env.XDG_DATA_HOME ?? join9(homedir8(), ".local", "share");
1590
+ return join9(base, "opencode", "storage");
1491
1591
  }
1492
1592
  async locate(opts = {}) {
1493
- const msgRoot = join8(this.storage(), "message");
1593
+ const msgRoot = join9(this.storage(), "message");
1494
1594
  let sessionDirs;
1495
1595
  try {
1496
1596
  sessionDirs = await readdir8(msgRoot);
@@ -1499,7 +1599,7 @@ var OpencodeAdapter = class {
1499
1599
  }
1500
1600
  const refs = [];
1501
1601
  for (const sid of sessionDirs) {
1502
- const dir = join8(msgRoot, sid);
1602
+ const dir = join9(msgRoot, sid);
1503
1603
  let st;
1504
1604
  try {
1505
1605
  st = await stat8(dir);
@@ -1516,7 +1616,7 @@ var OpencodeAdapter = class {
1516
1616
  async parse(ref) {
1517
1617
  const traceId = ref.sessionId;
1518
1618
  const rootId = `root:${traceId}`;
1519
- const partRoot = join8(this.storage(), "part");
1619
+ const partRoot = join9(this.storage(), "part");
1520
1620
  let msgFiles;
1521
1621
  try {
1522
1622
  msgFiles = (await readdir8(ref.path)).filter((f) => f.endsWith(".json"));
@@ -1526,7 +1626,7 @@ var OpencodeAdapter = class {
1526
1626
  const messages = [];
1527
1627
  for (const f of msgFiles) {
1528
1628
  try {
1529
- messages.push({ msg: JSON.parse(await readFile9(join8(ref.path, f), "utf8")), file: f });
1629
+ messages.push({ msg: JSON.parse(await readFile9(join9(ref.path, f), "utf8")), file: f });
1530
1630
  } catch {
1531
1631
  }
1532
1632
  }
@@ -1548,13 +1648,13 @@ var OpencodeAdapter = class {
1548
1648
  for (const { msg } of messages) {
1549
1649
  const mid = msg.id ?? `m${step}`;
1550
1650
  const llmId = `llm:${mid}`;
1551
- const pdir = join8(partRoot, mid);
1651
+ const pdir = join9(partRoot, mid);
1552
1652
  const parts = [];
1553
1653
  try {
1554
1654
  const partFiles = (await readdir8(pdir)).filter((f) => f.endsWith(".json"));
1555
1655
  for (const pf of partFiles) {
1556
1656
  try {
1557
- parts.push(JSON.parse(await readFile9(join8(pdir, pf), "utf8")));
1657
+ parts.push(JSON.parse(await readFile9(join9(pdir, pf), "utf8")));
1558
1658
  } catch {
1559
1659
  }
1560
1660
  }
@@ -1626,7 +1726,7 @@ var OpencodeAdapter = class {
1626
1726
  // src/adapters/pi.ts
1627
1727
  import { readdir as readdir9, readFile as readFile10, stat as stat9 } from "fs/promises";
1628
1728
  import { homedir as homedir9 } from "os";
1629
- import { basename as basename6, join as join9 } from "path";
1729
+ import { basename as basename6, join as join10 } from "path";
1630
1730
  var SERVICE8 = "pi";
1631
1731
  function parseLines5(raw) {
1632
1732
  const out = [];
@@ -1654,7 +1754,7 @@ function textOf8(content) {
1654
1754
  var PiAdapter = class {
1655
1755
  harness = "pi";
1656
1756
  root() {
1657
- return join9(homedir9(), ".pi", "agent", "sessions");
1757
+ return join10(homedir9(), ".pi", "agent", "sessions");
1658
1758
  }
1659
1759
  async locate(opts = {}) {
1660
1760
  const root = this.root();
@@ -1666,7 +1766,7 @@ var PiAdapter = class {
1666
1766
  }
1667
1767
  const refs = [];
1668
1768
  for (const dir of dirs) {
1669
- const dp = join9(root, dir);
1769
+ const dp = join10(root, dir);
1670
1770
  let files;
1671
1771
  try {
1672
1772
  files = await readdir9(dp);
@@ -1677,7 +1777,7 @@ var PiAdapter = class {
1677
1777
  if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
1678
1778
  for (const f of files) {
1679
1779
  if (!f.endsWith(".jsonl")) continue;
1680
- const path = join9(dp, f);
1780
+ const path = join10(dp, f);
1681
1781
  let st;
1682
1782
  try {
1683
1783
  st = await stat9(path);
@@ -1795,7 +1895,7 @@ var PiAdapter = class {
1795
1895
  // src/adapters/qwen.ts
1796
1896
  import { readdir as readdir10, readFile as readFile11, stat as stat10 } from "fs/promises";
1797
1897
  import { homedir as homedir10 } from "os";
1798
- import { basename as basename7, join as join10 } from "path";
1898
+ import { basename as basename7, join as join11 } from "path";
1799
1899
  var SERVICE9 = "qwen";
1800
1900
  function parseLines6(raw) {
1801
1901
  const out = [];
@@ -1816,7 +1916,7 @@ var QwenAdapter = class {
1816
1916
  harness = "qwen";
1817
1917
  aliases = ["qwen-code"];
1818
1918
  root() {
1819
- return join10(homedir10(), ".qwen", "projects");
1919
+ return join11(homedir10(), ".qwen", "projects");
1820
1920
  }
1821
1921
  async locate(opts = {}) {
1822
1922
  const root = this.root();
@@ -1828,7 +1928,7 @@ var QwenAdapter = class {
1828
1928
  }
1829
1929
  const refs = [];
1830
1930
  for (const pd of projectDirs) {
1831
- const chatsDir = join10(root, pd, "chats");
1931
+ const chatsDir = join11(root, pd, "chats");
1832
1932
  let files;
1833
1933
  try {
1834
1934
  files = await readdir10(chatsDir);
@@ -1838,7 +1938,7 @@ var QwenAdapter = class {
1838
1938
  if (opts.cwd) continue;
1839
1939
  for (const f of files) {
1840
1940
  if (!f.endsWith(".jsonl")) continue;
1841
- const path = join10(chatsDir, f);
1941
+ const path = join11(chatsDir, f);
1842
1942
  let st;
1843
1943
  try {
1844
1944
  st = await stat10(path);
@@ -1988,7 +2088,103 @@ function selectAdapters(opts) {
1988
2088
  });
1989
2089
  }
1990
2090
 
2091
+ // src/repo.ts
2092
+ function normalizeRemote(url) {
2093
+ const raw = url.trim();
2094
+ if (!raw) return null;
2095
+ const url2 = raw.match(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/(.+)$/);
2096
+ if (url2) {
2097
+ let rest = url2[1];
2098
+ const at = rest.lastIndexOf("@");
2099
+ if (at !== -1) rest = rest.slice(at + 1);
2100
+ rest = rest.replace(/\.git$/, "").replace(/\/+$/g, "");
2101
+ const slash = rest.indexOf("/");
2102
+ if (slash === -1) return null;
2103
+ const host = rest.slice(0, slash).replace(/:\d+$/, "");
2104
+ const path = rest.slice(slash + 1).replace(/^\/+/, "");
2105
+ return host && path ? `${host}/${path}` : null;
2106
+ }
2107
+ const scp = raw.match(/^[^@/]+@([^:/]+):(.+)$/);
2108
+ if (scp) {
2109
+ const path = scp[2].replace(/\.git$/, "").replace(/^\/+|\/+$/g, "");
2110
+ return path ? `${scp[1]}/${path}` : null;
2111
+ }
2112
+ return null;
2113
+ }
2114
+ async function readGit(cwd) {
2115
+ const { stat: stat11 } = await import("fs/promises");
2116
+ await stat11(`${cwd}/.git`);
2117
+ const { execFile } = await import("child_process");
2118
+ const { promisify } = await import("util");
2119
+ const run = promisify(execFile);
2120
+ const git = async (args) => {
2121
+ try {
2122
+ const { stdout } = await run("git", ["-C", cwd, ...args], { timeout: 5e3 });
2123
+ const v = stdout.trim();
2124
+ return v.length > 0 ? v : null;
2125
+ } catch {
2126
+ return null;
2127
+ }
2128
+ };
2129
+ let remote = await git(["remote", "get-url", "origin"]);
2130
+ if (!remote) {
2131
+ const first = await git(["remote"]);
2132
+ const name = first?.split("\n")[0]?.trim();
2133
+ if (name) remote = await git(["remote", "get-url", name]);
2134
+ }
2135
+ const branch = await git(["rev-parse", "--abbrev-ref", "HEAD"]);
2136
+ const commit = await git(["rev-parse", "--short", "HEAD"]);
2137
+ return {
2138
+ remote,
2139
+ branch: branch === "HEAD" ? null : branch,
2140
+ // detached HEAD → no branch name
2141
+ commit
2142
+ };
2143
+ }
2144
+ async function resolveRepoAttrs(cwd) {
2145
+ if (!cwd) return {};
2146
+ const attrs = { [ATTR.CWD]: cwd };
2147
+ const basename9 = (() => {
2148
+ const trimmed = cwd.replace(/\/+$/g, "");
2149
+ const idx = trimmed.lastIndexOf("/");
2150
+ const name = idx === -1 ? trimmed : trimmed.slice(idx + 1);
2151
+ return name || trimmed;
2152
+ })();
2153
+ try {
2154
+ const { remote, branch, commit } = await readGit(cwd);
2155
+ const normalized = remote ? normalizeRemote(remote) : null;
2156
+ if (normalized) {
2157
+ attrs[ATTR.SUBJECT_KEY] = normalized;
2158
+ attrs[ATTR.GIT_REPOSITORY] = normalized;
2159
+ if (branch) attrs[ATTR.GIT_BRANCH_NAME] = branch;
2160
+ if (commit) attrs[ATTR.GIT_COMMIT] = commit;
2161
+ return attrs;
2162
+ }
2163
+ if (branch) attrs[ATTR.GIT_BRANCH_NAME] = branch;
2164
+ if (commit) attrs[ATTR.GIT_COMMIT] = commit;
2165
+ } catch {
2166
+ }
2167
+ attrs[ATTR.SUBJECT_KEY] = basename9;
2168
+ return attrs;
2169
+ }
2170
+ function stampRepoAttrs(spans, attrs) {
2171
+ const keys = Object.keys(attrs);
2172
+ if (keys.length === 0) return spans;
2173
+ for (const s of spans) {
2174
+ for (const k of keys) {
2175
+ if (s.attributes[k] === void 0) s.attributes[k] = attrs[k];
2176
+ }
2177
+ }
2178
+ return spans;
2179
+ }
2180
+
1991
2181
  // src/session-source.ts
2182
+ async function parseSession(adapter, ref) {
2183
+ const spans = await adapter.parse(ref);
2184
+ const repoAttrs = await resolveRepoAttrs(ref.cwd);
2185
+ stampRepoAttrs(spans, repoAttrs);
2186
+ return spans;
2187
+ }
1992
2188
  async function* scanSessions(opts) {
1993
2189
  for (const adapter of selectAdapters(opts)) {
1994
2190
  if (opts.signal?.aborted) return;
@@ -2004,7 +2200,7 @@ async function* scanSessions(opts) {
2004
2200
  if (opts.signal?.aborted) return;
2005
2201
  let spans;
2006
2202
  try {
2007
- spans = await adapter.parse(ref);
2203
+ spans = await parseSession(adapter, ref);
2008
2204
  } catch (err) {
2009
2205
  opts.onError?.(err, ref);
2010
2206
  continue;
@@ -2015,106 +2211,233 @@ async function* scanSessions(opts) {
2015
2211
  }
2016
2212
  }
2017
2213
 
2018
- // src/observer.ts
2019
- var sleep = (ms, signal) => new Promise((resolve) => {
2020
- const onAbort = () => {
2021
- clearTimeout(t);
2022
- resolve();
2214
+ // src/evidence.ts
2215
+ import { mkdtemp, writeFile } from "fs/promises";
2216
+ import { tmpdir } from "os";
2217
+ import { join as join12 } from "path";
2218
+ function stringAttr(span2, key) {
2219
+ const value = span2.attributes[key];
2220
+ return typeof value === "string" && value.length > 0 ? value : void 0;
2221
+ }
2222
+ function numberAttr(span2, key) {
2223
+ const value = span2.attributes[key];
2224
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
2225
+ }
2226
+ function spanKind(span2) {
2227
+ return stringAttr(span2, "openinference.span.kind");
2228
+ }
2229
+ function repoFromSpans(spans) {
2230
+ const attrs = {};
2231
+ for (const span2 of spans) {
2232
+ attrs.subjectKey ??= stringAttr(span2, ATTR.SUBJECT_KEY);
2233
+ attrs.repository ??= stringAttr(span2, ATTR.GIT_REPOSITORY);
2234
+ attrs.branch ??= stringAttr(span2, ATTR.GIT_BRANCH_NAME);
2235
+ attrs.commit ??= stringAttr(span2, ATTR.GIT_COMMIT);
2236
+ attrs.cwd ??= stringAttr(span2, ATTR.CWD);
2237
+ if (attrs.subjectKey && attrs.repository && attrs.branch && attrs.commit && attrs.cwd) break;
2238
+ }
2239
+ return attrs;
2240
+ }
2241
+ function timeBounds(spans) {
2242
+ const times = spans.flatMap((span2) => [span2.start_time, span2.end_time]).filter((value) => value && value !== "now").sort();
2243
+ return {
2244
+ firstSpanAt: times[0] ?? null,
2245
+ lastSpanAt: times[times.length - 1] ?? null
2023
2246
  };
2024
- const t = setTimeout(() => {
2025
- signal?.removeEventListener("abort", onAbort);
2026
- resolve();
2027
- }, ms);
2028
- signal?.addEventListener("abort", onAbort, { once: true });
2029
- });
2030
- function loopKey(sessionId, toolName, argHash) {
2031
- return `${sessionId}\0${toolName}\0${argHash}`;
2032
2247
  }
2033
- async function watchSessions(opts) {
2034
- const windowMs = opts.windowMs ?? 30 * 6e4;
2035
- const intervalMs = Math.max(250, opts.intervalMs ?? 5e3);
2036
- const minLoop = opts.minLoopOccurrences ?? 3;
2037
- const seen = /* @__PURE__ */ new Map();
2038
- while (!opts.signal?.aborted) {
2039
- let sessionCount = 0;
2040
- for await (const { adapter, ref, spans } of scanSessions({
2041
- adapters: opts.adapters,
2042
- all: opts.all,
2043
- harnesses: opts.harnesses,
2044
- cwd: opts.cwd,
2045
- sinceMs: Date.now() - windowMs,
2046
- signal: opts.signal,
2047
- onError: opts.onError
2048
- })) {
2049
- let report;
2050
- try {
2051
- report = await runPipelines(spans, { minLoopOccurrences: minLoop });
2052
- } catch (err) {
2053
- opts.onError?.(err, ref);
2054
- continue;
2055
- }
2056
- sessionCount += 1;
2057
- if (opts.onReport) {
2058
- try {
2059
- await opts.onReport(ref, report);
2060
- } catch (err) {
2061
- opts.onError?.(err, ref);
2062
- }
2063
- }
2064
- for (const f of report.stuckLoops.findings) {
2065
- const key = loopKey(ref.sessionId, f.toolName, f.argHash);
2066
- if (f.occurrences > (seen.get(key) ?? 0)) {
2067
- seen.set(key, f.occurrences);
2068
- if (opts.onLoop) {
2069
- try {
2070
- await opts.onLoop({
2071
- harness: adapter.harness,
2072
- sessionId: ref.sessionId,
2073
- cwd: ref.cwd,
2074
- toolName: f.toolName,
2075
- argHash: f.argHash,
2076
- occurrences: f.occurrences,
2077
- windowMs: f.windowMs
2078
- });
2079
- } catch (err) {
2080
- opts.onError?.(err, ref);
2081
- }
2082
- }
2248
+ function summarizeTools(spans) {
2249
+ const byTool = /* @__PURE__ */ new Map();
2250
+ for (const span2 of spans) {
2251
+ if (spanKind(span2) !== "TOOL") continue;
2252
+ const name = stringAttr(span2, "tool.name") ?? span2.name.replace(/^tool\./, "");
2253
+ const current = byTool.get(name) ?? { calls: 0, errors: 0 };
2254
+ current.calls += 1;
2255
+ if (span2.status.code === "ERROR") current.errors += 1;
2256
+ byTool.set(name, current);
2257
+ }
2258
+ return [...byTool.entries()].map(([name, row]) => ({ name, calls: row.calls, errors: row.errors })).sort((a, b) => b.calls - a.calls || a.name.localeCompare(b.name));
2259
+ }
2260
+ async function buildPolicyEvidenceRecord(ref, spans, opts = {}) {
2261
+ const llmSpans = spans.filter((span2) => spanKind(span2) === "LLM");
2262
+ const toolSpans = spans.filter((span2) => spanKind(span2) === "TOOL");
2263
+ const erroredToolCallCount = toolSpans.filter((span2) => span2.status.code === "ERROR").length;
2264
+ const pipelines = await runPipelines(spans, { minLoopOccurrences: opts.minLoopOccurrences });
2265
+ const loopLimit = opts.maxLoopExamples ?? 25;
2266
+ const loopFindings = pipelines.stuckLoops.findings;
2267
+ const { firstSpanAt, lastSpanAt } = timeBounds(spans);
2268
+ return {
2269
+ schemaVersion: 1,
2270
+ kind: "traces.policy_evidence.session",
2271
+ generatedAt: opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
2272
+ session: {
2273
+ harness: ref.harness,
2274
+ sessionId: ref.sessionId,
2275
+ path: ref.path,
2276
+ cwd: ref.cwd,
2277
+ mtimeMs: ref.mtimeMs
2278
+ },
2279
+ repo: repoFromSpans(spans),
2280
+ metrics: {
2281
+ spanCount: spans.length,
2282
+ llmTurnCount: llmSpans.length,
2283
+ toolCallCount: toolSpans.length,
2284
+ erroredToolCallCount,
2285
+ inputTokens: llmSpans.reduce((sum, span2) => sum + numberAttr(span2, "llm.input_tokens"), 0),
2286
+ outputTokens: llmSpans.reduce((sum, span2) => sum + numberAttr(span2, "llm.output_tokens"), 0),
2287
+ models: [...new Set(llmSpans.map((span2) => stringAttr(span2, "llm.model_name")).filter((value) => Boolean(value)))].sort(),
2288
+ tools: summarizeTools(spans),
2289
+ firstSpanAt,
2290
+ lastSpanAt
2291
+ },
2292
+ signals: {
2293
+ stuckLoopCount: loopFindings.length,
2294
+ affectedRunRatio: pipelines.stuckLoops.affectedRunRatio,
2295
+ stuckLoops: loopFindings.slice(0, loopLimit).map((finding) => ({
2296
+ toolName: finding.toolName,
2297
+ occurrences: finding.occurrences
2298
+ })),
2299
+ stuckLoopsOmitted: Math.max(0, loopFindings.length - loopLimit),
2300
+ toolErrorRate: toolSpans.length === 0 ? 0 : erroredToolCallCount / toolSpans.length
2301
+ },
2302
+ provenance: {
2303
+ source: "traces",
2304
+ evidenceKind: "session-summary",
2305
+ ...opts.otlpPath ? { otlpPath: opts.otlpPath } : {},
2306
+ notCampaignCell: true,
2307
+ note: "This is normalized coding-agent session evidence for downstream policy mining; it is not an eval campaign cell."
2308
+ }
2309
+ };
2310
+ }
2311
+ async function collectPolicyEvidence(opts) {
2312
+ const records = [];
2313
+ for await (const session of scanSessions(opts)) {
2314
+ records.push(await buildPolicyEvidenceRecord(session.ref, session.spans, opts));
2315
+ }
2316
+ return records;
2317
+ }
2318
+ function serializePolicyEvidence(records) {
2319
+ if (records.length === 0) return "";
2320
+ return `${records.map((record) => JSON.stringify(record)).join("\n")}
2321
+ `;
2322
+ }
2323
+ async function writePolicyEvidenceFile(records, outPath) {
2324
+ const path = outPath ?? join12(await mkdtemp(join12(tmpdir(), "traces-evidence-")), "policy-evidence.jsonl");
2325
+ await writeFile(path, serializePolicyEvidence(records), "utf8");
2326
+ return path;
2327
+ }
2328
+
2329
+ // src/external.ts
2330
+ import { spawn } from "child_process";
2331
+ function runCommand(command, args = [], opts = {}) {
2332
+ const timeoutMs = opts.timeoutMs ?? 12e4;
2333
+ const maxBuffer = opts.maxBuffer ?? 32 * 1024 * 1024;
2334
+ return new Promise((resolve, reject) => {
2335
+ let child;
2336
+ try {
2337
+ child = spawn(command, [...args], { signal: opts.signal });
2338
+ } catch (err2) {
2339
+ reject(err2 instanceof Error ? err2 : new Error(String(err2)));
2340
+ return;
2341
+ }
2342
+ let out = "";
2343
+ let err = "";
2344
+ let killed = false;
2345
+ const timer = setTimeout(() => {
2346
+ killed = true;
2347
+ child.kill("SIGKILL");
2348
+ reject(new Error(`${command}: timed out after ${timeoutMs}ms`));
2349
+ }, timeoutMs);
2350
+ const cap = (add, buf) => {
2351
+ const next = buf + add;
2352
+ if (next.length > maxBuffer) {
2353
+ killed = true;
2354
+ child.kill("SIGKILL");
2355
+ clearTimeout(timer);
2356
+ reject(new Error(`${command}: output exceeded ${maxBuffer} bytes`));
2357
+ }
2358
+ return next;
2359
+ };
2360
+ child.stdout?.on("data", (d) => {
2361
+ out = cap(String(d), out);
2362
+ });
2363
+ child.stderr?.on("data", (d) => {
2364
+ err = cap(String(d), err);
2365
+ });
2366
+ child.on("error", (e) => {
2367
+ clearTimeout(timer);
2368
+ if (!killed) reject(e);
2369
+ });
2370
+ child.on("close", (code) => {
2371
+ clearTimeout(timer);
2372
+ if (!killed) resolve({ stdout: out, stderr: err, code });
2373
+ });
2374
+ if (opts.input !== void 0) {
2375
+ child.stdin?.on("error", () => {
2376
+ });
2377
+ child.stdin?.end(opts.input);
2378
+ }
2379
+ });
2380
+ }
2381
+ function commandAnalyzer(spec) {
2382
+ return {
2383
+ name: spec.name,
2384
+ async analyze(otlpPath, opts = {}) {
2385
+ try {
2386
+ const res = await runCommand(spec.command, spec.args(otlpPath, opts.prompt), {
2387
+ signal: opts.signal,
2388
+ timeoutMs: spec.timeoutMs
2389
+ });
2390
+ if (res.code !== 0) {
2391
+ return { analyzer: spec.name, ok: false, output: res.stdout, error: res.stderr.trim() || `exit ${res.code}` };
2083
2392
  }
2393
+ return { analyzer: spec.name, ok: true, output: res.stdout.trim(), findings: spec.parse?.(res.stdout) };
2394
+ } catch (e) {
2395
+ return { analyzer: spec.name, ok: false, output: "", error: e instanceof Error ? e.message : String(e) };
2084
2396
  }
2085
2397
  }
2086
- try {
2087
- opts.onTick?.({ sessions: sessionCount });
2088
- } catch (err) {
2089
- opts.onError?.(err);
2398
+ };
2399
+ }
2400
+ function haloAnalyzer(opts = {}) {
2401
+ const defaultPrompt = opts.defaultPrompt ?? "diagnose";
2402
+ return commandAnalyzer({
2403
+ name: "halo",
2404
+ command: opts.command ?? "halo",
2405
+ args: (otlpPath, prompt) => {
2406
+ const args = [otlpPath, "-p", prompt ?? defaultPrompt];
2407
+ if (opts.model) args.push("-m", opts.model);
2408
+ return args;
2409
+ },
2410
+ timeoutMs: opts.timeoutMs
2411
+ });
2412
+ }
2413
+ function runExternalAnalyzers(otlpPath, analyzers, opts = {}) {
2414
+ return Promise.all(analyzers.map((a) => a.analyze(otlpPath, opts)));
2415
+ }
2416
+ function commandRedactor(spec) {
2417
+ const name = spec.name ?? spec.command;
2418
+ return {
2419
+ name,
2420
+ async redactText(texts) {
2421
+ if (texts.length === 0) return [];
2422
+ const res = await runCommand(spec.command, spec.args ?? [], {
2423
+ input: JSON.stringify(texts),
2424
+ timeoutMs: spec.timeoutMs
2425
+ });
2426
+ if (res.code !== 0) throw new Error(`redactor ${name}: exit ${res.code}: ${res.stderr.trim()}`);
2427
+ let parsed;
2428
+ try {
2429
+ parsed = JSON.parse(res.stdout);
2430
+ } catch {
2431
+ throw new Error(`redactor ${name}: stdout was not JSON`);
2432
+ }
2433
+ if (!Array.isArray(parsed) || parsed.length !== texts.length) {
2434
+ throw new Error(`redactor ${name}: expected a JSON array of ${texts.length} strings`);
2435
+ }
2436
+ return parsed.map(String);
2090
2437
  }
2091
- if (opts.signal?.aborted) break;
2092
- await sleep(intervalMs, opts.signal);
2093
- }
2438
+ };
2094
2439
  }
2095
2440
 
2096
- // src/attributes.ts
2097
- var ATTR = {
2098
- /** = the trace/session id. Wire contract (dedup linkage). */
2099
- SESSION_ID: "tangle.sessionId",
2100
- /** "cli" marks a CLI upload so the platform can drop a re-upload of a session
2101
- * that also streamed live. Wire contract. */
2102
- INGEST_SOURCE: "tangle.ingest_source",
2103
- HARNESS: "tangle.harness",
2104
- CWD: "tangle.cwd",
2105
- GIT_BRANCH: "tangle.git_branch",
2106
- HOST: "tangle.host",
2107
- /** Basename of the session file. (Renamed from the ambiguous `tangle.source`,
2108
- * which collided with `tangle.ingest_source`.) */
2109
- SESSION_FILE: "tangle.session_file",
2110
- UPLOADED_AT: "tangle.uploaded_at",
2111
- UPLOADER: "tangle.uploader",
2112
- REDACTION_VERSION: "redaction.version",
2113
- REDACTION_COUNT: "redaction.count"
2114
- };
2115
- var INGEST_SOURCE_CLI = "cli";
2116
- var DEFAULT_HARNESS = "claude-code";
2117
-
2118
2441
  // src/redact.ts
2119
2442
  import { DEFAULT_REDACTION_RULES, redactValue } from "@tangle-network/agent-eval/traces";
2120
2443
  var CODING_REDACTION_RULES = [
@@ -2175,20 +2498,564 @@ async function applyRedactor(spans, redactor) {
2175
2498
  return { spans: out, changed };
2176
2499
  }
2177
2500
 
2178
- // src/upload-state.ts
2501
+ // src/file-export.ts
2179
2502
  import { createHash } from "crypto";
2180
- import { mkdir, readFile as readFile12, writeFile as writeFile2 } from "fs/promises";
2503
+ import { mkdtemp as mkdtemp2, readFile as readFile12, writeFile as writeFile2 } from "fs/promises";
2504
+ import { tmpdir as tmpdir2 } from "os";
2505
+ import { join as join13 } from "path";
2506
+ function isObject(value) {
2507
+ return value != null && typeof value === "object" && !Array.isArray(value);
2508
+ }
2509
+ function stringValue(value) {
2510
+ return typeof value === "string" && value.length > 0 ? value : void 0;
2511
+ }
2512
+ function objectValue(value) {
2513
+ return isObject(value) ? value : void 0;
2514
+ }
2515
+ function stableJson(value) {
2516
+ return JSON.stringify(value, (_key, v) => {
2517
+ if (!isObject(v)) return v;
2518
+ return Object.keys(v).sort().reduce((acc, key) => {
2519
+ acc[key] = v[key];
2520
+ return acc;
2521
+ }, {});
2522
+ });
2523
+ }
2524
+ function hashId(value, chars = 16) {
2525
+ return createHash("sha256").update(stableJson(value)).digest("hex").slice(0, chars);
2526
+ }
2527
+ function isoTime(value) {
2528
+ if (typeof value === "string") {
2529
+ const ms = Date.parse(value);
2530
+ return Number.isFinite(ms) ? new Date(ms).toISOString() : void 0;
2531
+ }
2532
+ if (typeof value === "number" && Number.isFinite(value)) {
2533
+ const ms = value < 1e10 ? value * 1e3 : value;
2534
+ return new Date(ms).toISOString();
2535
+ }
2536
+ return void 0;
2537
+ }
2538
+ function firstTime(obj, keys) {
2539
+ for (const key of keys) {
2540
+ const value = isoTime(obj[key]);
2541
+ if (value) return value;
2542
+ }
2543
+ return void 0;
2544
+ }
2545
+ function firstTimeDeep(value, keys, depth = 5) {
2546
+ if (depth < 0) return void 0;
2547
+ if (isObject(value)) {
2548
+ const direct = firstTime(value, keys);
2549
+ if (direct) return direct;
2550
+ for (const child of Object.values(value)) {
2551
+ const found = firstTimeDeep(child, keys, depth - 1);
2552
+ if (found) return found;
2553
+ }
2554
+ } else if (Array.isArray(value)) {
2555
+ for (const child of value) {
2556
+ const found = firstTimeDeep(child, keys, depth - 1);
2557
+ if (found) return found;
2558
+ }
2559
+ }
2560
+ return void 0;
2561
+ }
2562
+ function copyDefined(target, entries) {
2563
+ for (const [key, value] of Object.entries(entries)) {
2564
+ if (value != null && value !== "") target[key] = value;
2565
+ }
2566
+ }
2567
+ function parseJsonRows(text) {
2568
+ const trimmed = text.trim();
2569
+ if (!trimmed) throw new Error("input file is empty");
2570
+ if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
2571
+ const parsed = JSON.parse(trimmed);
2572
+ if (Array.isArray(parsed)) return { rows: parsed };
2573
+ if (isObject(parsed)) {
2574
+ const events = parsed.events;
2575
+ if (Array.isArray(events)) return { rows: events, wrapper: parsed };
2576
+ return { rows: [parsed] };
2577
+ }
2578
+ throw new Error("input JSON must be an object, array, or JSONL rows");
2579
+ }
2580
+ return {
2581
+ rows: trimmed.split(/\r?\n/).filter((line) => line.trim().length > 0).map((line) => JSON.parse(line))
2582
+ };
2583
+ }
2584
+ function isPolicyEvidenceRow(row) {
2585
+ return isObject(row) && row.kind === "traces.policy_evidence.session";
2586
+ }
2587
+ function isOpenInferenceRow(row) {
2588
+ if (!isObject(row)) return false;
2589
+ return typeof row.trace_id === "string" && typeof row.span_id === "string" && typeof row.name === "string" && typeof row.start_time === "string" && typeof row.end_time === "string" && isObject(row.status) && isObject(row.attributes) && isObject(row.resource) && isObject(row.scope);
2590
+ }
2591
+ function eventType(row) {
2592
+ return stringValue(row.type) ?? stringValue(row.event) ?? stringValue(row.name) ?? stringValue(row.kind);
2593
+ }
2594
+ function looksLikeSandboxEvent(row) {
2595
+ if (!isObject(row)) return false;
2596
+ const type = eventType(row)?.toLowerCase();
2597
+ return type === "start" || type === "raw" || type === "result" || type === "done" || type === "error";
2598
+ }
2599
+ function detectFormat(rows, requested) {
2600
+ if (requested !== "auto") return requested;
2601
+ if (rows.length === 0) throw new Error("input contains no rows");
2602
+ if (rows.every(isPolicyEvidenceRow)) return "policy-evidence";
2603
+ if (rows.every(isOpenInferenceRow)) return "openinference";
2604
+ if (rows.some(looksLikeSandboxEvent)) return "sandbox-events";
2605
+ throw new Error("could not detect input format; use --format policy-evidence, sandbox-events, or openinference");
2606
+ }
2607
+ function requirePolicyEvidenceRows(rows) {
2608
+ if (!rows.every(isPolicyEvidenceRow)) throw new Error("policy-evidence input must contain only traces.policy_evidence.session rows");
2609
+ return rows;
2610
+ }
2611
+ function requireOpenInferenceRows(rows) {
2612
+ if (!rows.every(isOpenInferenceRow)) throw new Error("openinference input must contain only complete OpenInference span rows");
2613
+ return rows;
2614
+ }
2615
+ function requireObjectRows(rows) {
2616
+ if (!rows.every(isObject)) throw new Error("sandbox-events input must contain only JSON object events");
2617
+ return rows;
2618
+ }
2619
+ function evidenceTimeBounds(record) {
2620
+ const sessionTime = new Date(record.session.mtimeMs).toISOString();
2621
+ const start = record.metrics.firstSpanAt ?? record.generatedAt ?? sessionTime;
2622
+ const end = record.metrics.lastSpanAt ?? start;
2623
+ return { start, end };
2624
+ }
2625
+ function policyEvidenceToSpans(records) {
2626
+ return records.map((record) => {
2627
+ const { start, end } = evidenceTimeBounds(record);
2628
+ const traceId = record.session.sessionId || hashId(record, 32);
2629
+ const extra = {
2630
+ "traces.source_format": "policy-evidence",
2631
+ "traces.policy_evidence.schema_version": record.schemaVersion,
2632
+ "traces.policy_evidence.kind": record.kind,
2633
+ "traces.session.id": record.session.sessionId,
2634
+ "traces.session.path": record.session.path,
2635
+ "traces.session.cwd": record.session.cwd,
2636
+ "traces.session.mtime_ms": record.session.mtimeMs,
2637
+ "traces.metrics.span_count": record.metrics.spanCount,
2638
+ "traces.metrics.llm_turn_count": record.metrics.llmTurnCount,
2639
+ "traces.metrics.tool_call_count": record.metrics.toolCallCount,
2640
+ "traces.metrics.errored_tool_call_count": record.metrics.erroredToolCallCount,
2641
+ "traces.metrics.input_tokens": record.metrics.inputTokens,
2642
+ "traces.metrics.output_tokens": record.metrics.outputTokens,
2643
+ "traces.metrics.models": record.metrics.models,
2644
+ "traces.metrics.tools": stableJson(record.metrics.tools),
2645
+ "traces.signals.stuck_loop_count": record.signals.stuckLoopCount,
2646
+ "traces.signals.affected_run_ratio": record.signals.affectedRunRatio,
2647
+ "traces.signals.stuck_loops": stableJson(record.signals.stuckLoops),
2648
+ "traces.signals.stuck_loops_omitted": record.signals.stuckLoopsOmitted,
2649
+ "traces.signals.tool_error_rate": record.signals.toolErrorRate,
2650
+ "traces.provenance.source": record.provenance.source,
2651
+ "traces.provenance.evidence_kind": record.provenance.evidenceKind,
2652
+ "traces.provenance.otlp_path": record.provenance.otlpPath,
2653
+ [ATTR.SUBJECT_KEY]: record.repo.subjectKey,
2654
+ [ATTR.GIT_REPOSITORY]: record.repo.repository,
2655
+ [ATTR.GIT_BRANCH_NAME]: record.repo.branch,
2656
+ [ATTR.GIT_COMMIT]: record.repo.commit,
2657
+ [ATTR.CWD]: record.repo.cwd ?? record.session.cwd
2658
+ };
2659
+ return span({
2660
+ traceId,
2661
+ spanId: `policy:${hashId(record)}`,
2662
+ parentSpanId: null,
2663
+ name: "policy_evidence.session",
2664
+ kind: "AGENT",
2665
+ startTime: start,
2666
+ endTime: end,
2667
+ service: record.session.harness,
2668
+ agent: "traces",
2669
+ content: capText(stableJson({
2670
+ session: record.session,
2671
+ repo: record.repo,
2672
+ metrics: record.metrics,
2673
+ signals: record.signals,
2674
+ provenance: record.provenance
2675
+ })),
2676
+ extra
2677
+ });
2678
+ });
2679
+ }
2680
+ function findStringKey(value, keys, depth = 4) {
2681
+ if (depth < 0) return void 0;
2682
+ if (isObject(value)) {
2683
+ for (const key of keys) {
2684
+ const found = stringValue(value[key]);
2685
+ if (found) return found;
2686
+ }
2687
+ for (const child of Object.values(value)) {
2688
+ const found = findStringKey(child, keys, depth - 1);
2689
+ if (found) return found;
2690
+ }
2691
+ } else if (Array.isArray(value)) {
2692
+ for (const child of value) {
2693
+ const found = findStringKey(child, keys, depth - 1);
2694
+ if (found) return found;
2695
+ }
2696
+ }
2697
+ return void 0;
2698
+ }
2699
+ function eventTimestamp(row) {
2700
+ return firstTimeDeep(row, [
2701
+ "timestamp",
2702
+ "start_time",
2703
+ "startTime",
2704
+ "startedAt",
2705
+ "createdAt",
2706
+ "created_at",
2707
+ "created",
2708
+ "time"
2709
+ ]);
2710
+ }
2711
+ function eventEndTimestamp(row, fallback) {
2712
+ return firstTimeDeep(row, [
2713
+ "end_time",
2714
+ "endTime",
2715
+ "completedAt",
2716
+ "completed_at",
2717
+ "finishedAt",
2718
+ "updatedAt",
2719
+ "updated_at",
2720
+ "updated"
2721
+ ]) ?? fallback;
2722
+ }
2723
+ function eventStatus(row, type) {
2724
+ const lowered = type.toLowerCase();
2725
+ const errorMessage = stringValue(row.error) ?? findStringKey(row, ["error", "errorMessage", "error_message"], 3);
2726
+ if (lowered.includes("error") || errorMessage) {
2727
+ const message = errorMessage ?? stringValue(row.message);
2728
+ return { status: "ERROR", ...message ? { message: capText(message) } : {} };
2729
+ }
2730
+ return { status: "OK" };
2731
+ }
2732
+ function eventKind(row, type) {
2733
+ const lowered = type.toLowerCase();
2734
+ const tool = findStringKey(row, ["tool", "toolName", "tool_name"], 4);
2735
+ if (tool || lowered.includes("tool") || stableJson(row).includes("tool-invocation")) return { kind: "TOOL", tool };
2736
+ if (lowered.includes("llm") || lowered === "token") return { kind: "LLM" };
2737
+ return { kind: "CHAIN" };
2738
+ }
2739
+ function sandboxEventsToSpans(rows, wrapper) {
2740
+ const context = wrapper ?? {};
2741
+ const traceId = findStringKey(context, ["trace_id", "traceId", "session_id", "sessionId", "run_id", "runId"], 3) ?? findStringKey(rows, ["trace_id", "traceId", "session_id", "sessionId", "run_id", "runId"], 4) ?? `sandbox:${hashId(rows, 32)}`;
2742
+ const service = findStringKey(context, ["service", "harness", "source"], 2) ?? "sandbox-opencode";
2743
+ const times = rows.flatMap((row) => {
2744
+ const time = eventTimestamp(row);
2745
+ return time ? [time] : [];
2746
+ }).sort();
2747
+ const rootId = `events:${hashId({ traceId, rows: rows.length })}`;
2748
+ const hasError = rows.some((row) => eventStatus(row, eventType(row) ?? "event").status === "ERROR");
2749
+ const rootStart = times[0] ?? (/* @__PURE__ */ new Date(0)).toISOString();
2750
+ const rootEnd = times[times.length - 1] ?? rootStart;
2751
+ const spans = [
2752
+ span({
2753
+ traceId,
2754
+ spanId: rootId,
2755
+ parentSpanId: null,
2756
+ name: "sandbox.events",
2757
+ kind: "AGENT",
2758
+ startTime: rootStart,
2759
+ endTime: rootEnd,
2760
+ status: hasError ? "ERROR" : "OK",
2761
+ service,
2762
+ agent: service,
2763
+ extra: {
2764
+ "traces.source_format": "sandbox-events",
2765
+ "traces.event_count": rows.length
2766
+ }
2767
+ })
2768
+ ];
2769
+ let previousTime = rootStart;
2770
+ rows.forEach((row, index) => {
2771
+ const type = eventType(row) ?? "event";
2772
+ const time = eventTimestamp(row) ?? previousTime;
2773
+ previousTime = time;
2774
+ const { status, message } = eventStatus(row, type);
2775
+ const { kind, tool } = eventKind(row, type);
2776
+ const name = kind === "TOOL" && tool ? `tool.${tool}` : `event.${type}`;
2777
+ const extra = {
2778
+ "traces.source_format": "sandbox-events",
2779
+ "traces.event.type": type,
2780
+ "traces.event.index": index
2781
+ };
2782
+ copyDefined(extra, {
2783
+ "traces.event.id": stringValue(row.id),
2784
+ "traces.event.raw_type": stringValue(row.type),
2785
+ "traces.event.raw_event": stringValue(row.event)
2786
+ });
2787
+ spans.push(span({
2788
+ traceId,
2789
+ spanId: `event:${index}:${hashId(row)}`,
2790
+ parentSpanId: rootId,
2791
+ name,
2792
+ kind,
2793
+ startTime: time,
2794
+ endTime: eventEndTimestamp(row, time),
2795
+ status,
2796
+ statusMessage: message,
2797
+ service,
2798
+ agent: service,
2799
+ tool: kind === "TOOL" ? tool ?? type : null,
2800
+ step: index,
2801
+ content: capText(stableJson(row)),
2802
+ extra
2803
+ }));
2804
+ });
2805
+ return spans;
2806
+ }
2807
+ function statusCode(value) {
2808
+ return value === "OK" || value === "ERROR" || value === "UNSET" ? value : "UNSET";
2809
+ }
2810
+ function openInferenceToSpans(rows) {
2811
+ return rows.map((row) => {
2812
+ const attrs = isObject(row.attributes) ? { ...row.attributes } : {};
2813
+ const resource = objectValue(row.resource);
2814
+ const resourceAttrs = objectValue(resource?.attributes);
2815
+ if (resourceAttrs) copyDefined(attrs, resourceAttrs);
2816
+ const kind = stringValue(row.kind);
2817
+ if (kind && attrs["openinference.span.kind"] == null) attrs["openinference.span.kind"] = kind;
2818
+ const status = objectValue(row.status);
2819
+ const message = stringValue(status?.message);
2820
+ return {
2821
+ trace_id: stringValue(row.trace_id) ?? hashId(row, 32),
2822
+ span_id: stringValue(row.span_id) ?? hashId({ row, field: "span" }),
2823
+ parent_span_id: stringValue(row.parent_span_id) || null,
2824
+ name: stringValue(row.name) ?? "span",
2825
+ start_time: stringValue(row.start_time) ?? (/* @__PURE__ */ new Date(0)).toISOString(),
2826
+ end_time: stringValue(row.end_time) ?? stringValue(row.start_time) ?? (/* @__PURE__ */ new Date(0)).toISOString(),
2827
+ status: { code: statusCode(status?.code), ...message ? { message } : {} },
2828
+ attributes: attrs
2829
+ };
2830
+ });
2831
+ }
2832
+ function normalizeAttributes(attributes) {
2833
+ const normalized = {};
2834
+ for (const [key, value] of Object.entries(attributes ?? {})) {
2835
+ if (!key || value == null) continue;
2836
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
2837
+ normalized[key] = value;
2838
+ } else if (Array.isArray(value) && value.every(
2839
+ (item) => typeof item === "string" || typeof item === "number" || typeof item === "boolean"
2840
+ )) {
2841
+ normalized[key] = value;
2842
+ } else {
2843
+ normalized[key] = stableJson(value);
2844
+ }
2845
+ }
2846
+ return normalized;
2847
+ }
2848
+ function withExportAttributes(spans, attributes) {
2849
+ const normalized = normalizeAttributes(attributes);
2850
+ if (Object.keys(normalized).length === 0) return [...spans];
2851
+ return spans.map((s) => ({ ...s, attributes: { ...s.attributes, ...normalized } }));
2852
+ }
2853
+ function exportTraceEvidenceRows(rows, opts = {}, wrapper) {
2854
+ const format = detectFormat(rows, opts.format ?? "auto");
2855
+ const converted = format === "policy-evidence" ? policyEvidenceToSpans(requirePolicyEvidenceRows(rows)) : format === "openinference" ? openInferenceToSpans(requireOpenInferenceRows(rows)) : sandboxEventsToSpans(requireObjectRows(rows), wrapper);
2856
+ const spans = withExportAttributes(converted, opts.attributes);
2857
+ if (spans.length === 0) throw new Error(`no spans exported from ${format} input`);
2858
+ const redacted = redactSpans(spans);
2859
+ return {
2860
+ format,
2861
+ spans: redacted.spans,
2862
+ redactionCount: redacted.report.redactionCount,
2863
+ redactionsByRule: redacted.report.byRule
2864
+ };
2865
+ }
2866
+ function exportTraceEvidenceText(text, opts = {}) {
2867
+ const parsed = parseJsonRows(text);
2868
+ return exportTraceEvidenceRows(parsed.rows, opts, parsed.wrapper);
2869
+ }
2870
+ async function exportTraceEvidenceFile(inputPath, opts = {}) {
2871
+ const text = await readFile12(inputPath, "utf8");
2872
+ return exportTraceEvidenceText(text, { ...opts, sourcePath: inputPath });
2873
+ }
2874
+ async function writeTraceEvidenceExportFile(inputPath, outPath, opts = {}) {
2875
+ const result = await exportTraceEvidenceFile(inputPath, opts);
2876
+ const path = outPath ?? join13(await mkdtemp2(join13(tmpdir2(), "traces-export-")), "spans.openinference.jsonl");
2877
+ await writeFile2(path, serializeSpans(result.spans), "utf8");
2878
+ return { ...result, path };
2879
+ }
2880
+
2881
+ // src/observer.ts
2882
+ var sleep = (ms, signal) => new Promise((resolve) => {
2883
+ const onAbort = () => {
2884
+ clearTimeout(t);
2885
+ resolve();
2886
+ };
2887
+ const t = setTimeout(() => {
2888
+ signal?.removeEventListener("abort", onAbort);
2889
+ resolve();
2890
+ }, ms);
2891
+ signal?.addEventListener("abort", onAbort, { once: true });
2892
+ });
2893
+ function loopKey(sessionId, toolName2, argHash) {
2894
+ return `${sessionId}\0${toolName2}\0${argHash}`;
2895
+ }
2896
+ async function watchSessions(opts) {
2897
+ const windowMs = opts.windowMs ?? 30 * 6e4;
2898
+ const intervalMs = Math.max(250, opts.intervalMs ?? 5e3);
2899
+ const minLoop = opts.minLoopOccurrences ?? 3;
2900
+ const seen = /* @__PURE__ */ new Map();
2901
+ while (!opts.signal?.aborted) {
2902
+ let sessionCount = 0;
2903
+ for await (const { adapter, ref, spans } of scanSessions({
2904
+ adapters: opts.adapters,
2905
+ all: opts.all,
2906
+ harnesses: opts.harnesses,
2907
+ cwd: opts.cwd,
2908
+ sinceMs: Date.now() - windowMs,
2909
+ signal: opts.signal,
2910
+ onError: opts.onError
2911
+ })) {
2912
+ let report;
2913
+ try {
2914
+ report = await runPipelines(spans, { minLoopOccurrences: minLoop });
2915
+ } catch (err) {
2916
+ opts.onError?.(err, ref);
2917
+ continue;
2918
+ }
2919
+ sessionCount += 1;
2920
+ if (opts.onReport) {
2921
+ try {
2922
+ await opts.onReport(ref, report);
2923
+ } catch (err) {
2924
+ opts.onError?.(err, ref);
2925
+ }
2926
+ }
2927
+ for (const f of report.stuckLoops.findings) {
2928
+ const key = loopKey(ref.sessionId, f.toolName, f.argHash);
2929
+ if (f.occurrences > (seen.get(key) ?? 0)) {
2930
+ seen.set(key, f.occurrences);
2931
+ if (opts.onLoop) {
2932
+ try {
2933
+ await opts.onLoop({
2934
+ harness: adapter.harness,
2935
+ sessionId: ref.sessionId,
2936
+ cwd: ref.cwd,
2937
+ toolName: f.toolName,
2938
+ argHash: f.argHash,
2939
+ occurrences: f.occurrences,
2940
+ windowMs: f.windowMs
2941
+ });
2942
+ } catch (err) {
2943
+ opts.onError?.(err, ref);
2944
+ }
2945
+ }
2946
+ }
2947
+ }
2948
+ }
2949
+ try {
2950
+ opts.onTick?.({ sessions: sessionCount });
2951
+ } catch (err) {
2952
+ opts.onError?.(err);
2953
+ }
2954
+ if (opts.signal?.aborted) break;
2955
+ await sleep(intervalMs, opts.signal);
2956
+ }
2957
+ }
2958
+
2959
+ // src/reactions.ts
2960
+ var REACTIONS = ["correction", "frustration", "jargon", "structure", "praise"];
2961
+ function emptySignals() {
2962
+ return { correction: 0, frustration: 0, jargon: 0, structure: 0, praise: 0 };
2963
+ }
2964
+ function spanStep(s) {
2965
+ const v = s.attributes.step;
2966
+ return typeof v === "number" ? v : Number.MAX_SAFE_INTEGER;
2967
+ }
2968
+ function ordered(spans) {
2969
+ return [...spans].sort((a, b) => {
2970
+ const ds = spanStep(a) - spanStep(b);
2971
+ if (ds !== 0) return ds;
2972
+ return a.start_time.localeCompare(b.start_time);
2973
+ });
2974
+ }
2975
+ function isAssistant(s) {
2976
+ const kind = s.attributes["openinference.span.kind"];
2977
+ if (s.name === "user.prompt") return false;
2978
+ return kind === "LLM" || s.name.startsWith("message.assistant");
2979
+ }
2980
+ function isHumanPrompt(s) {
2981
+ return s.name === "user.prompt" && s.attributes[ACTOR_ATTR] === "human";
2982
+ }
2983
+ function contentOf(s) {
2984
+ const c = s.attributes.content;
2985
+ return typeof c === "string" ? c : "";
2986
+ }
2987
+ var MAX_PAIRS = 10;
2988
+ function pairStrength(reactions) {
2989
+ let n = 0;
2990
+ for (const r of reactions) n += CORRECTIVE_REACTIONS.includes(r) ? 2 : 1;
2991
+ return n;
2992
+ }
2993
+ function analyzeReactions(spans) {
2994
+ const byTrace = /* @__PURE__ */ new Map();
2995
+ for (const s of spans) {
2996
+ const arr = byTrace.get(s.trace_id) ?? [];
2997
+ arr.push(s);
2998
+ byTrace.set(s.trace_id, arr);
2999
+ }
3000
+ const sessions = [];
3001
+ const corpus = emptySignals();
3002
+ let corpusTurns = 0;
3003
+ const pairs = [];
3004
+ for (const [sessionId, traceSpans] of byTrace) {
3005
+ const seq = ordered(traceSpans);
3006
+ const sessionSignals = emptySignals();
3007
+ let humanReactionTurns = 0;
3008
+ let lastAssistant = null;
3009
+ for (const s of seq) {
3010
+ if (isAssistant(s)) {
3011
+ const text = contentOf(s);
3012
+ if (text) lastAssistant = text;
3013
+ continue;
3014
+ }
3015
+ if (!isHumanPrompt(s)) continue;
3016
+ if (lastAssistant === null) continue;
3017
+ humanReactionTurns += 1;
3018
+ const reactions = classifyReaction(contentOf(s));
3019
+ if (reactions.length > 0) {
3020
+ for (const r of reactions) {
3021
+ sessionSignals[r] += 1;
3022
+ corpus[r] += 1;
3023
+ }
3024
+ pairs.push({ assistant: lastAssistant, human: contentOf(s), reactions, sessionId });
3025
+ }
3026
+ lastAssistant = null;
3027
+ }
3028
+ corpusTurns += humanReactionTurns;
3029
+ sessions.push({ sessionId, humanReactionTurns, signals: sessionSignals });
3030
+ }
3031
+ const corrective = CORRECTIVE_REACTIONS.reduce((n, r) => n + corpus[r], 0);
3032
+ const positive = corpus.praise;
3033
+ const totalSignals = corrective + positive;
3034
+ const ratio = totalSignals === 0 ? null : positive === 0 ? Number.POSITIVE_INFINITY : corrective / positive;
3035
+ pairs.sort((a, b) => pairStrength(b.reactions) - pairStrength(a.reactions));
3036
+ return {
3037
+ sessions,
3038
+ signals: corpus,
3039
+ humanReactionTurns: corpusTurns,
3040
+ correctiveToPositiveRatio: ratio,
3041
+ triggerPairs: pairs.slice(0, MAX_PAIRS)
3042
+ };
3043
+ }
3044
+
3045
+ // src/upload-state.ts
3046
+ import { createHash as createHash2 } from "crypto";
3047
+ import { mkdir, readFile as readFile13, writeFile as writeFile3 } from "fs/promises";
2181
3048
  import { homedir as homedir11 } from "os";
2182
- import { dirname, join as join11 } from "path";
3049
+ import { dirname, join as join14 } from "path";
2183
3050
  function stateFile() {
2184
- const base = process.env.XDG_STATE_HOME ?? join11(homedir11(), ".local", "state");
2185
- return join11(base, "tangle-traces", "uploaded.json");
3051
+ const base = process.env.XDG_STATE_HOME ?? join14(homedir11(), ".local", "state");
3052
+ return join14(base, "tangle-traces", "uploaded.json");
2186
3053
  }
2187
3054
  function uploadKey(harness, sessionId) {
2188
3055
  return `${harness}:${sessionId}`;
2189
3056
  }
2190
3057
  function sessionHash(spans) {
2191
- const h = createHash("sha256");
3058
+ const h = createHash2("sha256");
2192
3059
  for (const s of spans) {
2193
3060
  h.update(`${s.span_id}\0${s.name}\0${s.start_time}\0${s.status.code}
2194
3061
  `);
@@ -2197,7 +3064,7 @@ function sessionHash(spans) {
2197
3064
  }
2198
3065
  async function loadState() {
2199
3066
  try {
2200
- return JSON.parse(await readFile12(stateFile(), "utf8"));
3067
+ return JSON.parse(await readFile13(stateFile(), "utf8"));
2201
3068
  } catch {
2202
3069
  return {};
2203
3070
  }
@@ -2205,7 +3072,7 @@ async function loadState() {
2205
3072
  async function saveState(state) {
2206
3073
  const f = stateFile();
2207
3074
  await mkdir(dirname(f), { recursive: true });
2208
- await writeFile2(f, `${JSON.stringify(state, null, 2)}
3075
+ await writeFile3(f, `${JSON.stringify(state, null, 2)}
2209
3076
  `, "utf8");
2210
3077
  }
2211
3078
  function alreadyUploaded(state, harness, sessionId, hash) {
@@ -2213,10 +3080,10 @@ function alreadyUploaded(state, harness, sessionId, hash) {
2213
3080
  }
2214
3081
 
2215
3082
  // src/upload.ts
2216
- import { readFile as readFile13 } from "fs/promises";
3083
+ import { readFile as readFile14 } from "fs/promises";
2217
3084
  import { createRequire } from "module";
2218
3085
  import { hostname } from "os";
2219
- import { basename as basename8, join as join12 } from "path";
3086
+ import { basename as basename8, join as join15 } from "path";
2220
3087
  import { hostedClientFromEnv } from "@tangle-network/agent-eval/hosted";
2221
3088
  import { REDACTION_VERSION } from "@tangle-network/agent-eval/traces";
2222
3089
  var require2 = createRequire(import.meta.url);
@@ -2240,7 +3107,7 @@ async function planUpload(opts = {}) {
2240
3107
  async function gitBranch(cwd) {
2241
3108
  if (!cwd) return void 0;
2242
3109
  try {
2243
- const head = await readFile13(join12(cwd, ".git", "HEAD"), "utf8");
3110
+ const head = await readFile14(join15(cwd, ".git", "HEAD"), "utf8");
2244
3111
  const m = head.match(/^ref:\s*refs\/heads\/(.+)$/m);
2245
3112
  return m?.[1]?.trim();
2246
3113
  } catch {
@@ -2348,25 +3215,31 @@ async function executeUpload(plan, opts = {}) {
2348
3215
  }
2349
3216
 
2350
3217
  export {
3218
+ countSkillRunsJsonl,
3219
+ analyzeAdoption,
2351
3220
  span,
2352
3221
  toOpenInferenceSpan,
2353
3222
  serializeSpans,
2354
3223
  writeOtlpFile,
2355
3224
  analyzeSpans,
2356
- runCommand,
2357
- commandAnalyzer,
2358
- toCanonicalOpenInference,
2359
- haloAnalyzer,
2360
- runExternalAnalyzers,
2361
- commandRedactor,
3225
+ ATTR,
3226
+ INGEST_SOURCE_CLI,
3227
+ DEFAULT_HARNESS,
2362
3228
  parseIsoToEpochMs,
2363
3229
  parseSince,
2364
3230
  toRuntimeStore,
2365
3231
  runPipelines,
2366
3232
  CONTENT_CAP,
2367
3233
  capText,
3234
+ ACTOR_ATTR,
2368
3235
  userPromptSpan,
2369
3236
  AmpAdapter,
3237
+ textIsSynthetic,
3238
+ looksLikeAgentPrompt,
3239
+ claudeActor,
3240
+ codexActor,
3241
+ classifyReaction,
3242
+ CORRECTIVE_REACTIONS,
2370
3243
  ClaudeAdapter,
2371
3244
  CodexAdapter,
2372
3245
  CopilotAdapter,
@@ -2380,15 +3253,31 @@ export {
2380
3253
  resolveAdapter,
2381
3254
  knownHarnesses,
2382
3255
  selectAdapters,
3256
+ normalizeRemote,
3257
+ resolveRepoAttrs,
3258
+ stampRepoAttrs,
3259
+ parseSession,
2383
3260
  scanSessions,
2384
- watchSessions,
2385
- ATTR,
2386
- INGEST_SOURCE_CLI,
2387
- DEFAULT_HARNESS,
3261
+ buildPolicyEvidenceRecord,
3262
+ collectPolicyEvidence,
3263
+ serializePolicyEvidence,
3264
+ writePolicyEvidenceFile,
3265
+ runCommand,
3266
+ commandAnalyzer,
3267
+ haloAnalyzer,
3268
+ runExternalAnalyzers,
3269
+ commandRedactor,
2388
3270
  CODING_REDACTION_RULES,
2389
3271
  TRACES_REDACTION_RULES,
2390
3272
  redactSpans,
2391
3273
  applyRedactor,
3274
+ exportTraceEvidenceRows,
3275
+ exportTraceEvidenceText,
3276
+ exportTraceEvidenceFile,
3277
+ writeTraceEvidenceExportFile,
3278
+ watchSessions,
3279
+ REACTIONS,
3280
+ analyzeReactions,
2392
3281
  uploadKey,
2393
3282
  sessionHash,
2394
3283
  loadState,