@tangle-network/traces 0.8.0 → 0.8.11

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