@tangle-network/traces 0.8.17 → 0.8.18

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/README.md CHANGED
@@ -17,6 +17,7 @@ It reads the transcripts your harness leaves on disk, reconstructs the run as sp
17
17
  - [What it finds](#what-it-finds)
18
18
  - [Supported harnesses](#supported-harnesses)
19
19
  - [CLI reference](#cli-reference)
20
+ - [Live stream](#live-stream)
20
21
  - [Improvement engine](#improvement-engine)
21
22
  - [Session index](#session-index)
22
23
  - [Policy-mining evidence](#policy-mining-evidence)
@@ -44,6 +45,8 @@ Requires Node ≥ 22.
44
45
  ```bash
45
46
  traces analyze --harness claude-code --last 1
46
47
  traces improve --harness claude-code --last 5 --dir .traces/improvement
48
+ traces watch --all
49
+ traces stream --all --no-spans
47
50
  ```
48
51
 
49
52
  That's the command in the demo above. The **deterministic pass** — stuck loops, token growth, output decay, missing self-verification, tool monoculture — needs no API key and costs nothing.
@@ -100,7 +103,9 @@ traces index --all --since 24h --out session-index.json
100
103
  traces inspect session-index.json --out inspection-report.md
101
104
  traces evidence --harness codex --last 20 --out policy-evidence.jsonl
102
105
  traces export policy-evidence.jsonl --out spans.openinference.jsonl
103
- traces watch --all # live observer; notify on stuck loops
106
+ traces watch --all # live observer; loops + semantic findings
107
+ traces stream --all --no-spans # live JSONL events for dashboards/visualizers
108
+ traces stream spans.openinference.jsonl --format openinference --no-spans
104
109
  traces upload --since 1h --dry-run # redact + dedup + preview, no network
105
110
  traces upload --since 24h # upload last day to the Intelligence Platform
106
111
  ```
@@ -116,14 +121,34 @@ traces upload --since 24h # upload last day to the Inte
116
121
  | `--out <path>` | Write the report to a file |
117
122
  | `--dir <path>` | `improve`: write the full artifact pack to this directory |
118
123
  | `--otlp <path>` | OTLP artifact path (also evidence provenance / dry-run upload preview) |
119
- | `--format <kind>` | `export`: `auto`, `policy-evidence`, `sandbox-events`, or `openinference` |
124
+ | `--format <kind>` | `export` / file `stream`: `auto`, `policy-evidence`, `sandbox-events`, or `openinference` |
120
125
  | `--llm` / `--budget <usd>` | Enable agentic analysts (needs `OPENAI_API_KEY`) / cap their spend |
121
126
  | `--config <path>` | `investigate` / `improve`: load BYO analysts, external analyzers, and proposal adapters |
122
- | `--interval <s>` / `--window <m>` | `watch`: poll seconds (default 5) / active-session window minutes (default 30) |
127
+ | `--interval <s>` / `--window <m>` | `watch` / live `stream`: poll seconds (default 5) / active-session window minutes (default 30) |
123
128
  | `--min-loop <n>` | Identical repeated calls before flagging a loop (default 3) |
129
+ | `--replay` / `--once` | `stream`: scan once, then exit |
130
+ | `--no-spans` / `--no-findings` | `stream`: suppress raw span rows / finding rows |
124
131
  | `--no-content` | `upload`: send metadata only — strip all prompt/response text |
125
132
  | `--dry-run` / `--yes` | `upload`: preview without sending / skip the confirm prompt |
126
133
 
134
+ ## Live stream
135
+
136
+ `traces watch` is the human terminal view.
137
+ It prints repeated-tool loops and semantic findings while a coding agent is still running.
138
+
139
+ `traces stream` is the machine feed.
140
+ It emits newline-delimited JSON events that a dashboard, art visualizer, local watcher, or hosted product can consume without scraping terminal prose.
141
+
142
+ ```bash
143
+ traces stream --all
144
+ traces stream --all --no-spans
145
+ traces stream spans.openinference.jsonl --format openinference --no-spans
146
+ ```
147
+
148
+ The stream emits `session`, `span`, `analysis_batch`, `finding`, and `tick` events.
149
+ The semantic findings currently cover repeated failing commands, verification churn without code/config changes, completion claims without later verification, and high tool-error rates.
150
+ Use `--no-spans` when you want the low-volume meaning layer; keep spans on for real-time visualizers that need motion, timing, and tool-call texture.
151
+
127
152
  ## Improvement engine
128
153
 
129
154
  `traces improve` turns observed sessions into a portable improvement packet:
@@ -289,6 +314,9 @@ The CLI is a thin consumer of these exports.
289
314
  | `runTraceStoreInvestigation` | `({ traceStore }) → TraceStoreInvestigationResult` | run the same packet layer over a hosted/custom `TraceAnalysisStore` |
290
315
  | `loadTracesConfig` | `(path?) → TracesConfig \| undefined` | load BYO analysts, external analyzers, and proposal adapters |
291
316
  | `watchSessions` | `(ObserverOptions) → Promise<void>` | live observer; `onLoop` / `onReport` / `signal` / `adapters` |
317
+ | `streamSessions` | `(TraceStreamOptions) → Promise<void>` | live JSONL-ready event stream over active sessions |
318
+ | `traceStreamEventsFromSpans` | `(spans, opts?) → TraceStreamEvent[]` | replay an existing span list as stream events |
319
+ | `analyzeLiveBatch` | `(spans, opts?) → TraceLiveBatch` | compute semantic online findings for one batch |
292
320
  | `collectSessionIndex` | `(ScanOptions) → TraceSessionIndex` | scan sessions and return a reusable JSON-ready catalog |
293
321
  | `inspectSessionIndex` | `(TraceSessionIndex) → TraceInspectionReport` | rank improvement findings from an index without rescanning sessions |
294
322
  | `buildPolicyEvidenceRecord` | `(ref, spans, opts?) → PolicyEvidenceRecord` | summarize one session for downstream policy mining |
@@ -303,12 +331,15 @@ The CLI is a thin consumer of these exports.
303
331
  | `ExternalAnalyzer` / `Redactor` | `haloAnalyzer` / `commandAnalyzer` / `commandRedactor` | drive engines/models you install (not bundled) |
304
332
 
305
333
  ```ts
306
- import { watchSessions, analyzeSpans, AnalystRegistry, makeFinding } from '@tangle-network/traces'
334
+ import { watchSessions, streamSessions, analyzeSpans, AnalystRegistry, makeFinding } from '@tangle-network/traces'
307
335
 
308
336
  // Observe live sessions, feed findings anywhere (read-only, cancellable):
309
337
  const c = new AbortController()
310
338
  await watchSessions({ all: true, signal: c.signal, onLoop: (l) => alert(l.toolName, l.occurrences) })
311
339
 
340
+ // Feed a visualizer or dashboard:
341
+ await streamSessions({ all: true, signal: c.signal, includeSpans: false, onEvent: (event) => console.log(event) })
342
+
312
343
  // Run your own analyst instead of the built-ins:
313
344
  const registry = new AnalystRegistry()
314
345
  registry.register({
@@ -751,10 +751,10 @@ var ClaudeAdapter = class {
751
751
  let step = main.nextStep;
752
752
  for (const f of files) {
753
753
  if (!f.endsWith(".jsonl")) continue;
754
- const hash = basename2(f, ".jsonl");
754
+ const hash2 = basename2(f, ".jsonl");
755
755
  let meta = {};
756
756
  try {
757
- meta = JSON.parse(await readFile3(join3(subDir, `${hash}.meta.json`), "utf8"));
757
+ meta = JSON.parse(await readFile3(join3(subDir, `${hash2}.meta.json`), "utf8"));
758
758
  } catch {
759
759
  }
760
760
  const parent = meta.toolUseId && main.toolSpanByUseId.get(meta.toolUseId)?.span_id || `root:${traceId}`;
@@ -768,7 +768,7 @@ var ClaudeAdapter = class {
768
768
  traceId,
769
769
  agent: meta.agentType ? `subagent:${meta.agentType}` : "subagent",
770
770
  startStep: step,
771
- idPrefix: `${hash}:`,
771
+ idPrefix: `${hash2}:`,
772
772
  rootParent: parent
773
773
  });
774
774
  out.push(...parsed.spans);
@@ -2286,13 +2286,13 @@ function extractAbsolutePaths(spans) {
2286
2286
  let scannedChars = 0;
2287
2287
  for (const span2 of spans) {
2288
2288
  for (const value of [span2.name, span2.status.message, ...Object.values(span2.attributes)]) {
2289
- for (const evidence2 of pathsFromUnknown(value)) {
2290
- scannedChars += evidence2.path.length;
2289
+ for (const evidence3 of pathsFromUnknown(value)) {
2290
+ scannedChars += evidence3.path.length;
2291
2291
  if (scannedChars > MAX_SPAN_TEXT_CHARS) return paths;
2292
- const key = `${evidence2.source}:${evidence2.path}`;
2292
+ const key = `${evidence3.source}:${evidence3.path}`;
2293
2293
  if (seen.has(key)) continue;
2294
2294
  seen.add(key);
2295
- paths.push(evidence2);
2295
+ paths.push(evidence3);
2296
2296
  if (paths.length >= MAX_SPAN_PATH_CANDIDATES) return paths;
2297
2297
  }
2298
2298
  }
@@ -2346,8 +2346,8 @@ async function resolveSessionRepoAttrs(cwd, spans) {
2346
2346
  add(await repairDashedPath(cwd), "repaired-cwd");
2347
2347
  add(cwd, "ref-cwd");
2348
2348
  }
2349
- for (const evidence2 of extractAbsolutePaths(spans)) {
2350
- add(await repoCandidateForPath(evidence2.path), evidence2.source, evidence2.weight);
2349
+ for (const evidence3 of extractAbsolutePaths(spans)) {
2350
+ add(await repoCandidateForPath(evidence3.path), evidence3.source, evidence3.weight);
2351
2351
  }
2352
2352
  const resolved = [];
2353
2353
  for (const candidate of candidates.values()) {
@@ -2492,9 +2492,9 @@ async function buildPolicyEvidenceRecord(ref, spans, opts = {}) {
2492
2492
  signals: {
2493
2493
  stuckLoopCount: loopFindings.length,
2494
2494
  affectedRunRatio: pipelines.stuckLoops.affectedRunRatio,
2495
- stuckLoops: loopFindings.slice(0, loopLimit).map((finding) => ({
2496
- toolName: finding.toolName,
2497
- occurrences: finding.occurrences
2495
+ stuckLoops: loopFindings.slice(0, loopLimit).map((finding2) => ({
2496
+ toolName: finding2.toolName,
2497
+ occurrences: finding2.occurrences
2498
2498
  })),
2499
2499
  stuckLoopsOmitted: Math.max(0, loopFindings.length - loopLimit),
2500
2500
  toolErrorRate: toolSpans.length === 0 ? 0 : erroredToolCallCount / toolSpans.length
@@ -3235,12 +3235,12 @@ function topLoopTools(row) {
3235
3235
  current.maxOccurrences = Math.max(current.maxOccurrences, loop.occurrences);
3236
3236
  byTool.set(loop.toolName, current);
3237
3237
  }
3238
- const loops = [...byTool.entries()].sort(([, a], [, b]) => b.groups - a.groups || b.maxOccurrences - a.maxOccurrences).slice(0, 3).map(([toolName2, stats]) => `${toolName2} ${stats.groups} group(s), max x${stats.maxOccurrences}`);
3238
+ const loops = [...byTool.entries()].sort(([, a], [, b]) => b.groups - a.groups || b.maxOccurrences - a.maxOccurrences).slice(0, 3).map(([toolName3, stats]) => `${toolName3} ${stats.groups} group(s), max x${stats.maxOccurrences}`);
3239
3239
  const suffix = row.signals.stuckLoopsOmitted > 0 ? `, +${row.signals.stuckLoopsOmitted} omitted` : "";
3240
3240
  return loops.length > 0 ? `${loops.join(", ")}${suffix}` : `${row.signals.stuckLoopCount} loop(s)`;
3241
3241
  }
3242
3242
  function sortFindings(findings) {
3243
- return findings.sort((a, b) => severityRank[a.severity] - severityRank[b.severity] || b.impact - a.impact || a.id.localeCompare(b.id)).map((finding) => ({ ...finding }));
3243
+ return findings.sort((a, b) => severityRank[a.severity] - severityRank[b.severity] || b.impact - a.impact || a.id.localeCompare(b.id)).map((finding2) => ({ ...finding2 }));
3244
3244
  }
3245
3245
  function contextSourceCounts(index) {
3246
3246
  return { contextFiles: allContextFiles(index).length };
@@ -3435,9 +3435,9 @@ function inspectSessionIndex(index, opts = {}) {
3435
3435
  },
3436
3436
  totals: {
3437
3437
  findings: ranked.length,
3438
- high: ranked.filter((finding) => finding.severity === "high").length,
3439
- medium: ranked.filter((finding) => finding.severity === "medium").length,
3440
- low: ranked.filter((finding) => finding.severity === "low").length
3438
+ high: ranked.filter((finding2) => finding2.severity === "high").length,
3439
+ medium: ranked.filter((finding2) => finding2.severity === "medium").length,
3440
+ low: ranked.filter((finding2) => finding2.severity === "low").length
3441
3441
  },
3442
3442
  findings: ranked
3443
3443
  };
@@ -3456,16 +3456,16 @@ function renderInspectionReport(report) {
3456
3456
  return `${lines.join("\n")}
3457
3457
  `;
3458
3458
  }
3459
- report.findings.forEach((finding, i) => {
3459
+ report.findings.forEach((finding2, i) => {
3460
3460
  lines.push("");
3461
- lines.push(`## ${i + 1}. [${finding.severity}] ${finding.title}`);
3462
- lines.push(`Area: ${finding.area} Impact: ${finding.impact}`);
3461
+ lines.push(`## ${i + 1}. [${finding2.severity}] ${finding2.title}`);
3462
+ lines.push(`Area: ${finding2.area} Impact: ${finding2.impact}`);
3463
3463
  lines.push("Evidence:");
3464
- for (const item of finding.evidence) lines.push(`- ${item}`);
3465
- lines.push(`Next: ${finding.next}`);
3466
- if (finding.refs && finding.refs.length > 0) {
3464
+ for (const item of finding2.evidence) lines.push(`- ${item}`);
3465
+ lines.push(`Next: ${finding2.next}`);
3466
+ if (finding2.refs && finding2.refs.length > 0) {
3467
3467
  lines.push("Refs:");
3468
- for (const ref of finding.refs.slice(0, 5)) lines.push(`- ${ref}`);
3468
+ for (const ref of finding2.refs.slice(0, 5)) lines.push(`- ${ref}`);
3469
3469
  }
3470
3470
  });
3471
3471
  return `${lines.join("\n")}
@@ -3926,55 +3926,55 @@ function normalizeExternalFindings(results) {
3926
3926
  for (const result of results) {
3927
3927
  if (result.findings) out.push(...result.findings);
3928
3928
  externalRows(result.output).forEach((row, index) => {
3929
- const finding = coerceExternalFinding(result.analyzer, row, index);
3930
- if (finding) out.push(finding);
3929
+ const finding2 = coerceExternalFinding(result.analyzer, row, index);
3930
+ if (finding2) out.push(finding2);
3931
3931
  });
3932
3932
  }
3933
3933
  return out;
3934
3934
  }
3935
- function sourceOf(finding) {
3936
- if (finding.analyst_id.startsWith("external:")) return "external";
3937
- if (finding.analyst_id === "traces-deterministic") return "deterministic";
3935
+ function sourceOf(finding2) {
3936
+ if (finding2.analyst_id.startsWith("external:")) return "external";
3937
+ if (finding2.analyst_id === "traces-deterministic") return "deterministic";
3938
3938
  return "analyst";
3939
3939
  }
3940
- function recommendationTitle(finding) {
3941
- if (finding.recommended_action) return finding.recommended_action.replace(/\.$/, "");
3942
- return `Investigate ${finding.area}: ${finding.claim}`.replace(/\.$/, "");
3940
+ function recommendationTitle(finding2) {
3941
+ if (finding2.recommended_action) return finding2.recommended_action.replace(/\.$/, "");
3942
+ return `Investigate ${finding2.area}: ${finding2.claim}`.replace(/\.$/, "");
3943
3943
  }
3944
3944
  function buildRecommendations(findings) {
3945
- return [...findings].filter((finding) => finding.severity !== "info").sort((a, b) => severityRank2[a.severity] - severityRank2[b.severity] || b.confidence - a.confidence || a.finding_id.localeCompare(b.finding_id)).slice(0, 20).map((finding, index) => ({
3945
+ return [...findings].filter((finding2) => finding2.severity !== "info").sort((a, b) => severityRank2[a.severity] - severityRank2[b.severity] || b.confidence - a.confidence || a.finding_id.localeCompare(b.finding_id)).slice(0, 20).map((finding2, index) => ({
3946
3946
  schemaVersion: 1,
3947
- id: `rec-${index + 1}-${slug(finding.area)}`,
3948
- severity: finding.severity,
3949
- title: recommendationTitle(finding),
3950
- action: finding.recommended_action ?? `Review and fix: ${finding.claim}`,
3951
- rationale: finding.rationale ?? finding.claim,
3952
- evidenceRefs: finding.evidence_refs,
3953
- findingIds: [finding.finding_id],
3954
- validationPlan: finding.validation_plan ?? "Rerun `traces improve` on a fresh comparable session window and confirm this finding disappears or drops in severity.",
3955
- source: sourceOf(finding)
3947
+ id: `rec-${index + 1}-${slug(finding2.area)}`,
3948
+ severity: finding2.severity,
3949
+ title: recommendationTitle(finding2),
3950
+ action: finding2.recommended_action ?? `Review and fix: ${finding2.claim}`,
3951
+ rationale: finding2.rationale ?? finding2.claim,
3952
+ evidenceRefs: finding2.evidence_refs,
3953
+ findingIds: [finding2.finding_id],
3954
+ validationPlan: finding2.validation_plan ?? "Rerun `traces improve` on a fresh comparable session window and confirm this finding disappears or drops in severity.",
3955
+ source: sourceOf(finding2)
3956
3956
  }));
3957
3957
  }
3958
3958
  function buildClaims(findings) {
3959
- return findings.map((finding, index) => ({
3959
+ return findings.map((finding2, index) => ({
3960
3960
  schemaVersion: 1,
3961
- id: `claim-${index + 1}-${slug(finding.area)}`,
3962
- severity: finding.severity,
3963
- text: finding.claim,
3964
- area: finding.area,
3965
- sourceFindingId: finding.finding_id,
3966
- evidenceRefs: finding.evidence_refs,
3967
- validationPlan: finding.validation_plan
3961
+ id: `claim-${index + 1}-${slug(finding2.area)}`,
3962
+ severity: finding2.severity,
3963
+ text: finding2.claim,
3964
+ area: finding2.area,
3965
+ sourceFindingId: finding2.finding_id,
3966
+ evidenceRefs: finding2.evidence_refs,
3967
+ validationPlan: finding2.validation_plan
3968
3968
  }));
3969
3969
  }
3970
3970
  function buildEvidenceRows(findings) {
3971
- return findings.flatMap((finding) => finding.evidence_refs.map((ref) => ({
3971
+ return findings.flatMap((finding2) => finding2.evidence_refs.map((ref) => ({
3972
3972
  schemaVersion: 1,
3973
3973
  kind: "traces.improvement_evidence",
3974
- findingId: finding.finding_id,
3975
- severity: finding.severity,
3976
- area: finding.area,
3977
- claim: finding.claim,
3974
+ findingId: finding2.finding_id,
3975
+ severity: finding2.severity,
3976
+ area: finding2.area,
3977
+ claim: finding2.claim,
3978
3978
  evidence: ref
3979
3979
  })));
3980
3980
  }
@@ -4006,14 +4006,14 @@ function renderFindingPacket(packet, title = "Trace finding packet") {
4006
4006
  if (packet.findings.length > 0) {
4007
4007
  lines.push("## findings");
4008
4008
  lines.push("");
4009
- for (const finding of [...packet.findings].sort((a, b) => severityRank2[a.severity] - severityRank2[b.severity] || a.area.localeCompare(b.area) || a.claim.localeCompare(b.claim))) {
4010
- lines.push(`### ${finding.severity.toUpperCase()} \u2014 ${finding.claim}`);
4009
+ for (const finding2 of [...packet.findings].sort((a, b) => severityRank2[a.severity] - severityRank2[b.severity] || a.area.localeCompare(b.area) || a.claim.localeCompare(b.claim))) {
4010
+ lines.push(`### ${finding2.severity.toUpperCase()} \u2014 ${finding2.claim}`);
4011
4011
  lines.push("");
4012
- lines.push(`- **Area:** ${finding.area}`);
4013
- lines.push(`- **Analyst:** ${finding.analyst_id}`);
4014
- if (finding.recommended_action) lines.push(`- **Action:** ${finding.recommended_action}`);
4015
- if (finding.validation_plan) lines.push(`- **Check:** ${finding.validation_plan}`);
4016
- for (const ref of finding.evidence_refs.slice(0, 3)) {
4012
+ lines.push(`- **Area:** ${finding2.area}`);
4013
+ lines.push(`- **Analyst:** ${finding2.analyst_id}`);
4014
+ if (finding2.recommended_action) lines.push(`- **Action:** ${finding2.recommended_action}`);
4015
+ if (finding2.validation_plan) lines.push(`- **Check:** ${finding2.validation_plan}`);
4016
+ for (const ref of finding2.evidence_refs.slice(0, 3)) {
4017
4017
  lines.push(`- **Evidence:** ${ref.kind} ${ref.uri}${ref.excerpt ? ` \u2014 \`${ref.excerpt.slice(0, 180)}\`` : ""}`);
4018
4018
  }
4019
4019
  lines.push("");
@@ -4242,8 +4242,330 @@ async function saveReport(path, report) {
4242
4242
  await writeFile4(path, report, "utf8");
4243
4243
  }
4244
4244
 
4245
- // src/observer.ts
4245
+ // src/live.ts
4246
+ import { createHash as createHash2 } from "crypto";
4247
+ var VERIFY_RE = /\b(test|tests|vitest|jest|pytest|go test|cargo test|tsc|typecheck|lint|biome|eslint|build|check:invariants|preflight)\b/i;
4248
+ var CHANGE_RE = /\b(apply_patch|edit|write|create|delete|rename|replace|update|git apply|sed -i|perl -pi|tee\s|cat\s+>)/i;
4249
+ var CLAIM_DONE_RE = /\b(done|fixed|complete|completed|implemented|merged|verified|all green|tests pass|ready|works now)\b/i;
4246
4250
  var sleep = (ms, signal) => new Promise((resolve2) => {
4251
+ if (signal?.aborted) {
4252
+ resolve2();
4253
+ return;
4254
+ }
4255
+ const onAbort = () => {
4256
+ clearTimeout(t);
4257
+ resolve2();
4258
+ };
4259
+ const t = setTimeout(() => {
4260
+ signal?.removeEventListener("abort", onAbort);
4261
+ resolve2();
4262
+ }, ms);
4263
+ signal?.addEventListener("abort", onAbort, { once: true });
4264
+ });
4265
+ function hash(parts) {
4266
+ return createHash2("sha256").update(parts.join("\0")).digest("hex").slice(0, 16);
4267
+ }
4268
+ function asString(value) {
4269
+ if (typeof value === "string" && value.trim()) return value;
4270
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
4271
+ return void 0;
4272
+ }
4273
+ function spanKind2(span2) {
4274
+ return asString(span2.attributes["openinference.span.kind"])?.toUpperCase() ?? "SPAN";
4275
+ }
4276
+ function spanContent(span2) {
4277
+ return asString(span2.attributes.content) ?? asString(span2.attributes["input.value"]) ?? asString(span2.attributes["llm.input"]) ?? asString(span2.attributes["gen_ai.prompt"]) ?? "";
4278
+ }
4279
+ function toolName2(span2) {
4280
+ return asString(span2.attributes["tool.name"]) ?? span2.name.replace(/^tool\./, "");
4281
+ }
4282
+ function toolSignature(span2) {
4283
+ const content = spanContent(span2).replace(/\s+/g, " ").trim();
4284
+ return `${toolName2(span2)}:${content || span2.name}`;
4285
+ }
4286
+ function isTool(span2) {
4287
+ return spanKind2(span2) === "TOOL" || span2.attributes["tool.name"] != null || span2.name.startsWith("tool.");
4288
+ }
4289
+ function isTextSpan(span2) {
4290
+ return !isTool(span2) && spanContent(span2).length > 0;
4291
+ }
4292
+ function isVerification(span2) {
4293
+ if (!isTool(span2)) return false;
4294
+ return VERIFY_RE.test(`${toolName2(span2)} ${span2.name} ${spanContent(span2)}`);
4295
+ }
4296
+ function isChange(span2) {
4297
+ if (!isTool(span2)) return false;
4298
+ return CHANGE_RE.test(`${toolName2(span2)} ${span2.name} ${spanContent(span2)}`);
4299
+ }
4300
+ function timeOf(span2) {
4301
+ const parsed = Date.parse(span2.start_time);
4302
+ return Number.isFinite(parsed) ? parsed : 0;
4303
+ }
4304
+ function orderSpans(spans) {
4305
+ return [...spans].sort((a, b) => {
4306
+ const stepA = typeof a.attributes.step === "number" ? a.attributes.step : void 0;
4307
+ const stepB = typeof b.attributes.step === "number" ? b.attributes.step : void 0;
4308
+ if (stepA != null && stepB != null && stepA !== stepB) return stepA - stepB;
4309
+ return timeOf(a) - timeOf(b);
4310
+ });
4311
+ }
4312
+ function sessionFrom(spans, opts) {
4313
+ const first = spans[0];
4314
+ return {
4315
+ harness: opts.session?.harness ?? asString(first?.attributes["service.name"]) ?? "unknown",
4316
+ sessionId: opts.session?.sessionId ?? first?.trace_id ?? "unknown",
4317
+ cwd: opts.session?.cwd ?? asString(first?.attributes["tangle.cwd"]) ?? null,
4318
+ ...opts.session?.path ? { path: opts.session.path } : {}
4319
+ };
4320
+ }
4321
+ function evidence2(kind, label, value, spanIds) {
4322
+ return { kind, label, value, ...spanIds && spanIds.length > 0 ? { spanIds } : {} };
4323
+ }
4324
+ function finding(input) {
4325
+ const fingerprint = hash([input.ruleId, ...input.signature]);
4326
+ return {
4327
+ schemaVersion: 1,
4328
+ kind: "traces.live_finding",
4329
+ id: `live.${input.ruleId}.${fingerprint}`,
4330
+ fingerprint,
4331
+ ruleId: input.ruleId,
4332
+ severity: input.severity,
4333
+ title: input.title,
4334
+ claim: input.claim,
4335
+ action: input.action,
4336
+ check: input.check,
4337
+ evidence: input.evidence,
4338
+ session: input.session,
4339
+ observedAt: input.observedAt
4340
+ };
4341
+ }
4342
+ function analyzeLiveBatch(spans, opts = {}) {
4343
+ const ordered2 = orderSpans(spans);
4344
+ const generatedAt = opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
4345
+ const session = sessionFrom(ordered2, opts);
4346
+ const tools = ordered2.filter(isTool);
4347
+ const erroredTools = tools.filter((span2) => span2.status.code === "ERROR");
4348
+ const verificationCalls = tools.filter(isVerification);
4349
+ const changeCalls = tools.filter(isChange);
4350
+ const findings = [];
4351
+ const failedBySignature = /* @__PURE__ */ new Map();
4352
+ for (const span2 of erroredTools) {
4353
+ const signature = toolSignature(span2);
4354
+ failedBySignature.set(signature, [...failedBySignature.get(signature) ?? [], span2]);
4355
+ }
4356
+ for (const [signature, rows] of failedBySignature) {
4357
+ if (rows.length < 2) continue;
4358
+ findings.push(finding({
4359
+ ruleId: "same-failing-command",
4360
+ severity: rows.length >= 3 ? "high" : "medium",
4361
+ title: "Same failing command is repeating",
4362
+ claim: `${rows.length} failed tool call(s) repeated the same command or arguments.`,
4363
+ action: "Stop rerunning it; inspect the first failure, change state, or choose a different diagnostic path before another retry.",
4364
+ check: "The next action should read the failing file, inspect the stack/output, or edit code before this command runs again.",
4365
+ evidence: [
4366
+ evidence2("pattern", "command", signature, rows.map((span2) => span2.span_id)),
4367
+ evidence2("metric", "failed_repeats", String(rows.length))
4368
+ ],
4369
+ session,
4370
+ observedAt: generatedAt,
4371
+ signature: [signature, String(rows.length)]
4372
+ }));
4373
+ }
4374
+ let lastChangeIndex = -1;
4375
+ let verificationSinceChange = 0;
4376
+ const verificationSpanIds = [];
4377
+ for (let i = 0; i < ordered2.length; i++) {
4378
+ const span2 = ordered2[i];
4379
+ if (isChange(span2)) {
4380
+ lastChangeIndex = i;
4381
+ verificationSinceChange = 0;
4382
+ verificationSpanIds.length = 0;
4383
+ } else if (isVerification(span2)) {
4384
+ verificationSinceChange += 1;
4385
+ verificationSpanIds.push(span2.span_id);
4386
+ }
4387
+ }
4388
+ if (verificationSinceChange >= 2) {
4389
+ findings.push(finding({
4390
+ ruleId: "verification-without-change",
4391
+ severity: verificationSinceChange >= 3 ? "high" : "medium",
4392
+ title: "Verification is repeating without a state change",
4393
+ claim: `${verificationSinceChange} verification command(s) ran after the last detected code/config change.`,
4394
+ action: "Do not spend another verification run until the agent either edits state or explains what new signal the rerun will reveal.",
4395
+ check: "The next action should be a code/config edit, targeted file read, or a different narrow diagnostic command.",
4396
+ evidence: [
4397
+ evidence2("metric", "verification_calls_since_change", String(verificationSinceChange), verificationSpanIds),
4398
+ evidence2("metric", "change_calls", String(changeCalls.length))
4399
+ ],
4400
+ session,
4401
+ observedAt: generatedAt,
4402
+ signature: [String(verificationSinceChange), verificationSpanIds.join(",")]
4403
+ }));
4404
+ }
4405
+ const lastClaim = [...ordered2].reverse().find((span2) => isTextSpan(span2) && CLAIM_DONE_RE.test(spanContent(span2)));
4406
+ if (lastClaim) {
4407
+ const claimTime = timeOf(lastClaim);
4408
+ const verificationAfterClaim = verificationCalls.some((span2) => timeOf(span2) > claimTime);
4409
+ if (!verificationAfterClaim) {
4410
+ findings.push(finding({
4411
+ ruleId: "completion-claim-without-verification",
4412
+ severity: "medium",
4413
+ title: "Completion claim has no later verification",
4414
+ claim: "The agent claimed completion, correctness, or verification without a later verification tool call in the observed trace.",
4415
+ action: "Require a concrete check after the claim, or downgrade the claim to an unverified hypothesis.",
4416
+ check: "A later test/build/typecheck/lint/smoke command should appear after the claim.",
4417
+ evidence: [
4418
+ evidence2("span", "claim_span", spanContent(lastClaim).slice(0, 240), [lastClaim.span_id]),
4419
+ evidence2("metric", "verification_after_claim", "0")
4420
+ ],
4421
+ session,
4422
+ observedAt: generatedAt,
4423
+ signature: [lastClaim.span_id, spanContent(lastClaim).slice(0, 120)]
4424
+ }));
4425
+ }
4426
+ }
4427
+ if (tools.length >= 4 && erroredTools.length / tools.length >= 0.5) {
4428
+ findings.push(finding({
4429
+ ruleId: "high-tool-error-rate",
4430
+ severity: erroredTools.length / tools.length >= 0.75 ? "high" : "medium",
4431
+ title: "Tool calls are mostly failing",
4432
+ claim: `${erroredTools.length}/${tools.length} tool call(s) in this live batch ended with ERROR.`,
4433
+ action: "Switch from broad execution to diagnosis: classify the dominant error, then run one targeted command to validate the fix path.",
4434
+ check: "The next batch should reduce the error ratio or show a new targeted diagnostic result.",
4435
+ evidence: [
4436
+ evidence2("metric", "errored_tool_calls", `${erroredTools.length}/${tools.length}`, erroredTools.map((span2) => span2.span_id))
4437
+ ],
4438
+ session,
4439
+ observedAt: generatedAt,
4440
+ signature: [String(erroredTools.length), String(tools.length)]
4441
+ }));
4442
+ }
4443
+ return {
4444
+ schemaVersion: 1,
4445
+ kind: "traces.live_batch",
4446
+ generatedAt,
4447
+ session,
4448
+ spanCount: ordered2.length,
4449
+ newSpanCount: opts.newSpanCount ?? ordered2.length,
4450
+ toolCallCount: tools.length,
4451
+ erroredToolCallCount: erroredTools.length,
4452
+ verificationCallCount: verificationCalls.length,
4453
+ changeCallCount: changeCalls.length,
4454
+ findingCount: findings.length,
4455
+ findings
4456
+ };
4457
+ }
4458
+ function sessionEvent(session, spans, generatedAt) {
4459
+ const ordered2 = orderSpans(spans);
4460
+ return {
4461
+ schemaVersion: 1,
4462
+ kind: "traces.stream.session",
4463
+ event: "session",
4464
+ generatedAt,
4465
+ session,
4466
+ spanCount: ordered2.length,
4467
+ startedAt: ordered2[0]?.start_time ?? null,
4468
+ endedAt: ordered2.at(-1)?.end_time ?? null
4469
+ };
4470
+ }
4471
+ function traceStreamEventsFromSpans(spans, opts = {}) {
4472
+ const generatedAt = opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
4473
+ const session = sessionFrom(spans, {
4474
+ ...opts,
4475
+ session: opts.ref ? { harness: opts.ref.harness, sessionId: opts.ref.sessionId, cwd: opts.ref.cwd, path: opts.ref.path } : opts.session
4476
+ });
4477
+ const ordered2 = orderSpans(spans);
4478
+ const batch = analyzeLiveBatch(ordered2, { generatedAt, session, newSpanCount: ordered2.length });
4479
+ const events = [sessionEvent(session, ordered2, generatedAt)];
4480
+ if (opts.includeSpans !== false) {
4481
+ for (const span2 of ordered2) {
4482
+ events.push({ schemaVersion: 1, kind: "traces.stream.span", event: "span", generatedAt, session, span: span2 });
4483
+ }
4484
+ }
4485
+ events.push({ schemaVersion: 1, kind: "traces.stream.batch", event: "analysis_batch", generatedAt, batch });
4486
+ if (opts.includeFindings !== false) {
4487
+ for (const liveFinding of batch.findings) {
4488
+ events.push({ schemaVersion: 1, kind: "traces.stream.finding", event: "finding", generatedAt, finding: liveFinding });
4489
+ }
4490
+ }
4491
+ return events;
4492
+ }
4493
+ function serializeTraceStreamEvent(event) {
4494
+ return `${JSON.stringify(event)}
4495
+ `;
4496
+ }
4497
+ async function streamSessions(opts) {
4498
+ const intervalMs = Math.max(250, opts.intervalMs ?? 5e3);
4499
+ const windowMs = opts.windowMs ?? 30 * 6e4;
4500
+ const includeSpans = opts.includeSpans !== false;
4501
+ const includeFindings = opts.includeFindings !== false;
4502
+ const includeBatches = opts.includeBatches !== false;
4503
+ const seenSessions = /* @__PURE__ */ new Set();
4504
+ const seenSpans = /* @__PURE__ */ new Set();
4505
+ const seenFindings = /* @__PURE__ */ new Set();
4506
+ while (!opts.signal?.aborted) {
4507
+ let sessions = 0;
4508
+ let newSpans = 0;
4509
+ let findings = 0;
4510
+ for await (const { adapter, ref, spans } of scanSessions({
4511
+ ...opts,
4512
+ sinceMs: opts.sinceMs ?? Date.now() - windowMs,
4513
+ signal: opts.signal
4514
+ })) {
4515
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
4516
+ const session = { harness: adapter.harness, sessionId: ref.sessionId, cwd: ref.cwd, path: ref.path };
4517
+ const ordered2 = orderSpans(spans);
4518
+ const sessionKey = `${session.harness}:${session.sessionId}`;
4519
+ const unseen = ordered2.filter((span2) => {
4520
+ const key = `${sessionKey}:${span2.span_id}`;
4521
+ if (seenSpans.has(key)) return false;
4522
+ seenSpans.add(key);
4523
+ return true;
4524
+ });
4525
+ sessions += 1;
4526
+ newSpans += unseen.length;
4527
+ if (!seenSessions.has(sessionKey)) {
4528
+ seenSessions.add(sessionKey);
4529
+ await opts.onEvent(sessionEvent(session, ordered2, generatedAt));
4530
+ }
4531
+ if (includeSpans) {
4532
+ for (const span2 of unseen) {
4533
+ await opts.onEvent({ schemaVersion: 1, kind: "traces.stream.span", event: "span", generatedAt, session, span: span2 });
4534
+ }
4535
+ }
4536
+ const batch = analyzeLiveBatch(ordered2, { generatedAt, session, newSpanCount: unseen.length });
4537
+ if (includeBatches) {
4538
+ await opts.onEvent({ schemaVersion: 1, kind: "traces.stream.batch", event: "analysis_batch", generatedAt, batch });
4539
+ }
4540
+ if (includeFindings) {
4541
+ for (const liveFinding of batch.findings) {
4542
+ if (seenFindings.has(liveFinding.id)) continue;
4543
+ seenFindings.add(liveFinding.id);
4544
+ findings += 1;
4545
+ await opts.onEvent({ schemaVersion: 1, kind: "traces.stream.finding", event: "finding", generatedAt, finding: liveFinding });
4546
+ }
4547
+ }
4548
+ }
4549
+ await opts.onEvent({
4550
+ schemaVersion: 1,
4551
+ kind: "traces.stream.tick",
4552
+ event: "tick",
4553
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4554
+ sessions,
4555
+ newSpans,
4556
+ findings
4557
+ });
4558
+ if (opts.once || opts.signal?.aborted) break;
4559
+ await sleep(intervalMs, opts.signal);
4560
+ }
4561
+ }
4562
+
4563
+ // src/observer.ts
4564
+ var sleep2 = (ms, signal) => new Promise((resolve2) => {
4565
+ if (signal?.aborted) {
4566
+ resolve2();
4567
+ return;
4568
+ }
4247
4569
  const onAbort = () => {
4248
4570
  clearTimeout(t);
4249
4571
  resolve2();
@@ -4254,14 +4576,15 @@ var sleep = (ms, signal) => new Promise((resolve2) => {
4254
4576
  }, ms);
4255
4577
  signal?.addEventListener("abort", onAbort, { once: true });
4256
4578
  });
4257
- function loopKey(sessionId, toolName2, argHash) {
4258
- return `${sessionId}\0${toolName2}\0${argHash}`;
4579
+ function loopKey(sessionId, toolName3, argHash) {
4580
+ return `${sessionId}:${toolName3}:${argHash}`;
4259
4581
  }
4260
4582
  async function watchSessions(opts) {
4261
4583
  const windowMs = opts.windowMs ?? 30 * 6e4;
4262
4584
  const intervalMs = Math.max(250, opts.intervalMs ?? 5e3);
4263
4585
  const minLoop = opts.minLoopOccurrences ?? 3;
4264
4586
  const seen = /* @__PURE__ */ new Map();
4587
+ const seenFindings = /* @__PURE__ */ new Set();
4265
4588
  while (!opts.signal?.aborted) {
4266
4589
  let sessionCount = 0;
4267
4590
  for await (const { adapter, ref, spans } of scanSessions({
@@ -4288,6 +4611,27 @@ async function watchSessions(opts) {
4288
4611
  opts.onError?.(err, ref);
4289
4612
  }
4290
4613
  }
4614
+ const batch = analyzeLiveBatch(spans, {
4615
+ session: { harness: adapter.harness, sessionId: ref.sessionId, cwd: ref.cwd, path: ref.path }
4616
+ });
4617
+ if (opts.onBatch) {
4618
+ try {
4619
+ await opts.onBatch(ref, batch);
4620
+ } catch (err) {
4621
+ opts.onError?.(err, ref);
4622
+ }
4623
+ }
4624
+ if (opts.onFinding) {
4625
+ for (const finding2 of batch.findings) {
4626
+ if (seenFindings.has(finding2.id)) continue;
4627
+ seenFindings.add(finding2.id);
4628
+ try {
4629
+ await opts.onFinding(finding2);
4630
+ } catch (err) {
4631
+ opts.onError?.(err, ref);
4632
+ }
4633
+ }
4634
+ }
4291
4635
  for (const f of report.stuckLoops.findings) {
4292
4636
  const key = loopKey(ref.sessionId, f.toolName, f.argHash);
4293
4637
  if (f.occurrences > (seen.get(key) ?? 0)) {
@@ -4316,7 +4660,7 @@ async function watchSessions(opts) {
4316
4660
  opts.onError?.(err);
4317
4661
  }
4318
4662
  if (opts.signal?.aborted) break;
4319
- await sleep(intervalMs, opts.signal);
4663
+ await sleep2(intervalMs, opts.signal);
4320
4664
  }
4321
4665
  }
4322
4666
 
@@ -4543,7 +4887,7 @@ async function writeSessionIndexFile(index, outPath) {
4543
4887
  }
4544
4888
 
4545
4889
  // src/upload-state.ts
4546
- import { createHash as createHash2 } from "crypto";
4890
+ import { createHash as createHash3 } from "crypto";
4547
4891
  import { mkdir as mkdir2, readFile as readFile15, writeFile as writeFile6 } from "fs/promises";
4548
4892
  import { homedir as homedir11 } from "os";
4549
4893
  import { dirname as dirname4, join as join18 } from "path";
@@ -4555,7 +4899,7 @@ function uploadKey(harness, sessionId) {
4555
4899
  return `${harness}:${sessionId}`;
4556
4900
  }
4557
4901
  function sessionHash(spans) {
4558
- const h = createHash2("sha256");
4902
+ const h = createHash3("sha256");
4559
4903
  for (const s of spans) {
4560
4904
  h.update(`${s.span_id}\0${s.name}\0${s.start_time}\0${s.status.code}
4561
4905
  `);
@@ -4575,8 +4919,8 @@ async function saveState(state) {
4575
4919
  await writeFile6(f, `${JSON.stringify(state, null, 2)}
4576
4920
  `, "utf8");
4577
4921
  }
4578
- function alreadyUploaded(state, harness, sessionId, hash) {
4579
- return state[uploadKey(harness, sessionId)]?.hash === hash;
4922
+ function alreadyUploaded(state, harness, sessionId, hash2) {
4923
+ return state[uploadKey(harness, sessionId)]?.hash === hash2;
4580
4924
  }
4581
4925
 
4582
4926
  // src/upload.ts
@@ -4593,13 +4937,13 @@ async function planUpload(opts = {}) {
4593
4937
  const items = [];
4594
4938
  for await (const { ref, spans: raw } of scanSessions(opts)) {
4595
4939
  const { spans, report } = redactSpans(raw);
4596
- const hash = sessionHash(spans);
4940
+ const hash2 = sessionHash(spans);
4597
4941
  items.push({
4598
4942
  ref,
4599
4943
  spans,
4600
4944
  redaction: report,
4601
- hash,
4602
- isNew: !alreadyUploaded(state, ref.harness, ref.sessionId, hash)
4945
+ hash: hash2,
4946
+ isNew: !alreadyUploaded(state, ref.harness, ref.sessionId, hash2)
4603
4947
  });
4604
4948
  }
4605
4949
  return { items, state, sinceMs: opts.sinceMs };
@@ -4796,6 +5140,10 @@ export {
4796
5140
  writeTraceImprovementArtifacts,
4797
5141
  runTraceImprovementLoop,
4798
5142
  saveReport,
5143
+ analyzeLiveBatch,
5144
+ traceStreamEventsFromSpans,
5145
+ serializeTraceStreamEvent,
5146
+ streamSessions,
4799
5147
  watchSessions,
4800
5148
  buildSessionIndex,
4801
5149
  buildSessionIndexFromRows,
package/dist/cli.js CHANGED
@@ -33,18 +33,22 @@ import {
33
33
  serializePolicyEvidence,
34
34
  serializeSessionIndex,
35
35
  serializeSpans,
36
+ serializeTraceStreamEvent,
37
+ streamSessions,
36
38
  summarizeDeterministicSignals,
39
+ traceStreamEventsFromSpans,
37
40
  watchSessions,
38
41
  writeInspectionReportFile,
39
42
  writeOtlpFile,
40
43
  writePolicyEvidenceFile,
41
44
  writeSessionIndexFile,
42
45
  writeTraceEvidenceExportFile
43
- } from "./chunk-QM4QAG3V.js";
46
+ } from "./chunk-G3OBZSWY.js";
44
47
 
45
48
  // src/cli.ts
46
49
  import { readFileSync } from "fs";
47
50
  import { readFile, stat, writeFile } from "fs/promises";
51
+ import { basename, resolve } from "path";
48
52
  function packageVersion() {
49
53
  const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
50
54
  if (typeof pkg.version !== "string" || !pkg.version) throw new Error("package.json is missing version");
@@ -66,6 +70,9 @@ function parseArgs(argv) {
66
70
  yes: false,
67
71
  noContent: false,
68
72
  analyzers: [],
73
+ replay: false,
74
+ noSpans: false,
75
+ noFindings: false,
69
76
  attrs: []
70
77
  };
71
78
  for (let i = 1; i < argv.length; i++) {
@@ -135,6 +142,16 @@ function parseArgs(argv) {
135
142
  case "--no-content":
136
143
  a.noContent = true;
137
144
  break;
145
+ case "--replay":
146
+ case "--once":
147
+ a.replay = true;
148
+ break;
149
+ case "--no-spans":
150
+ a.noSpans = true;
151
+ break;
152
+ case "--no-findings":
153
+ a.noFindings = true;
154
+ break;
138
155
  case "--analyzer": {
139
156
  const v = next();
140
157
  if (v) a.analyzers.push(v);
@@ -486,12 +503,102 @@ async function cmdImprove(args) {
486
503
  `improvement artifacts \u2192 ${dir} (${result.findings.length} findings, ${result.recommendations.length} recommendations, ${result.proposals.length} proposal(s), OTLP: ${result.otlpPath})`
487
504
  );
488
505
  }
506
+ function summarizeFindingEvidence(finding) {
507
+ const first = finding.evidence[0];
508
+ if (!first) return "no evidence";
509
+ return `${first.label}: ${first.value}`;
510
+ }
511
+ function formatLiveFinding(finding) {
512
+ const ts = new Date(finding.observedAt).toISOString().slice(11, 19);
513
+ const where = `[${finding.session.harness}] ${finding.session.sessionId.slice(0, 8)}`;
514
+ const cwd = finding.session.cwd ? ` \xB7 ${finding.session.cwd}` : "";
515
+ return [
516
+ `${ts} ${finding.severity.toUpperCase()} ${where} \u2014 ${finding.title}${cwd}`,
517
+ ` evidence: ${summarizeFindingEvidence(finding)}`,
518
+ ` action: ${finding.action}`,
519
+ ` check: ${finding.check}`
520
+ ].join("\n");
521
+ }
522
+ async function streamExplicitSession(args) {
523
+ if (!args.session) throw new Error("streamExplicitSession needs --session");
524
+ const adapter = resolveAdapter(args.harness);
525
+ if (!adapter) throw new Error(`unknown harness "${args.harness}"`);
526
+ const st = await stat(args.session);
527
+ const ref = {
528
+ harness: adapter.harness,
529
+ sessionId: basename(args.session),
530
+ path: resolve(args.session),
531
+ cwd: args.cwd ?? null,
532
+ mtimeMs: st.mtimeMs
533
+ };
534
+ const spans = await parseSession(adapter, ref);
535
+ for (const event of traceStreamEventsFromSpans(spans, {
536
+ ref,
537
+ includeSpans: !args.noSpans,
538
+ includeFindings: !args.noFindings
539
+ })) {
540
+ process.stdout.write(serializeTraceStreamEvent(event));
541
+ }
542
+ }
543
+ async function cmdStream(args) {
544
+ if (args.input) {
545
+ const { spans, harness } = await collectImportedSpans(args);
546
+ const ref = {
547
+ harness,
548
+ sessionId: basename(args.input),
549
+ path: resolve(args.input),
550
+ cwd: args.cwd ?? null,
551
+ mtimeMs: Date.now()
552
+ };
553
+ for (const event of traceStreamEventsFromSpans(spans, {
554
+ ref,
555
+ includeSpans: !args.noSpans,
556
+ includeFindings: !args.noFindings
557
+ })) {
558
+ process.stdout.write(serializeTraceStreamEvent(event));
559
+ }
560
+ return;
561
+ }
562
+ if (args.session) {
563
+ await streamExplicitSession(args);
564
+ return;
565
+ }
566
+ const all = args.all || !args.harnessExplicit && !args.cwd;
567
+ const controller = new AbortController();
568
+ process.once("SIGINT", () => controller.abort());
569
+ if (!args.replay) {
570
+ process.stderr.write(
571
+ `traces stream \u2014 JSONL live feed for ${all ? "all harnesses" : args.harness}, last ${args.window}m, every ${args.interval}s. Ctrl-C to stop.
572
+ `
573
+ );
574
+ }
575
+ await streamSessions({
576
+ all,
577
+ harnesses: all ? void 0 : [args.harness],
578
+ cwd: args.cwd,
579
+ last: args.last > 0 ? args.last : args.replay ? 1 : void 0,
580
+ windowMs: args.window * 6e4,
581
+ intervalMs: args.interval * 1e3,
582
+ once: args.replay,
583
+ includeSpans: !args.noSpans,
584
+ includeFindings: !args.noFindings,
585
+ signal: controller.signal,
586
+ onEvent: (event) => {
587
+ process.stdout.write(serializeTraceStreamEvent(event));
588
+ },
589
+ onError: (err, ref) => {
590
+ const where = ref ? ` [${ref.harness}] ${ref.sessionId}` : "";
591
+ process.stderr.write(`stream error${where}: ${err instanceof Error ? err.message : String(err)}
592
+ `);
593
+ }
594
+ });
595
+ }
489
596
  async function cmdWatch(args) {
490
597
  const all = args.all || !args.harnessExplicit && !args.cwd;
491
598
  const controller = new AbortController();
492
599
  process.once("SIGINT", () => controller.abort());
493
600
  process.stderr.write(
494
- `traces watch \u2014 observing ${all ? "all harnesses" : args.harness}, sessions active in the last ${args.window}m, every ${args.interval}s. Read-only; Ctrl-C to stop.
601
+ `traces watch \u2014 observing ${all ? "all harnesses" : args.harness}, sessions active in the last ${args.window}m, every ${args.interval}s. Loop + semantic live findings; read-only; Ctrl-C to stop.
495
602
  `
496
603
  );
497
604
  await watchSessions({
@@ -505,9 +612,13 @@ async function cmdWatch(args) {
505
612
  onLoop: (l) => {
506
613
  const ts = (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
507
614
  process.stdout.write(
508
- `\u26A0\uFE0F ${ts} [${l.harness}] ${l.sessionId.slice(0, 8)} \u2014 \`${l.toolName}\` repeated \xD7${l.occurrences} with identical args (${(l.windowMs / 1e3).toFixed(0)}s)${l.cwd ? ` \xB7 ${l.cwd}` : ""}
615
+ `${ts} LOOP [${l.harness}] ${l.sessionId.slice(0, 8)} \u2014 \`${l.toolName}\` repeated \xD7${l.occurrences} with identical args (${(l.windowMs / 1e3).toFixed(0)}s)${l.cwd ? ` \xB7 ${l.cwd}` : ""}
509
616
  `
510
617
  );
618
+ },
619
+ onFinding: (finding) => {
620
+ process.stdout.write(`${formatLiveFinding(finding)}
621
+ `);
511
622
  }
512
623
  });
513
624
  }
@@ -607,14 +718,15 @@ Commands:
607
718
  inspect Read a session index and print ranked improvement findings
608
719
  export Convert evidence/events files to OpenInference JSONL for HALO
609
720
  evidence Emit compact session-evidence JSONL for downstream policy miners
610
- watch Online observer: tail active sessions, notify on stuck loops (read-only)
721
+ stream Emit JSONL trace stream events for live visualizers or replay
722
+ watch Online observer: tail active sessions, notify on loops + semantic findings
611
723
  upload Redact + upload sessions in a time window to the Tangle Intelligence Platform
612
724
 
613
725
  Options:
614
726
  --harness <id> Harness or alias (default: claude-code). Known: ${knownHarnesses().join(", ")}
615
727
  --all Sweep every known harness
616
728
  --last <n> Most-recent N sessions
617
- --session <path> Analyze one explicit session file
729
+ --session <path> Analyze/stream one explicit harness session file
618
730
  --cwd <dir> Filter sessions by working directory
619
731
  --since <t> upload: window \u2014 30m / 2h / 7d or an ISO date (default 24h); analyze: ISO cutoff
620
732
  --out <path> Write report to a file
@@ -623,6 +735,9 @@ Options:
623
735
  --format <kind> analyze/export file: auto | policy-evidence | sandbox-events | openinference | intelligence-spans
624
736
  --metadata <json> analyze/export file: attach JSON object fields as span attributes
625
737
  --attr <k=v> analyze/export file: attach one span attribute (repeatable)
738
+ --replay, --once stream: scan once and exit (default for positional input / --session)
739
+ --no-spans stream: omit per-span pulse events
740
+ --no-findings stream: omit semantic live-finding events
626
741
  --llm Enable agentic RLM analysts (needs OPENAI_API_KEY / OPENAI_BASE_URL)
627
742
  --model <id> --llm model id (e.g. a router model like glm-5.2); default is agent-eval's
628
743
  --config <path> investigate/improve: JS config with analysts, external analyzers, or proposal adapter
@@ -685,6 +800,9 @@ async function main() {
685
800
  case "evidence":
686
801
  await cmdEvidence(args);
687
802
  break;
803
+ case "stream":
804
+ await cmdStream(args);
805
+ break;
688
806
  case "watch":
689
807
  await cmdWatch(args);
690
808
  break;
package/dist/index.d.ts CHANGED
@@ -1276,6 +1276,115 @@ declare function writeTraceImprovementArtifacts(result: Pick<TraceImprovementRes
1276
1276
  declare function runTraceImprovementLoop(opts: TraceImprovementOptions): Promise<TraceImprovementResult>;
1277
1277
  declare function saveReport(path: string, report: string): Promise<void>;
1278
1278
 
1279
+ type TraceLiveSeverity = 'info' | 'low' | 'medium' | 'high';
1280
+ interface TraceLiveEvidence {
1281
+ readonly kind: 'metric' | 'span' | 'pattern';
1282
+ readonly label: string;
1283
+ readonly value: string;
1284
+ readonly spanIds?: readonly string[];
1285
+ }
1286
+ interface TraceLiveFinding {
1287
+ readonly schemaVersion: 1;
1288
+ readonly kind: 'traces.live_finding';
1289
+ readonly id: string;
1290
+ readonly ruleId: string;
1291
+ readonly fingerprint: string;
1292
+ readonly severity: TraceLiveSeverity;
1293
+ readonly title: string;
1294
+ readonly claim: string;
1295
+ readonly action: string;
1296
+ readonly check: string;
1297
+ readonly evidence: readonly TraceLiveEvidence[];
1298
+ readonly session: TraceStreamSession;
1299
+ readonly observedAt: string;
1300
+ }
1301
+ interface TraceStreamSession {
1302
+ readonly harness: string;
1303
+ readonly sessionId: string;
1304
+ readonly cwd: string | null;
1305
+ readonly path?: string;
1306
+ }
1307
+ interface TraceLiveBatch {
1308
+ readonly schemaVersion: 1;
1309
+ readonly kind: 'traces.live_batch';
1310
+ readonly generatedAt: string;
1311
+ readonly session: TraceStreamSession;
1312
+ readonly spanCount: number;
1313
+ readonly newSpanCount: number;
1314
+ readonly toolCallCount: number;
1315
+ readonly erroredToolCallCount: number;
1316
+ readonly verificationCallCount: number;
1317
+ readonly changeCallCount: number;
1318
+ readonly findingCount: number;
1319
+ readonly findings: readonly TraceLiveFinding[];
1320
+ }
1321
+ type TraceStreamEvent = {
1322
+ readonly schemaVersion: 1;
1323
+ readonly kind: 'traces.stream.session';
1324
+ readonly event: 'session';
1325
+ readonly generatedAt: string;
1326
+ readonly session: TraceStreamSession;
1327
+ readonly spanCount: number;
1328
+ readonly startedAt: string | null;
1329
+ readonly endedAt: string | null;
1330
+ } | {
1331
+ readonly schemaVersion: 1;
1332
+ readonly kind: 'traces.stream.span';
1333
+ readonly event: 'span';
1334
+ readonly generatedAt: string;
1335
+ readonly session: TraceStreamSession;
1336
+ readonly span: OtlpSpan;
1337
+ } | {
1338
+ readonly schemaVersion: 1;
1339
+ readonly kind: 'traces.stream.batch';
1340
+ readonly event: 'analysis_batch';
1341
+ readonly generatedAt: string;
1342
+ readonly batch: TraceLiveBatch;
1343
+ } | {
1344
+ readonly schemaVersion: 1;
1345
+ readonly kind: 'traces.stream.finding';
1346
+ readonly event: 'finding';
1347
+ readonly generatedAt: string;
1348
+ readonly finding: TraceLiveFinding;
1349
+ } | {
1350
+ readonly schemaVersion: 1;
1351
+ readonly kind: 'traces.stream.tick';
1352
+ readonly event: 'tick';
1353
+ readonly generatedAt: string;
1354
+ readonly sessions: number;
1355
+ readonly newSpans: number;
1356
+ readonly findings: number;
1357
+ };
1358
+ interface LiveBatchOptions {
1359
+ readonly generatedAt?: string;
1360
+ readonly session?: Partial<TraceStreamSession>;
1361
+ readonly newSpanCount?: number;
1362
+ }
1363
+ interface TraceStreamReplayOptions extends LiveBatchOptions {
1364
+ readonly ref?: SessionRef;
1365
+ readonly includeSpans?: boolean;
1366
+ readonly includeFindings?: boolean;
1367
+ }
1368
+ interface TraceStreamOptions extends ScanOptions {
1369
+ /** Observe sessions active within this window. Default 30 min. */
1370
+ readonly windowMs?: number;
1371
+ /** Poll interval. Default 5s. */
1372
+ readonly intervalMs?: number;
1373
+ /** Stop after one scan, useful for replay or tests. */
1374
+ readonly once?: boolean;
1375
+ /** Emit one JSON event per new span. Default true. */
1376
+ readonly includeSpans?: boolean;
1377
+ /** Emit live semantic findings. Default true. */
1378
+ readonly includeFindings?: boolean;
1379
+ /** Emit session and analysis-batch events. Default true. */
1380
+ readonly includeBatches?: boolean;
1381
+ readonly onEvent: (event: TraceStreamEvent) => void | Promise<void>;
1382
+ }
1383
+ declare function analyzeLiveBatch(spans: readonly OtlpSpan[], opts?: LiveBatchOptions): TraceLiveBatch;
1384
+ declare function traceStreamEventsFromSpans(spans: readonly OtlpSpan[], opts?: TraceStreamReplayOptions): TraceStreamEvent[];
1385
+ declare function serializeTraceStreamEvent(event: TraceStreamEvent): string;
1386
+ declare function streamSessions(opts: TraceStreamOptions): Promise<void>;
1387
+
1279
1388
  /**
1280
1389
  * Live trace observer — the reusable, event-driven core behind `traces watch`.
1281
1390
  *
@@ -1323,6 +1432,10 @@ interface ObserverOptions {
1323
1432
  /** Fired for every observed session each tick, with its full pipeline report
1324
1433
  * (stuck loops + tool-use metrics) — react to anything, not just loops. */
1325
1434
  onReport?: (ref: SessionRef, report: PipelineReport) => void | Promise<void>;
1435
+ /** Fired for every observed session each tick, after semantic live analysis. */
1436
+ onBatch?: (ref: SessionRef, batch: TraceLiveBatch) => void | Promise<void>;
1437
+ /** Fired once for each newly-observed live semantic finding. */
1438
+ onFinding?: (finding: TraceLiveFinding) => void | Promise<void>;
1326
1439
  /** Per-session parse/pipeline error (the observer keeps going). */
1327
1440
  onError?: (error: unknown, ref?: SessionRef) => void;
1328
1441
  /** Fired after each poll cycle. */
@@ -1489,4 +1602,4 @@ interface ExecuteOptions {
1489
1602
  /** Send the NEW items (or, on dryRun, write the redacted OTLP that would send). */
1490
1603
  declare function executeUpload(plan: UploadPlan, opts?: ExecuteOptions): Promise<UploadResult>;
1491
1604
 
1492
- export { ACTOR_ATTR, ATTR, type Actor, type AdoptionOptions, type AdoptionReport, AmpAdapter, type AnalyzeOptions, type AnalyzeResult, type BuildPolicyEvidenceOptions, type BuildSessionIndexOptions, type BuildTraceFindingPacketOptions, CODING_REDACTION_RULES, CONTENT_CAP, CORRECTIVE_REACTIONS, ClaudeAdapter, CodexAdapter, type CollectOptions, type CollectPolicyEvidenceOptions, type CollectSessionIndexOptions, CopilotAdapter, DEFAULT_HARNESS, type ExecuteOptions, type ExternalAnalysisResult, type ExternalAnalyzer, FactoryAdapter, type FindingStore, ForgeAdapter, GeminiAdapter, type HarnessTraceAdapter, INGEST_SOURCE_CLI, type ImprovementAdapter, type ImprovementAdapterInput, type ImprovementProposal, type InspectSessionIndexOptions, type LocateOptions, type ObservedLoop, type ObserverOptions, OpencodeAdapter, type OtlpSpan, type OtlpSpanKind, type OtlpStatusCode, PiAdapter, type PipelineOptions, type PipelineReport, type PlanOptions, type PolicyEvidenceLoopSummary, type PolicyEvidenceRecord, type PolicyEvidenceToolSummary, QwenAdapter, REACTIONS, type Reaction, type ReactionReport, type ReactionSessionReport, type RecommendationWriter, type Redactor, type RepoAttrs, type RepoResolution, type RepoResolutionSource, type RunResult, type RuntimeTrace, type ScanOptions, type ScannedSession, type SessionBatch, type SessionRef, type SpanInput, type SpanRedaction, TRACES_REDACTION_RULES, type TraceClaim, type TraceContextIndex, type TraceContextRoot, type TraceEvidenceExportOptions, type TraceEvidenceExportResult, type TraceEvidenceFormatOption, type TraceEvidenceInputFormat, type TraceEvidenceRow, type TraceFindingPacket, type TraceImprovementArtifacts, type TraceImprovementOptions, type TraceImprovementResult, type TraceIndexedFile, type TraceInspectionArea, type TraceInspectionFinding, type TraceInspectionReport, type TraceInspectionSeverity, type TraceInvestigationOptions, type TraceInvestigationResult, type TraceRecommendation, type TraceReplayProof, type TraceSessionIndex, type TraceSessionIndexRow, type TraceStoreInvestigationOptions, type TraceStoreInvestigationResult, type TracesConfig, type TriggerPair, type UploadBackend, type UploadItem, type UploadPlan, type UploadRecord, type UploadResult, type UploadState, type UserPromptInput, alreadyUploaded, analyzeAdoption, analyzeReactions, analyzeSpans, applyRedactor, buildPolicyEvidenceRecord, buildSessionIndex, buildSessionIndexFromRows, buildTraceFindingPacket, capText, classifyReaction, claudeActor, codexActor, collectPolicyEvidence, collectSessionIndex, collectSessions, commandAnalyzer, commandRedactor, countSkillRunsJsonl, executeUpload, exportTraceEvidenceFile, exportTraceEvidenceRows, exportTraceEvidenceText, haloAnalyzer, inspectSessionIndex, knownHarnesses, listAdapters, loadState, loadTracesConfig, looksLikeAgentPrompt, mergeTracesConfig, normalizeRemote, parseIsoToEpochMs, parseSession, parseSince, planUpload, readSessionIndexFile, redactSpans, renderInspectionReport, resolveAdapter, resolveRepoAttrs, resolveSessionRepoAttrs, runCommand, runExternalAnalyzers, runPipelines, runTraceImprovementLoop, runTraceInvestigation, runTraceStoreInvestigation, saveReport, saveState, scanSessions, selectAdapters, serializeInspectionReport, serializePolicyEvidence, serializeSessionIndex, serializeSpans, sessionHash, span, stampRepoAttrs, textIsSynthetic, toOpenInferenceSpan, toRuntimeStore, toTraceSpanEvents, uploadKey, userPromptSpan, watchSessions, writeInspectionReportFile, writeOtlpFile, writePolicyEvidenceFile, writeSessionIndexFile, writeTraceEvidenceExportFile, writeTraceImprovementArtifacts };
1605
+ export { ACTOR_ATTR, ATTR, type Actor, type AdoptionOptions, type AdoptionReport, AmpAdapter, type AnalyzeOptions, type AnalyzeResult, type BuildPolicyEvidenceOptions, type BuildSessionIndexOptions, type BuildTraceFindingPacketOptions, CODING_REDACTION_RULES, CONTENT_CAP, CORRECTIVE_REACTIONS, ClaudeAdapter, CodexAdapter, type CollectOptions, type CollectPolicyEvidenceOptions, type CollectSessionIndexOptions, CopilotAdapter, DEFAULT_HARNESS, type ExecuteOptions, type ExternalAnalysisResult, type ExternalAnalyzer, FactoryAdapter, type FindingStore, ForgeAdapter, GeminiAdapter, type HarnessTraceAdapter, INGEST_SOURCE_CLI, type ImprovementAdapter, type ImprovementAdapterInput, type ImprovementProposal, type InspectSessionIndexOptions, type LiveBatchOptions, type LocateOptions, type ObservedLoop, type ObserverOptions, OpencodeAdapter, type OtlpSpan, type OtlpSpanKind, type OtlpStatusCode, PiAdapter, type PipelineOptions, type PipelineReport, type PlanOptions, type PolicyEvidenceLoopSummary, type PolicyEvidenceRecord, type PolicyEvidenceToolSummary, QwenAdapter, REACTIONS, type Reaction, type ReactionReport, type ReactionSessionReport, type RecommendationWriter, type Redactor, type RepoAttrs, type RepoResolution, type RepoResolutionSource, type RunResult, type RuntimeTrace, type ScanOptions, type ScannedSession, type SessionBatch, type SessionRef, type SpanInput, type SpanRedaction, TRACES_REDACTION_RULES, type TraceClaim, type TraceContextIndex, type TraceContextRoot, type TraceEvidenceExportOptions, type TraceEvidenceExportResult, type TraceEvidenceFormatOption, type TraceEvidenceInputFormat, type TraceEvidenceRow, type TraceFindingPacket, type TraceImprovementArtifacts, type TraceImprovementOptions, type TraceImprovementResult, type TraceIndexedFile, type TraceInspectionArea, type TraceInspectionFinding, type TraceInspectionReport, type TraceInspectionSeverity, type TraceInvestigationOptions, type TraceInvestigationResult, type TraceLiveBatch, type TraceLiveEvidence, type TraceLiveFinding, type TraceLiveSeverity, type TraceRecommendation, type TraceReplayProof, type TraceSessionIndex, type TraceSessionIndexRow, type TraceStoreInvestigationOptions, type TraceStoreInvestigationResult, type TraceStreamEvent, type TraceStreamOptions, type TraceStreamReplayOptions, type TraceStreamSession, type TracesConfig, type TriggerPair, type UploadBackend, type UploadItem, type UploadPlan, type UploadRecord, type UploadResult, type UploadState, type UserPromptInput, alreadyUploaded, analyzeAdoption, analyzeLiveBatch, analyzeReactions, analyzeSpans, applyRedactor, buildPolicyEvidenceRecord, buildSessionIndex, buildSessionIndexFromRows, buildTraceFindingPacket, capText, classifyReaction, claudeActor, codexActor, collectPolicyEvidence, collectSessionIndex, collectSessions, commandAnalyzer, commandRedactor, countSkillRunsJsonl, executeUpload, exportTraceEvidenceFile, exportTraceEvidenceRows, exportTraceEvidenceText, haloAnalyzer, inspectSessionIndex, knownHarnesses, listAdapters, loadState, loadTracesConfig, looksLikeAgentPrompt, mergeTracesConfig, normalizeRemote, parseIsoToEpochMs, parseSession, parseSince, planUpload, readSessionIndexFile, redactSpans, renderInspectionReport, resolveAdapter, resolveRepoAttrs, resolveSessionRepoAttrs, runCommand, runExternalAnalyzers, runPipelines, runTraceImprovementLoop, runTraceInvestigation, runTraceStoreInvestigation, saveReport, saveState, scanSessions, selectAdapters, serializeInspectionReport, serializePolicyEvidence, serializeSessionIndex, serializeSpans, serializeTraceStreamEvent, sessionHash, span, stampRepoAttrs, streamSessions, textIsSynthetic, toOpenInferenceSpan, toRuntimeStore, toTraceSpanEvents, traceStreamEventsFromSpans, uploadKey, userPromptSpan, watchSessions, writeInspectionReportFile, writeOtlpFile, writePolicyEvidenceFile, writeSessionIndexFile, writeTraceEvidenceExportFile, writeTraceImprovementArtifacts };
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  TRACES_REDACTION_RULES,
21
21
  alreadyUploaded,
22
22
  analyzeAdoption,
23
+ analyzeLiveBatch,
23
24
  analyzeReactions,
24
25
  analyzeSpans,
25
26
  applyRedactor,
@@ -73,13 +74,16 @@ import {
73
74
  serializePolicyEvidence,
74
75
  serializeSessionIndex,
75
76
  serializeSpans,
77
+ serializeTraceStreamEvent,
76
78
  sessionHash,
77
79
  span,
78
80
  stampRepoAttrs,
81
+ streamSessions,
79
82
  textIsSynthetic,
80
83
  toOpenInferenceSpan,
81
84
  toRuntimeStore,
82
85
  toTraceSpanEvents,
86
+ traceStreamEventsFromSpans,
83
87
  uploadKey,
84
88
  userPromptSpan,
85
89
  watchSessions,
@@ -89,7 +93,7 @@ import {
89
93
  writeSessionIndexFile,
90
94
  writeTraceEvidenceExportFile,
91
95
  writeTraceImprovementArtifacts
92
- } from "./chunk-QM4QAG3V.js";
96
+ } from "./chunk-G3OBZSWY.js";
93
97
 
94
98
  // src/collect.ts
95
99
  async function collectSessions(opts = {}) {
@@ -137,6 +141,7 @@ export {
137
141
  TRACES_REDACTION_RULES,
138
142
  alreadyUploaded,
139
143
  analyzeAdoption,
144
+ analyzeLiveBatch,
140
145
  analyzeReactions,
141
146
  analyzeSpans,
142
147
  applyRedactor,
@@ -197,13 +202,16 @@ export {
197
202
  serializePolicyEvidence,
198
203
  serializeSessionIndex,
199
204
  serializeSpans,
205
+ serializeTraceStreamEvent,
200
206
  sessionHash,
201
207
  span,
202
208
  stampRepoAttrs,
209
+ streamSessions,
203
210
  textIsSynthetic,
204
211
  toOpenInferenceSpan,
205
212
  toRuntimeStore,
206
213
  toTraceSpanEvents,
214
+ traceStreamEventsFromSpans,
207
215
  uploadKey,
208
216
  userPromptSpan,
209
217
  watchSessions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/traces",
3
- "version": "0.8.17",
3
+ "version": "0.8.18",
4
4
  "description": "Point it at your coding-agent session traces (Claude Code, Codex, OpenCode, Gemini, Pi, …) and get failure-mode + efficiency findings. CLI + SDK over the @tangle-network/agent-eval analyst suite — observe live sessions, run your own analysts, redact, and upload to the Tangle Intelligence Platform.",
5
5
  "type": "module",
6
6
  "license": "MIT",