@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.
package/dist/cli.js CHANGED
@@ -1,23 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ analyzeAdoption,
4
+ analyzeReactions,
3
5
  analyzeSpans,
6
+ buildPolicyEvidenceRecord,
4
7
  commandAnalyzer,
5
8
  commandRedactor,
6
9
  executeUpload,
10
+ exportTraceEvidenceFile,
7
11
  haloAnalyzer,
8
12
  knownHarnesses,
13
+ parseSession,
9
14
  parseSince,
10
15
  planUpload,
11
16
  resolveAdapter,
12
17
  runExternalAnalyzers,
13
18
  runPipelines,
14
19
  selectAdapters,
20
+ serializePolicyEvidence,
21
+ serializeSpans,
15
22
  watchSessions,
16
- writeOtlpFile
17
- } from "./chunk-BUD6I3ZJ.js";
23
+ writeOtlpFile,
24
+ writePolicyEvidenceFile,
25
+ writeTraceEvidenceExportFile
26
+ } from "./chunk-D5OMTM4S.js";
18
27
 
19
28
  // src/cli.ts
20
- import { stat, writeFile } from "fs/promises";
29
+ import { readFile, stat, writeFile } from "fs/promises";
21
30
 
22
31
  // src/report.ts
23
32
  var SEVERITY_RANK = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
@@ -95,11 +104,93 @@ function renderPipelines(pr) {
95
104
  lines.push("");
96
105
  return lines.join("\n");
97
106
  }
107
+ var REACTION_BADGE = {
108
+ correction: "correction",
109
+ frustration: "frustration",
110
+ jargon: "jargon-complaint",
111
+ structure: "structure-complaint",
112
+ praise: "praise"
113
+ };
114
+ function ratioStr(r) {
115
+ if (r === null) return "n/a (no reaction signals)";
116
+ if (!Number.isFinite(r)) return "\u221E (corrective with zero praise)";
117
+ return `${r.toFixed(2)}:1`;
118
+ }
119
+ function oneLine(s, n) {
120
+ const t = s.replace(/\s+/g, " ").trim();
121
+ return t.length > n ? `${t.slice(0, n)}\u2026` : t;
122
+ }
123
+ function renderReactions(rr) {
124
+ const lines = ["## user reactions (deterministic, human turns only)", ""];
125
+ const total = Object.values(rr.signals).reduce((a, b) => a + b, 0);
126
+ if (rr.humanReactionTurns === 0) {
127
+ lines.push("- No human turns followed an assistant turn (nothing to classify).");
128
+ lines.push("");
129
+ return lines.join("\n");
130
+ }
131
+ lines.push(
132
+ `- **Reaction turns:** ${rr.humanReactionTurns} human turn(s) followed an assistant turn; ${total} carried a reaction signal.`
133
+ );
134
+ lines.push(`- **Corrective-to-positive ratio:** ${ratioStr(rr.correctiveToPositiveRatio)}`);
135
+ lines.push("");
136
+ lines.push("| Signal | Count |");
137
+ lines.push("|---|---|");
138
+ for (const [k, v] of Object.entries(rr.signals)) {
139
+ if (v > 0) lines.push(`| ${REACTION_BADGE[k] ?? k} | ${v} |`);
140
+ }
141
+ lines.push("");
142
+ if (rr.triggerPairs.length > 0) {
143
+ lines.push("### top trigger pairs (assistant prose \u2192 human reaction)");
144
+ lines.push("");
145
+ for (const p of rr.triggerPairs) {
146
+ lines.push(`- **[${p.reactions.map((r) => REACTION_BADGE[r] ?? r).join(", ")}]**`);
147
+ lines.push(` - assistant: \`${oneLine(p.assistant, 180)}\``);
148
+ lines.push(` - human: \`${oneLine(p.human, 180)}\``);
149
+ }
150
+ lines.push("");
151
+ }
152
+ return lines.join("\n");
153
+ }
154
+ function renderAdoption(ar) {
155
+ const lines = ["## skill & subagent adoption (deterministic)", ""];
156
+ lines.push(
157
+ `- **Skill penetration:** ${(ar.skillPenetration * 100).toFixed(0)}% (${ar.sessionsWithSkill}/${ar.sessionCount} session(s) invoked a skill explicitly)`
158
+ );
159
+ lines.push(
160
+ `- **Explicit skill invocations:** ${ar.totalSkillInvocations} (Skill tool spans) \xB7 **Loop-dispatched runs:** ${ar.totalLoopDispatchedRuns} (from ${ar.skillRunFilesRead} \`.evolve/skill-runs.jsonl\` file(s))`
161
+ );
162
+ lines.push(
163
+ `- **Subagent spawns:** ${ar.totalSubagentSpawns} across ${ar.sessionsWithSubagent} session(s)`
164
+ );
165
+ lines.push("");
166
+ const skillRows = Object.entries(ar.skillInvocations).sort((a, b) => b[1] - a[1]);
167
+ const loopRows = Object.entries(ar.loopDispatchedRuns).sort((a, b) => b[1] - a[1]);
168
+ if (skillRows.length > 0 || loopRows.length > 0) {
169
+ lines.push("| Skill | Explicit invocations | Loop-dispatched runs |");
170
+ lines.push("|---|---|---|");
171
+ const names = [.../* @__PURE__ */ new Set([...skillRows.map((r) => r[0]), ...loopRows.map((r) => r[0])])].sort(
172
+ (a, b) => (ar.skillInvocations[b] ?? 0) + (ar.loopDispatchedRuns[b] ?? 0) - (ar.skillInvocations[a] ?? 0) - (ar.loopDispatchedRuns[a] ?? 0)
173
+ );
174
+ for (const n of names) {
175
+ lines.push(`| \`${n}\` | ${ar.skillInvocations[n] ?? 0} | ${ar.loopDispatchedRuns[n] ?? 0} |`);
176
+ }
177
+ lines.push("");
178
+ }
179
+ const agentRows = Object.entries(ar.subagentSpawns).sort((a, b) => b[1] - a[1]);
180
+ if (agentRows.length > 0) {
181
+ lines.push("| Subagent | Spawns |");
182
+ lines.push("|---|---|");
183
+ for (const [n, c] of agentRows) lines.push(`| \`${n}\` | ${c} |`);
184
+ lines.push("");
185
+ }
186
+ return lines.join("\n");
187
+ }
98
188
 
99
189
  // src/cli.ts
100
190
  function parseArgs(argv) {
101
191
  const a = {
102
- command: argv[0] ?? "help",
192
+ command: argv[0] === "--help" || argv[0] === "-h" ? "help" : argv[0] ?? "help",
193
+ help: argv[0] === "--help" || argv[0] === "-h",
103
194
  harness: "claude-code",
104
195
  harnessExplicit: false,
105
196
  all: false,
@@ -111,7 +202,8 @@ function parseArgs(argv) {
111
202
  dryRun: false,
112
203
  yes: false,
113
204
  noContent: false,
114
- analyzers: []
205
+ analyzers: [],
206
+ attrs: []
115
207
  };
116
208
  for (let i = 1; i < argv.length; i++) {
117
209
  const arg = argv[i];
@@ -151,6 +243,14 @@ function parseArgs(argv) {
151
243
  case "--model":
152
244
  a.model = next();
153
245
  break;
246
+ case "--metadata":
247
+ a.metadata = next();
248
+ break;
249
+ case "--attr": {
250
+ const v = next();
251
+ if (v) a.attrs.push(v);
252
+ break;
253
+ }
154
254
  case "--interval":
155
255
  a.interval = Number(next());
156
256
  break;
@@ -177,12 +277,21 @@ function parseArgs(argv) {
177
277
  case "--redactor":
178
278
  a.redactorCmd = next();
179
279
  break;
280
+ case "--format":
281
+ a.format = next();
282
+ break;
283
+ case "--help":
284
+ case "-h":
285
+ a.help = true;
286
+ break;
180
287
  case "--yes":
181
288
  case "-y":
182
289
  a.yes = true;
183
290
  break;
184
291
  default:
185
292
  if (arg?.startsWith("--")) throw new Error(`unknown flag: ${arg}`);
293
+ if (arg && !a.input) a.input = arg;
294
+ else if (arg) throw new Error(`unexpected positional argument: ${arg}`);
186
295
  }
187
296
  }
188
297
  return a;
@@ -243,23 +352,27 @@ async function collectSpans(args) {
243
352
  harness: adapter.harness,
244
353
  sessionId: args.session,
245
354
  path: args.session,
246
- cwd: null,
355
+ // --session is an explicit file; honor --cwd so adoption can find the
356
+ // project's .evolve/skill-runs.jsonl (locate() infers cwd in the scan path).
357
+ cwd: args.cwd ?? null,
247
358
  mtimeMs: st.mtimeMs
248
359
  };
249
- return { spans: await adapter.parse(ref), harness: adapter.harness, sessionCount: 1 };
360
+ return { spans: await parseSession(adapter, ref), harness: adapter.harness, sessionCount: 1, cwds: ref.cwd ? [ref.cwd] : [] };
250
361
  }
251
362
  const groups = await discover({ ...args, last: args.last || 1 });
252
363
  const spans = [];
253
364
  let sessionCount = 0;
254
365
  const harnesses = [];
366
+ const cwds = [];
255
367
  for (const { adapter, refs } of groups) {
256
368
  if (refs.length > 0) harnesses.push(adapter.harness);
257
369
  for (const ref of refs) {
258
- spans.push(...await adapter.parse(ref));
370
+ spans.push(...await parseSession(adapter, ref));
371
+ if (ref.cwd) cwds.push(ref.cwd);
259
372
  sessionCount += 1;
260
373
  }
261
374
  }
262
- return { spans, harness: harnesses.join("+") || args.harness, sessionCount };
375
+ return { spans, harness: harnesses.join("+") || args.harness, sessionCount, cwds };
263
376
  }
264
377
  async function cmdConvert(args) {
265
378
  const { spans } = await collectSpans(args);
@@ -267,8 +380,92 @@ async function cmdConvert(args) {
267
380
  const path = await writeOtlpFile(spans, args.otlp);
268
381
  console.log(`wrote ${spans.length} spans \u2192 ${path}`);
269
382
  }
383
+ async function collectSessionRows(args) {
384
+ if (args.session) {
385
+ const adapter = resolveAdapter(args.harness);
386
+ if (!adapter) throw new Error(`unknown harness "${args.harness}"`);
387
+ const st = await stat(args.session);
388
+ const ref = {
389
+ harness: adapter.harness,
390
+ sessionId: args.session,
391
+ path: args.session,
392
+ cwd: args.cwd ?? null,
393
+ mtimeMs: st.mtimeMs
394
+ };
395
+ return [{ ref, spans: await parseSession(adapter, ref) }];
396
+ }
397
+ const groups = await discover({ ...args, last: args.last || 20 });
398
+ const rows = [];
399
+ for (const { adapter, refs } of groups) {
400
+ for (const ref of refs) rows.push({ ref, spans: await parseSession(adapter, ref) });
401
+ }
402
+ return rows;
403
+ }
404
+ async function cmdEvidence(args) {
405
+ const rows = (await collectSessionRows(args)).filter((row) => row.spans.length > 0);
406
+ if (rows.length === 0) throw new Error("no spans found for the given selection");
407
+ const otlpPath = args.otlp ? await writeOtlpFile(rows.flatMap((row) => row.spans), args.otlp) : void 0;
408
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
409
+ const records = await Promise.all(rows.map(
410
+ (row) => buildPolicyEvidenceRecord(row.ref, row.spans, {
411
+ generatedAt,
412
+ minLoopOccurrences: args.minLoop,
413
+ maxLoopExamples: 25,
414
+ otlpPath
415
+ })
416
+ ));
417
+ if (args.out) {
418
+ const path = await writePolicyEvidenceFile(records, args.out);
419
+ console.log(`policy evidence \u2192 ${path} (${records.length} session rows${otlpPath ? `, OTLP: ${otlpPath}` : ""})`);
420
+ } else {
421
+ process.stdout.write(serializePolicyEvidence(records));
422
+ }
423
+ }
424
+ async function cmdExport(args) {
425
+ if (!args.input) throw new Error("export needs an input file; run `traces export --help` for examples");
426
+ const format = args.format ?? "auto";
427
+ if (format !== "auto" && format !== "policy-evidence" && format !== "sandbox-events" && format !== "openinference") {
428
+ throw new Error("--format must be auto, policy-evidence, sandbox-events, or openinference");
429
+ }
430
+ const attributes = await loadExportAttributes(args);
431
+ const outPath = args.out ?? args.otlp;
432
+ if (outPath) {
433
+ const result2 = await writeTraceEvidenceExportFile(args.input, outPath, { format, attributes });
434
+ console.log(
435
+ `exported ${result2.spans.length} OpenInference span(s) from ${result2.format} \u2192 ${result2.path} (${result2.redactionCount} redaction${result2.redactionCount === 1 ? "" : "s"})`
436
+ );
437
+ return;
438
+ }
439
+ const result = await exportTraceEvidenceFile(args.input, { format, attributes });
440
+ process.stdout.write(serializeSpans(result.spans));
441
+ }
442
+ function parseAttributeValue(raw) {
443
+ if (raw === "true") return true;
444
+ if (raw === "false") return false;
445
+ if (/^-?\d+(?:\.\d+)?$/.test(raw)) {
446
+ const number = Number(raw);
447
+ if (Number.isFinite(number)) return number;
448
+ }
449
+ return raw;
450
+ }
451
+ async function loadExportAttributes(args) {
452
+ const attributes = {};
453
+ if (args.metadata) {
454
+ const parsed = JSON.parse(await readFile(args.metadata, "utf8"));
455
+ if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
456
+ throw new Error("--metadata must point to a JSON object");
457
+ }
458
+ Object.assign(attributes, parsed);
459
+ }
460
+ for (const attr of args.attrs) {
461
+ const eq = attr.indexOf("=");
462
+ if (eq <= 0) throw new Error(`--attr must be key=value, got ${attr}`);
463
+ attributes[attr.slice(0, eq)] = parseAttributeValue(attr.slice(eq + 1));
464
+ }
465
+ return attributes;
466
+ }
270
467
  async function cmdAnalyze(args) {
271
- const { spans, harness, sessionCount } = await collectSpans(args);
468
+ const { spans, harness, sessionCount, cwds } = await collectSpans(args);
272
469
  if (spans.length === 0) throw new Error("no spans found for the given selection");
273
470
  const ai = args.llm ? await buildAxService() : void 0;
274
471
  const { otlpPath, result } = await analyzeSpans(spans, {
@@ -280,8 +477,12 @@ async function cmdAnalyze(args) {
280
477
  `)
281
478
  });
282
479
  const pipelines = await runPipelines(spans, { minLoopOccurrences: args.minLoop });
480
+ const reactions = analyzeReactions(spans);
481
+ const adoption = await analyzeAdoption(spans, { cwds });
283
482
  let report = `${renderReport(result, { harness, sessionCount, spanCount: spans.length, otlpPath })}
284
- ${renderPipelines(pipelines)}`;
483
+ ${renderPipelines(pipelines)}
484
+ ${renderReactions(reactions)}
485
+ ${renderAdoption(adoption)}`;
285
486
  if (args.analyzers.length > 0 && otlpPath) {
286
487
  const engines = args.analyzers.map(
287
488
  (spec) => spec === "halo" ? haloAnalyzer({ defaultPrompt: args.analyzerPrompt, model: args.model }) : commandAnalyzer({ name: spec, command: spec, args: (p, prompt) => prompt ? [p, prompt] : [p] })
@@ -382,6 +583,32 @@ Window: since ${new Date(sinceMs).toISOString()}
382
583
  `Uploaded ${res.uploadedSessions} session(s), ${res.acceptedSpans} spans accepted, ${res.redactionCount} redaction(s); ${res.skippedSessions} already-uploaded skipped.`
383
584
  );
384
585
  }
586
+ function usageExport() {
587
+ console.log(`traces export \u2014 convert trace evidence files to OpenInference JSONL
588
+
589
+ Usage:
590
+ traces export <input.jsonl|input.json> --out <spans.jsonl> [--format auto]
591
+ traces export <input.json> --attr task.id=abc --attr arm=baseline --out spans.jsonl
592
+ traces export <input.json> --metadata run-metadata.json --out spans.jsonl
593
+ traces export <input.jsonl|input.json> > spans.jsonl
594
+
595
+ Input formats:
596
+ policy-evidence Compact JSONL rows from \`traces evidence --out policy-evidence.jsonl\`
597
+ sandbox-events Sandbox/OpenCode event arrays with start/raw/result/done/error events
598
+ openinference Existing OpenInference JSONL; rewrites through traces redaction
599
+ auto Detect the format from the file contents (default)
600
+
601
+ Examples:
602
+ traces evidence --all --since 24h --out policy-evidence.jsonl
603
+ traces export policy-evidence.jsonl --out spans.openinference.jsonl
604
+ traces export sandbox-events.json --format sandbox-events --out spans.openinference.jsonl
605
+ traces export sandbox-events.json --attr task.id=aec-001 --attr outcome.score=1 --out spans.openinference.jsonl
606
+ halo spans.openinference.jsonl --prompt "Analyze this trace slice" --max-turns 1
607
+
608
+ Safety:
609
+ export runs the same local regex redaction used by upload before writing spans.
610
+ --metadata must be a JSON object; --attr key=value is repeatable and overrides matching metadata keys.`);
611
+ }
385
612
  function usage() {
386
613
  console.log(`traces \u2014 analyze, observe & upload coding-agent sessions
387
614
 
@@ -389,6 +616,8 @@ Commands:
389
616
  list List discovered sessions
390
617
  analyze Run analyst suite + loop/waste pipelines, write a markdown report
391
618
  convert Emit OTLP-JSONL only (HALO: use analyze --analyzer halo)
619
+ export Convert evidence/events files to OpenInference JSONL for HALO
620
+ evidence Emit compact session-evidence JSONL for downstream policy miners
392
621
  watch Online observer: tail active sessions, notify on stuck loops (read-only)
393
622
  upload Redact + upload sessions in a time window to the Tangle Intelligence Platform
394
623
 
@@ -400,7 +629,10 @@ Options:
400
629
  --cwd <dir> Filter sessions by working directory
401
630
  --since <t> upload: window \u2014 30m / 2h / 7d or an ISO date (default 24h); analyze: ISO cutoff
402
631
  --out <path> Write report to a file
403
- --otlp <path> OTLP artifact path (also the dry-run upload preview path)
632
+ --otlp <path> OTLP artifact path (also evidence provenance / dry-run upload preview)
633
+ --format <kind> export: auto | policy-evidence | sandbox-events | openinference
634
+ --metadata <json> export: attach JSON object fields as span attributes
635
+ --attr <k=v> export: attach one span attribute (repeatable)
404
636
  --llm Enable agentic RLM analysts (needs OPENAI_API_KEY / OPENAI_BASE_URL)
405
637
  --model <id> --llm model id (e.g. a router model like glm-5.2); default is agent-eval's
406
638
  --budget <usd> USD cap for agentic analysts
@@ -413,12 +645,22 @@ Options:
413
645
  --no-content upload: strip prompt/response text \u2014 send metadata only
414
646
  --redactor <cmd> upload: external PII scrubber (JSON array stdin\u2192stdout) after the regex pass
415
647
  --yes, -y upload: skip the confirmation prompt
648
+ --help, -h Show help (use \`traces export --help\` for export examples)
416
649
 
417
650
  Upload env: TANGLE_INGEST_URL (or TANGLE_ORCHESTRATOR_URL), TANGLE_INGEST_API_KEY (or TANGLE_API_KEY), TANGLE_TENANT_ID`);
418
651
  }
419
652
  async function main() {
420
653
  const args = parseArgs(process.argv.slice(2));
654
+ if (args.help) {
655
+ if (args.command === "export" || args.command === "help" && args.input === "export") usageExport();
656
+ else usage();
657
+ return;
658
+ }
421
659
  switch (args.command) {
660
+ case "help":
661
+ if (args.input === "export") usageExport();
662
+ else usage();
663
+ break;
422
664
  case "list":
423
665
  await cmdList(args);
424
666
  break;
@@ -428,6 +670,12 @@ async function main() {
428
670
  case "convert":
429
671
  await cmdConvert(args);
430
672
  break;
673
+ case "export":
674
+ await cmdExport(args);
675
+ break;
676
+ case "evidence":
677
+ await cmdEvidence(args);
678
+ break;
431
679
  case "watch":
432
680
  await cmdWatch(args);
433
681
  break;