@tangle-network/traces 0.11.5 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,145 @@ function appendAll(target, values) {
3
3
  for (const value of values) target.push(value);
4
4
  }
5
5
 
6
+ // src/attributes.ts
7
+ var ATTR = {
8
+ /** = the trace/session id. Wire contract (dedup linkage). */
9
+ SESSION_ID: "tangle.sessionId",
10
+ /** "cli" marks a CLI upload so the platform can drop a re-upload of a session
11
+ * that also streamed live. Wire contract. */
12
+ INGEST_SOURCE: "tangle.ingest_source",
13
+ HARNESS: "tangle.harness",
14
+ CWD: "tangle.cwd",
15
+ /** THE per-session grouping key the spine derives subjects from
16
+ * (`deriveSubjectKey` reads `tangle.subject.key` first). Resolves to the git
17
+ * remote (host/owner/repo) when readable, else the cwd path basename. */
18
+ SUBJECT_KEY: "tangle.subject.key",
19
+ /** Normalized git remote (e.g. `github.com/tangle-network/agent-dev-container`). */
20
+ GIT_REPOSITORY: "git.repository",
21
+ /** Current branch at conversion time. */
22
+ GIT_BRANCH_NAME: "git.branch",
23
+ /** HEAD short sha at conversion time. */
24
+ GIT_COMMIT: "git.commit",
25
+ /** How traces resolved the session cwd: recorded cwd, repaired cwd, or span path. */
26
+ REPO_RESOLUTION_SOURCE: "traces.repo_resolution_source",
27
+ GIT_BRANCH: "tangle.git_branch",
28
+ HOST: "tangle.host",
29
+ /** Basename of the session file. (Renamed from the ambiguous `tangle.source`,
30
+ * which collided with `tangle.ingest_source`.) */
31
+ SESSION_FILE: "tangle.session_file",
32
+ UPLOADED_AT: "tangle.uploaded_at",
33
+ UPLOADER: "tangle.uploader",
34
+ REDACTION_VERSION: "redaction.version",
35
+ REDACTION_COUNT: "redaction.count",
36
+ /** Upload contract: bounded root summary plus location-only receipt child spans.
37
+ * Receipt hashes are deliberate integrity evidence; raw malformed bytes never upload. */
38
+ SESSION_INTEGRITY: "traces.session.integrity",
39
+ CORRUPTION_COUNT: "traces.session.corruption_count",
40
+ CORRUPTION_DIGEST: "traces.session.corruption_digest",
41
+ CORRUPTION_RECEIPT_VERSION: "traces.session.corruption.receipt_version",
42
+ CORRUPTION_RECEIPT_KIND: "traces.session.corruption.kind",
43
+ CORRUPTION_SOURCE_PATH: "traces.session.corruption.source_path",
44
+ CORRUPTION_LINE_NUMBER: "traces.session.corruption.line_number",
45
+ CORRUPTION_BYTE_OFFSET: "traces.session.corruption.byte_offset",
46
+ CORRUPTION_BYTE_LENGTH: "traces.session.corruption.byte_length",
47
+ CORRUPTION_SHA256: "traces.session.corruption.sha256",
48
+ RAW_SOURCE_RETENTION: "traces.session.raw_source_retention",
49
+ /** Container image reference the session executed in (OTel semconv key). */
50
+ CONTAINER_IMAGE: "container.image.name",
51
+ /** Immutable image digest (`sha256:…`) — the replay-grade environment pin. */
52
+ CONTAINER_IMAGE_DIGEST: "container.image.digest",
53
+ /** Sandbox/container instance id that executed the session. */
54
+ SANDBOX_ID: "tangle.sandbox.id",
55
+ /** Recorded execution cwd, verbatim. Unlike `tangle.cwd` it is never
56
+ * repaired against the host filesystem — replay verification needs the
57
+ * in-container path even when no such path exists on this host. */
58
+ ENVIRONMENT_CWD: "tangle.environment.cwd"
59
+ };
60
+ var INGEST_SOURCE_CLI = "cli";
61
+ function environmentAttributes(environment) {
62
+ if (!environment) return {};
63
+ const attrs = {};
64
+ if (environment.image) attrs[ATTR.CONTAINER_IMAGE] = environment.image;
65
+ if (environment.imageDigest) attrs[ATTR.CONTAINER_IMAGE_DIGEST] = environment.imageDigest;
66
+ if (environment.sandboxId) attrs[ATTR.SANDBOX_ID] = environment.sandboxId;
67
+ if (environment.cwd) attrs[ATTR.ENVIRONMENT_CWD] = environment.cwd;
68
+ if (environment.gitCommit) attrs[ATTR.GIT_COMMIT] = environment.gitCommit;
69
+ return attrs;
70
+ }
71
+ function stampEnvironmentAttrs(spans, environment) {
72
+ const attrs = environmentAttributes(environment);
73
+ const keys = Object.keys(attrs);
74
+ if (keys.length === 0) return;
75
+ for (const span2 of spans) {
76
+ for (const key of keys) {
77
+ if (span2.attributes[key] === void 0) span2.attributes[key] = attrs[key];
78
+ }
79
+ }
80
+ }
81
+ function environmentFromSpanAttributes(spans) {
82
+ const first = (key) => {
83
+ for (const span2 of spans) {
84
+ const value = span2.attributes[key];
85
+ if (typeof value === "string" && value.length > 0) return value;
86
+ }
87
+ return void 0;
88
+ };
89
+ const image = first(ATTR.CONTAINER_IMAGE);
90
+ const imageDigest = first(ATTR.CONTAINER_IMAGE_DIGEST);
91
+ const sandboxId = first(ATTR.SANDBOX_ID);
92
+ const cwd = first(ATTR.ENVIRONMENT_CWD);
93
+ if (!image && !imageDigest && !sandboxId && !cwd) return null;
94
+ return {
95
+ ...image ? { image } : {},
96
+ ...imageDigest ? { imageDigest } : {},
97
+ ...sandboxId ? { sandboxId } : {},
98
+ cwd: cwd ?? null
99
+ };
100
+ }
101
+ var DEFAULT_HARNESS = "claude-code";
102
+ var SESSION_ID_ATTRIBUTE_KEYS = [ATTR.SESSION_ID, "session.id", "traces.session.id"];
103
+ function sessionIdFromAttributes(attributes2) {
104
+ return SESSION_ID_ATTRIBUTE_KEYS.map((key) => attributes2[key]).find(
105
+ (value) => typeof value === "string" && value.length > 0
106
+ );
107
+ }
108
+ function indexSessionIdsByTrace(spans) {
109
+ const candidatesByTrace = /* @__PURE__ */ new Map();
110
+ for (const span2 of spans) {
111
+ const candidates = candidatesByTrace.get(span2.trace_id) ?? /* @__PURE__ */ new Set();
112
+ for (const key of SESSION_ID_ATTRIBUTE_KEYS) {
113
+ const value = span2.attributes[key];
114
+ if (typeof value === "string" && value.length > 0) candidates.add(value);
115
+ }
116
+ if (candidates.size > 0) candidatesByTrace.set(span2.trace_id, candidates);
117
+ }
118
+ const sessionByTrace = /* @__PURE__ */ new Map();
119
+ const conflicts = [];
120
+ for (const [traceId, candidates] of candidatesByTrace) {
121
+ const sessionIds = [...candidates].sort();
122
+ if (sessionIds.length === 1) {
123
+ sessionByTrace.set(traceId, sessionIds[0]);
124
+ } else {
125
+ conflicts.push({ traceId, sessionIds });
126
+ }
127
+ }
128
+ conflicts.sort((a, b) => a.traceId.localeCompare(b.traceId));
129
+ return { sessionByTrace, conflicts };
130
+ }
131
+ function stampSessionIdentity(spans, fallbackSessionId) {
132
+ const { sessionByTrace, conflicts } = indexSessionIdsByTrace(spans);
133
+ const conflictingTraceIds = new Set(conflicts.map((conflict) => conflict.traceId));
134
+ const traceIds = new Set(spans.map((span2) => span2.trace_id));
135
+ const singleTraceId = traceIds.size === 1 ? traceIds.values().next().value : void 0;
136
+ for (const span2 of spans) {
137
+ if (conflictingTraceIds.has(span2.trace_id)) continue;
138
+ const sessionId = sessionByTrace.get(span2.trace_id) ?? singleTraceId ?? fallbackSessionId;
139
+ if (!sessionIdFromAttributes(span2.attributes)) {
140
+ span2.attributes[ATTR.SESSION_ID] = sessionId;
141
+ }
142
+ }
143
+ }
144
+
6
145
  // src/time.ts
7
146
  function parseIsoToEpochMs(ts) {
8
147
  if (typeof ts !== "string" || ts.trim().length === 0) {
@@ -31,7 +170,7 @@ function parseSince(value) {
31
170
  import { readdir, readFile, stat } from "fs/promises";
32
171
  import { basename, join } from "path";
33
172
  import {
34
- ATTR,
173
+ ATTR as ATTR2,
35
174
  declaredSpanKind,
36
175
  resolveSpanKind,
37
176
  SPAN_KIND_ATTR_KEYS,
@@ -325,7 +464,7 @@ function readLinks(value, traceId) {
325
464
  return links;
326
465
  }
327
466
  function bridgeContractAttributes(attributes2) {
328
- const toolName4 = nonEmptyString(attributes2[ATTR.toolName]);
467
+ const toolName4 = nonEmptyString(attributes2[ATTR2.toolName]);
329
468
  if (toolName4 !== null && nonEmptyString(attributes2[TOOL_NAME]) === null) {
330
469
  attributes2[TOOL_NAME] = toolName4;
331
470
  }
@@ -385,7 +524,7 @@ function otlpRowToSpan(row, fallbackTraceId) {
385
524
  bridgeContractAttributes(attributes2);
386
525
  const { kind, declaredRaw, substituted: kindSubstituted } = resolveRowKind(row, attributes2, name);
387
526
  if (kindSubstituted) substitute("kind", declaredRaw ?? void 0);
388
- attributes2[ATTR.spanKind] = kind;
527
+ attributes2[ATTR2.spanKind] = kind;
389
528
  if (kind === "TOOL") Object.assign(attributes2, capturedToolIo(attributes2));
390
529
  const status = isObject(row.status) ? row.status : void 0;
391
530
  const statusMessage = nonEmptyString(status?.message);
@@ -725,6 +864,9 @@ function toOpenInferenceSpan(s) {
725
864
  for (const k of ["tangle.subject.key", "git.repository", "git.branch", "git.commit", "tangle.cwd", "traces.repo_resolution_source"]) {
726
865
  if (a[k] != null) resourceAttrs[k] = a[k];
727
866
  }
867
+ for (const k of ["container.image.name", "container.image.digest", "tangle.sandbox.id", "tangle.environment.cwd"]) {
868
+ if (a[k] != null) resourceAttrs[k] = a[k];
869
+ }
728
870
  for (const k of ["traces.source.unreadable_rows", "traces.source.unreadable_row_kinds"]) {
729
871
  if (a[k] != null) resourceAttrs[k] = a[k];
730
872
  }
@@ -764,11 +906,11 @@ function serializeSpans(spans) {
764
906
  `;
765
907
  }
766
908
  async function writeOtlpFile(spans, outPath) {
767
- const { mkdtemp: mkdtemp8, writeFile: writeFile10 } = await import("fs/promises");
909
+ const { mkdtemp: mkdtemp8, writeFile: writeFile12 } = await import("fs/promises");
768
910
  const { tmpdir: tmpdir9 } = await import("os");
769
- const { join: join34 } = await import("path");
770
- const path = outPath ?? join34(await mkdtemp8(join34(tmpdir9(), "traces-")), "spans.otlp.jsonl");
771
- await writeFile10(path, serializeSpans(spans), "utf8");
911
+ const { join: join36 } = await import("path");
912
+ const path = outPath ?? join36(await mkdtemp8(join36(tmpdir9(), "traces-")), "spans.otlp.jsonl");
913
+ await writeFile12(path, serializeSpans(spans), "utf8");
772
914
  return path;
773
915
  }
774
916
 
@@ -796,95 +938,6 @@ function userPromptSpan(o) {
796
938
  });
797
939
  }
798
940
 
799
- // src/attributes.ts
800
- var ATTR2 = {
801
- /** = the trace/session id. Wire contract (dedup linkage). */
802
- SESSION_ID: "tangle.sessionId",
803
- /** "cli" marks a CLI upload so the platform can drop a re-upload of a session
804
- * that also streamed live. Wire contract. */
805
- INGEST_SOURCE: "tangle.ingest_source",
806
- HARNESS: "tangle.harness",
807
- CWD: "tangle.cwd",
808
- /** THE per-session grouping key the spine derives subjects from
809
- * (`deriveSubjectKey` reads `tangle.subject.key` first). Resolves to the git
810
- * remote (host/owner/repo) when readable, else the cwd path basename. */
811
- SUBJECT_KEY: "tangle.subject.key",
812
- /** Normalized git remote (e.g. `github.com/tangle-network/agent-dev-container`). */
813
- GIT_REPOSITORY: "git.repository",
814
- /** Current branch at conversion time. */
815
- GIT_BRANCH_NAME: "git.branch",
816
- /** HEAD short sha at conversion time. */
817
- GIT_COMMIT: "git.commit",
818
- /** How traces resolved the session cwd: recorded cwd, repaired cwd, or span path. */
819
- REPO_RESOLUTION_SOURCE: "traces.repo_resolution_source",
820
- GIT_BRANCH: "tangle.git_branch",
821
- HOST: "tangle.host",
822
- /** Basename of the session file. (Renamed from the ambiguous `tangle.source`,
823
- * which collided with `tangle.ingest_source`.) */
824
- SESSION_FILE: "tangle.session_file",
825
- UPLOADED_AT: "tangle.uploaded_at",
826
- UPLOADER: "tangle.uploader",
827
- REDACTION_VERSION: "redaction.version",
828
- REDACTION_COUNT: "redaction.count",
829
- /** Upload contract: bounded root summary plus location-only receipt child spans.
830
- * Receipt hashes are deliberate integrity evidence; raw malformed bytes never upload. */
831
- SESSION_INTEGRITY: "traces.session.integrity",
832
- CORRUPTION_COUNT: "traces.session.corruption_count",
833
- CORRUPTION_DIGEST: "traces.session.corruption_digest",
834
- CORRUPTION_RECEIPT_VERSION: "traces.session.corruption.receipt_version",
835
- CORRUPTION_RECEIPT_KIND: "traces.session.corruption.kind",
836
- CORRUPTION_SOURCE_PATH: "traces.session.corruption.source_path",
837
- CORRUPTION_LINE_NUMBER: "traces.session.corruption.line_number",
838
- CORRUPTION_BYTE_OFFSET: "traces.session.corruption.byte_offset",
839
- CORRUPTION_BYTE_LENGTH: "traces.session.corruption.byte_length",
840
- CORRUPTION_SHA256: "traces.session.corruption.sha256",
841
- RAW_SOURCE_RETENTION: "traces.session.raw_source_retention"
842
- };
843
- var INGEST_SOURCE_CLI = "cli";
844
- var DEFAULT_HARNESS = "claude-code";
845
- var SESSION_ID_ATTRIBUTE_KEYS = [ATTR2.SESSION_ID, "session.id", "traces.session.id"];
846
- function sessionIdFromAttributes(attributes2) {
847
- return SESSION_ID_ATTRIBUTE_KEYS.map((key) => attributes2[key]).find(
848
- (value) => typeof value === "string" && value.length > 0
849
- );
850
- }
851
- function indexSessionIdsByTrace(spans) {
852
- const candidatesByTrace = /* @__PURE__ */ new Map();
853
- for (const span2 of spans) {
854
- const candidates = candidatesByTrace.get(span2.trace_id) ?? /* @__PURE__ */ new Set();
855
- for (const key of SESSION_ID_ATTRIBUTE_KEYS) {
856
- const value = span2.attributes[key];
857
- if (typeof value === "string" && value.length > 0) candidates.add(value);
858
- }
859
- if (candidates.size > 0) candidatesByTrace.set(span2.trace_id, candidates);
860
- }
861
- const sessionByTrace = /* @__PURE__ */ new Map();
862
- const conflicts = [];
863
- for (const [traceId, candidates] of candidatesByTrace) {
864
- const sessionIds = [...candidates].sort();
865
- if (sessionIds.length === 1) {
866
- sessionByTrace.set(traceId, sessionIds[0]);
867
- } else {
868
- conflicts.push({ traceId, sessionIds });
869
- }
870
- }
871
- conflicts.sort((a, b) => a.traceId.localeCompare(b.traceId));
872
- return { sessionByTrace, conflicts };
873
- }
874
- function stampSessionIdentity(spans, fallbackSessionId) {
875
- const { sessionByTrace, conflicts } = indexSessionIdsByTrace(spans);
876
- const conflictingTraceIds = new Set(conflicts.map((conflict) => conflict.traceId));
877
- const traceIds = new Set(spans.map((span2) => span2.trace_id));
878
- const singleTraceId = traceIds.size === 1 ? traceIds.values().next().value : void 0;
879
- for (const span2 of spans) {
880
- if (conflictingTraceIds.has(span2.trace_id)) continue;
881
- const sessionId = sessionByTrace.get(span2.trace_id) ?? singleTraceId ?? fallbackSessionId;
882
- if (!sessionIdFromAttributes(span2.attributes)) {
883
- span2.attributes[ATTR2.SESSION_ID] = sessionId;
884
- }
885
- }
886
- }
887
-
888
941
  // src/chat-trajectory.ts
889
942
  import { createHash as createHash3 } from "crypto";
890
943
  function isChatTrajectoryMessage(value) {
@@ -1763,6 +1816,76 @@ function summarizeSpanExecution(spans, opts = {}) {
1763
1816
  });
1764
1817
  }
1765
1818
 
1819
+ // src/failure-followup.ts
1820
+ import { OPENINFERENCE_SPAN_KIND as OPENINFERENCE_SPAN_KIND2, TOOL_NAME as TOOL_NAME3 } from "@tangle-network/agent-eval/trace-attributes";
1821
+ function argKeyOf(span2) {
1822
+ const sha = span2.attributes["traces.input.sha256"];
1823
+ if (typeof sha === "string" && sha.length > 0) return `sha:${sha}`;
1824
+ const value = span2.attributes["input.value"];
1825
+ if (typeof value === "string" && value.length > 0) return `raw:${value}`;
1826
+ return null;
1827
+ }
1828
+ function schemeOf(key) {
1829
+ return key.slice(0, key.indexOf(":"));
1830
+ }
1831
+ function classifyFailureFollowUps(spans) {
1832
+ const calls = [];
1833
+ for (const s of spans) {
1834
+ if (s.attributes[OPENINFERENCE_SPAN_KIND2] !== "TOOL") continue;
1835
+ const toolName4 = s.attributes[TOOL_NAME3];
1836
+ if (typeof toolName4 !== "string" || toolName4.length === 0) continue;
1837
+ calls.push({ span: s, toolName: toolName4, argKey: argKeyOf(s) });
1838
+ }
1839
+ calls.sort(
1840
+ (a, b) => a.span.start_time.localeCompare(b.span.start_time) || (Number(a.span.attributes.step) || 0) - (Number(b.span.attributes.step) || 0)
1841
+ );
1842
+ const sequences = /* @__PURE__ */ new Map();
1843
+ for (const call of calls) {
1844
+ const key = `${call.span.trace_id}|${call.toolName}`;
1845
+ const seq = sequences.get(key);
1846
+ if (seq) seq.push(call);
1847
+ else sequences.set(key, [call]);
1848
+ }
1849
+ const items = [];
1850
+ const blindByTool = {};
1851
+ for (const seq of sequences.values()) {
1852
+ for (let i = 0; i < seq.length; i++) {
1853
+ const call = seq[i];
1854
+ if (call.span.status.code !== "ERROR") continue;
1855
+ if (call.span.attributes["traces.expected_blocking"] === true) continue;
1856
+ const followUp = seq[i + 1] ?? null;
1857
+ let kind;
1858
+ if (!followUp) {
1859
+ kind = "none";
1860
+ } else if (call.argKey !== null && followUp.argKey !== null && schemeOf(call.argKey) === schemeOf(followUp.argKey)) {
1861
+ kind = call.argKey === followUp.argKey ? "blind" : "adapted";
1862
+ } else {
1863
+ kind = "args-unknown";
1864
+ }
1865
+ if (kind === "blind") blindByTool[call.toolName] = (blindByTool[call.toolName] ?? 0) + 1;
1866
+ items.push({
1867
+ traceId: call.span.trace_id,
1868
+ toolName: call.toolName,
1869
+ failedSpanId: call.span.span_id,
1870
+ followUpSpanId: followUp?.span.span_id ?? null,
1871
+ kind,
1872
+ followUpSucceeded: followUp ? followUp.span.status.code !== "ERROR" : null
1873
+ });
1874
+ }
1875
+ }
1876
+ const followedItems = items.filter((item) => item.kind !== "none");
1877
+ return {
1878
+ failures: items.length,
1879
+ followed: followedItems.length,
1880
+ blind: items.filter((item) => item.kind === "blind").length,
1881
+ adapted: items.filter((item) => item.kind === "adapted").length,
1882
+ argsUnknown: items.filter((item) => item.kind === "args-unknown").length,
1883
+ followUpSucceeded: followedItems.filter((item) => item.followUpSucceeded === true).length,
1884
+ blindByTool,
1885
+ items
1886
+ };
1887
+ }
1888
+
1766
1889
  // src/runtime-store.ts
1767
1890
  import { InMemoryTraceStore } from "@tangle-network/agent-eval";
1768
1891
  var KIND = {
@@ -1851,7 +1974,7 @@ async function runPipelines(spans, opts = {}) {
1851
1974
  failureClusterView(store),
1852
1975
  Promise.all(runIds.map((runId) => computeToolUseMetrics(store, runId)))
1853
1976
  ]);
1854
- return { stuckLoops, failureClusters, toolUse };
1977
+ return { stuckLoops, failureClusters, toolUse, failureFollowUps: classifyFailureFollowUps(spans) };
1855
1978
  }
1856
1979
 
1857
1980
  // src/integrity.ts
@@ -1907,10 +2030,10 @@ function sessionJsonlOptions(ref, options = {}) {
1907
2030
  function sessionIntegrityAttributes(ref) {
1908
2031
  if (!ref.integrity) return {};
1909
2032
  return {
1910
- [ATTR2.SESSION_INTEGRITY]: ref.integrity.status,
1911
- [ATTR2.CORRUPTION_COUNT]: ref.integrity.corruptions.length,
1912
- [ATTR2.CORRUPTION_DIGEST]: corruptionDigest(ref.integrity.corruptions),
1913
- [ATTR2.RAW_SOURCE_RETENTION]: "local_source_only"
2033
+ [ATTR.SESSION_INTEGRITY]: ref.integrity.status,
2034
+ [ATTR.CORRUPTION_COUNT]: ref.integrity.corruptions.length,
2035
+ [ATTR.CORRUPTION_DIGEST]: corruptionDigest(ref.integrity.corruptions),
2036
+ [ATTR.RAW_SOURCE_RETENTION]: "local_source_only"
1914
2037
  };
1915
2038
  }
1916
2039
  function corruptionDigest(receipts) {
@@ -1951,15 +2074,15 @@ function stampSessionIntegrity(ref, spans) {
1951
2074
  status: "ERROR",
1952
2075
  service: typeof parent.attributes["service.name"] === "string" ? parent.attributes["service.name"] : ref.harness,
1953
2076
  extra: {
1954
- [ATTR2.SESSION_INTEGRITY]: ref.integrity.status,
1955
- [ATTR2.CORRUPTION_RECEIPT_VERSION]: receipt.receiptVersion,
1956
- [ATTR2.CORRUPTION_RECEIPT_KIND]: receipt.kind,
1957
- [ATTR2.CORRUPTION_SOURCE_PATH]: receipt.sourcePath,
1958
- [ATTR2.CORRUPTION_LINE_NUMBER]: receipt.lineNumber,
1959
- [ATTR2.CORRUPTION_BYTE_OFFSET]: receipt.byteOffset,
1960
- [ATTR2.CORRUPTION_BYTE_LENGTH]: receipt.byteLength,
1961
- [ATTR2.CORRUPTION_SHA256]: receipt.sha256,
1962
- [ATTR2.RAW_SOURCE_RETENTION]: "local_source_only"
2077
+ [ATTR.SESSION_INTEGRITY]: ref.integrity.status,
2078
+ [ATTR.CORRUPTION_RECEIPT_VERSION]: receipt.receiptVersion,
2079
+ [ATTR.CORRUPTION_RECEIPT_KIND]: receipt.kind,
2080
+ [ATTR.CORRUPTION_SOURCE_PATH]: receipt.sourcePath,
2081
+ [ATTR.CORRUPTION_LINE_NUMBER]: receipt.lineNumber,
2082
+ [ATTR.CORRUPTION_BYTE_OFFSET]: receipt.byteOffset,
2083
+ [ATTR.CORRUPTION_BYTE_LENGTH]: receipt.byteLength,
2084
+ [ATTR.CORRUPTION_SHA256]: receipt.sha256,
2085
+ [ATTR.RAW_SOURCE_RETENTION]: "local_source_only"
1963
2086
  }
1964
2087
  }));
1965
2088
  existingSpanIds.add(spanId);
@@ -6003,7 +6126,7 @@ function selectAdapters(opts) {
6003
6126
  }
6004
6127
 
6005
6128
  // src/repo.ts
6006
- import { stat as stat13 } from "fs/promises";
6129
+ import { realpath as realpath3, stat as stat13 } from "fs/promises";
6007
6130
  import { dirname as dirname6, isAbsolute as isAbsolute4, join as join17, normalize, parse as parsePath } from "path";
6008
6131
  function normalizeRemote(url) {
6009
6132
  const raw = url.trim();
@@ -6082,6 +6205,13 @@ function uniqNormalized(paths) {
6082
6205
  function parseWorktreePorcelain(output) {
6083
6206
  return output.split("\n").filter((line) => line.startsWith("worktree ")).map((line) => line.slice("worktree ".length).trim()).filter(Boolean);
6084
6207
  }
6208
+ async function realpathOrNull(path) {
6209
+ try {
6210
+ return await realpath3(path);
6211
+ } catch {
6212
+ return null;
6213
+ }
6214
+ }
6085
6215
  async function equivalentGitCwds(cwd) {
6086
6216
  if (!cwd) return [];
6087
6217
  const selected = normalize(cwd);
@@ -6089,7 +6219,20 @@ async function equivalentGitCwds(cwd) {
6089
6219
  const top = await gitOutput(selected, ["rev-parse", "--show-toplevel"]);
6090
6220
  if (!top) return [selected];
6091
6221
  const worktrees = await gitOutput(top, ["worktree", "list", "--porcelain"]);
6092
- return uniqNormalized([selected, top, ...worktrees ? parseWorktreePorcelain(worktrees) : []]);
6222
+ const candidates = [selected, top, ...worktrees ? parseWorktreePorcelain(worktrees) : []];
6223
+ const expanded = [];
6224
+ for (const candidate of candidates) {
6225
+ expanded.push(candidate);
6226
+ const resolved = await realpathOrNull(candidate);
6227
+ if (resolved) expanded.push(resolved);
6228
+ }
6229
+ return uniqNormalized(expanded);
6230
+ }
6231
+ async function cwdMatchesSelectionResolved(cwd, selections) {
6232
+ if (cwdMatchesSelection(cwd, selections)) return true;
6233
+ if (!cwd || !isAbsolute4(cwd)) return false;
6234
+ const resolved = await realpathOrNull(normalize(cwd));
6235
+ return resolved !== null && resolved !== normalize(cwd) && cwdMatchesSelection(resolved, selections);
6093
6236
  }
6094
6237
  function cwdMatchesSelection(cwd, selections) {
6095
6238
  if (!cwd) return false;
@@ -6236,7 +6379,7 @@ function extractAbsolutePaths(spans) {
6236
6379
  }
6237
6380
  async function resolveRepoAttrs(cwd) {
6238
6381
  if (!cwd) return {};
6239
- const attrs = { [ATTR2.CWD]: cwd };
6382
+ const attrs = { [ATTR.CWD]: cwd };
6240
6383
  const basename16 = (() => {
6241
6384
  const trimmed = cwd.replace(/\/+$/g, "");
6242
6385
  const idx = trimmed.lastIndexOf("/");
@@ -6249,17 +6392,17 @@ async function resolveRepoAttrs(cwd) {
6249
6392
  const { remote, branch, commit } = await readGit(gitRoot);
6250
6393
  const normalized = remote ? normalizeRemote(remote) : null;
6251
6394
  if (normalized) {
6252
- attrs[ATTR2.SUBJECT_KEY] = normalized;
6253
- attrs[ATTR2.GIT_REPOSITORY] = normalized;
6254
- if (branch) attrs[ATTR2.GIT_BRANCH_NAME] = branch;
6255
- if (commit) attrs[ATTR2.GIT_COMMIT] = commit;
6395
+ attrs[ATTR.SUBJECT_KEY] = normalized;
6396
+ attrs[ATTR.GIT_REPOSITORY] = normalized;
6397
+ if (branch) attrs[ATTR.GIT_BRANCH_NAME] = branch;
6398
+ if (commit) attrs[ATTR.GIT_COMMIT] = commit;
6256
6399
  return attrs;
6257
6400
  }
6258
- if (branch) attrs[ATTR2.GIT_BRANCH_NAME] = branch;
6259
- if (commit) attrs[ATTR2.GIT_COMMIT] = commit;
6401
+ if (branch) attrs[ATTR.GIT_BRANCH_NAME] = branch;
6402
+ if (commit) attrs[ATTR.GIT_COMMIT] = commit;
6260
6403
  } catch {
6261
6404
  }
6262
- attrs[ATTR2.SUBJECT_KEY] = basename16;
6405
+ attrs[ATTR.SUBJECT_KEY] = basename16;
6263
6406
  return attrs;
6264
6407
  }
6265
6408
  async function repoCandidateForPath(path) {
@@ -6290,12 +6433,12 @@ async function resolveSessionRepoAttrs(cwd, spans) {
6290
6433
  resolved.push({ ...candidate, attrs });
6291
6434
  }
6292
6435
  const byScore = (a, b) => b.score - a.score || a.order - b.order;
6293
- const withGit = resolved.filter((candidate) => candidate.attrs[ATTR2.GIT_REPOSITORY]).sort(byScore);
6294
- const selected = withGit[0] ?? resolved.filter((candidate) => candidate.attrs[ATTR2.SUBJECT_KEY]).sort(byScore)[0];
6436
+ const withGit = resolved.filter((candidate) => candidate.attrs[ATTR.GIT_REPOSITORY]).sort(byScore);
6437
+ const selected = withGit[0] ?? resolved.filter((candidate) => candidate.attrs[ATTR.SUBJECT_KEY]).sort(byScore)[0];
6295
6438
  if (!selected) return { attrs: {}, cwd: null, source: "none" };
6296
6439
  return {
6297
- attrs: { ...selected.attrs, [ATTR2.REPO_RESOLUTION_SOURCE]: selected.source },
6298
- cwd: selected.attrs[ATTR2.CWD] ?? selected.cwd,
6440
+ attrs: { ...selected.attrs, [ATTR.REPO_RESOLUTION_SOURCE]: selected.source },
6441
+ cwd: selected.attrs[ATTR.CWD] ?? selected.cwd,
6299
6442
  source: selected.source
6300
6443
  };
6301
6444
  }
@@ -6323,9 +6466,9 @@ async function resolutionForWorkdirEvidence(evidence3, cache) {
6323
6466
  return null;
6324
6467
  }
6325
6468
  const attrs = await resolveRepoAttrs(cwd);
6326
- const resolved = attrs[ATTR2.SUBJECT_KEY] ? {
6327
- attrs: { ...attrs, [ATTR2.REPO_RESOLUTION_SOURCE]: evidence3.source },
6328
- cwd: attrs[ATTR2.CWD] ?? cwd,
6469
+ const resolved = attrs[ATTR.SUBJECT_KEY] ? {
6470
+ attrs: { ...attrs, [ATTR.REPO_RESOLUTION_SOURCE]: evidence3.source },
6471
+ cwd: attrs[ATTR.CWD] ?? cwd,
6329
6472
  source: evidence3.source
6330
6473
  } : null;
6331
6474
  cache.set(evidence3.path, resolved);
@@ -6348,7 +6491,7 @@ async function resolveSpanWorkdirRepoAttrs(span2, cache) {
6348
6491
  }
6349
6492
  const byScore = (a, b) => b.score - a.score || a.order - b.order;
6350
6493
  const resolved = [...candidates.values()];
6351
- const selected = resolved.filter((candidate) => candidate.repo.attrs[ATTR2.GIT_REPOSITORY]).sort(byScore)[0] ?? resolved.filter((candidate) => candidate.repo.attrs[ATTR2.SUBJECT_KEY]).sort(byScore)[0];
6494
+ const selected = resolved.filter((candidate) => candidate.repo.attrs[ATTR.GIT_REPOSITORY]).sort(byScore)[0] ?? resolved.filter((candidate) => candidate.repo.attrs[ATTR.SUBJECT_KEY]).sort(byScore)[0];
6352
6495
  if (!selected) return null;
6353
6496
  return selected.repo;
6354
6497
  }
@@ -6364,7 +6507,7 @@ async function stampSpanWorkdirRepoAttrs(spans) {
6364
6507
  async function awaitWithAbort(operation, signal) {
6365
6508
  if (!signal) return operation;
6366
6509
  signal.throwIfAborted();
6367
- return new Promise((resolve8, reject) => {
6510
+ return new Promise((resolve10, reject) => {
6368
6511
  const stop = () => {
6369
6512
  signal.removeEventListener("abort", stop);
6370
6513
  reject(signal.reason);
@@ -6373,7 +6516,7 @@ async function awaitWithAbort(operation, signal) {
6373
6516
  operation.then(
6374
6517
  (value) => {
6375
6518
  signal.removeEventListener("abort", stop);
6376
- resolve8(value);
6519
+ resolve10(value);
6377
6520
  },
6378
6521
  (error) => {
6379
6522
  signal.removeEventListener("abort", stop);
@@ -6392,6 +6535,8 @@ async function parseSession(adapter, ref, options = {}) {
6392
6535
  if (spans.length === 0) throw new EmptySessionError(ref.path);
6393
6536
  stampSessionIntegrity(ref, spans);
6394
6537
  stampSessionIdentity(spans, ref.sessionId);
6538
+ stampEnvironmentAttrs(spans, ref.environment);
6539
+ if (ref.cwd === null && ref.environment?.cwd) ref.cwd = ref.environment.cwd;
6395
6540
  const repo = await resolveSessionRepoAttrs(ref.cwd, spans);
6396
6541
  if (repo.cwd) ref.cwd = repo.cwd;
6397
6542
  stampRepoAttrs(spans, repo.attrs);
@@ -6414,8 +6559,15 @@ async function locateSessions(adapter, opts = {}) {
6414
6559
  const cwdSelections = await equivalentGitCwds(opts.cwd);
6415
6560
  const refs = await adapter.locate({ sinceMs: opts.sinceMs });
6416
6561
  const byKey = /* @__PURE__ */ new Map();
6562
+ const resolvedMatch = /* @__PURE__ */ new Map();
6417
6563
  for (const ref of refs) {
6418
- if (!cwdMatchesSelection(ref.cwd, cwdSelections) && !(ref.cwd === null && ref.integrity)) continue;
6564
+ let matched = cwdMatchesSelection(ref.cwd, cwdSelections);
6565
+ if (!matched && ref.cwd !== null) {
6566
+ const cached = resolvedMatch.get(ref.cwd);
6567
+ matched = cached ?? await cwdMatchesSelectionResolved(ref.cwd, cwdSelections);
6568
+ if (cached === void 0) resolvedMatch.set(ref.cwd, matched);
6569
+ }
6570
+ if (!matched && !(ref.cwd === null && ref.integrity)) continue;
6419
6571
  byKey.set(refKey(ref), ref);
6420
6572
  }
6421
6573
  return [...byKey.values()].sort((a, b) => b.mtimeMs - a.mtimeMs);
@@ -6550,25 +6702,26 @@ import { mkdtemp as mkdtemp2, writeFile as writeFile3 } from "fs/promises";
6550
6702
  import { tmpdir as tmpdir2 } from "os";
6551
6703
  import { join as join18 } from "path";
6552
6704
  import {
6553
- OPENINFERENCE_SPAN_KIND as OPENINFERENCE_SPAN_KIND2,
6554
- TOOL_NAME as TOOL_NAME3
6705
+ OPENINFERENCE_SPAN_KIND as OPENINFERENCE_SPAN_KIND3,
6706
+ TOOL_NAME as TOOL_NAME4
6555
6707
  } from "@tangle-network/agent-eval/trace-attributes";
6708
+ var POLICY_EVIDENCE_NOTE = "This is normalized coding-agent session evidence for downstream policy mining; it is not an eval campaign cell.";
6556
6709
  function stringAttr(span2, key) {
6557
6710
  const value = span2.attributes[key];
6558
6711
  return typeof value === "string" && value.length > 0 ? value : void 0;
6559
6712
  }
6560
6713
  function spanKind(span2) {
6561
- return stringAttr(span2, OPENINFERENCE_SPAN_KIND2);
6714
+ return stringAttr(span2, OPENINFERENCE_SPAN_KIND3);
6562
6715
  }
6563
6716
  function repoFromSpans(spans) {
6564
6717
  const attrs = {};
6565
6718
  for (const span2 of spans) {
6566
- attrs.subjectKey ??= stringAttr(span2, ATTR2.SUBJECT_KEY);
6567
- attrs.repository ??= stringAttr(span2, ATTR2.GIT_REPOSITORY);
6568
- attrs.branch ??= stringAttr(span2, ATTR2.GIT_BRANCH_NAME);
6569
- attrs.commit ??= stringAttr(span2, ATTR2.GIT_COMMIT);
6570
- attrs.cwd ??= stringAttr(span2, ATTR2.CWD);
6571
- attrs.resolutionSource ??= stringAttr(span2, ATTR2.REPO_RESOLUTION_SOURCE);
6719
+ attrs.subjectKey ??= stringAttr(span2, ATTR.SUBJECT_KEY);
6720
+ attrs.repository ??= stringAttr(span2, ATTR.GIT_REPOSITORY);
6721
+ attrs.branch ??= stringAttr(span2, ATTR.GIT_BRANCH_NAME);
6722
+ attrs.commit ??= stringAttr(span2, ATTR.GIT_COMMIT);
6723
+ attrs.cwd ??= stringAttr(span2, ATTR.CWD);
6724
+ attrs.resolutionSource ??= stringAttr(span2, ATTR.REPO_RESOLUTION_SOURCE);
6572
6725
  if (attrs.subjectKey && attrs.repository && attrs.branch && attrs.commit && attrs.cwd && attrs.resolutionSource) break;
6573
6726
  }
6574
6727
  return attrs;
@@ -6584,7 +6737,7 @@ function summarizeTools(spans) {
6584
6737
  const byTool = /* @__PURE__ */ new Map();
6585
6738
  for (const span2 of spans) {
6586
6739
  if (spanKind(span2) !== "TOOL") continue;
6587
- const name = stringAttr(span2, TOOL_NAME3) ?? span2.name.replace(/^tool\./, "");
6740
+ const name = stringAttr(span2, TOOL_NAME4) ?? span2.name.replace(/^tool\./, "");
6588
6741
  const current = byTool.get(name) ?? { calls: 0, errors: 0 };
6589
6742
  current.calls += 1;
6590
6743
  if (span2.status.code === "ERROR") current.errors += 1;
@@ -6657,7 +6810,7 @@ async function buildPolicyEvidenceRecord(ref, spans, opts = {}) {
6657
6810
  ...opts.otlpPath ? { otlpPath: opts.otlpPath } : {},
6658
6811
  ...opts.sourceSha256 ? { sourceSha256: opts.sourceSha256 } : {},
6659
6812
  notCampaignCell: true,
6660
- note: "This is normalized coding-agent session evidence for downstream policy mining; it is not an eval campaign cell."
6813
+ note: POLICY_EVIDENCE_NOTE
6661
6814
  }
6662
6815
  };
6663
6816
  }
@@ -7181,11 +7334,11 @@ function parseCorpusFlag(value) {
7181
7334
  if (eq <= 0) throw new Error(`--corpus must be name=<labels>::<prepared>, got: ${value}`);
7182
7335
  const name = value.slice(0, eq);
7183
7336
  const rest = value.slice(eq + 1);
7184
- const sep2 = rest.indexOf("::");
7185
- if (sep2 <= 0 || sep2 === rest.length - 2) {
7337
+ const sep3 = rest.indexOf("::");
7338
+ if (sep3 <= 0 || sep3 === rest.length - 2) {
7186
7339
  throw new Error(`--corpus must be name=<labels>::<prepared>, got: ${value}`);
7187
7340
  }
7188
- return { name, labelsPath: rest.slice(0, sep2), preparedDir: rest.slice(sep2 + 2) };
7341
+ return { name, labelsPath: rest.slice(0, sep3), preparedDir: rest.slice(sep3 + 2) };
7189
7342
  }
7190
7343
  var SUBMIT_ACTION_SIGNATURE = "COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT";
7191
7344
  function isSubmitAction(action) {
@@ -7211,7 +7364,7 @@ function goldIncorrectSteps(entry) {
7211
7364
  function findRawTrajFiles(sweRawDir) {
7212
7365
  if (!existsSync2(sweRawDir)) return [];
7213
7366
  const entries = readdirSync2(sweRawDir, { recursive: true, encoding: "utf8" });
7214
- return entries.filter((relative5) => relative5.endsWith(".traj.json")).map((relative5) => join20(sweRawDir, relative5)).sort();
7367
+ return entries.filter((relative6) => relative6.endsWith(".traj.json")).map((relative6) => join20(sweRawDir, relative6)).sort();
7215
7368
  }
7216
7369
  function cwdFromPwdObservation(steps) {
7217
7370
  for (const step of steps) {
@@ -8376,9 +8529,9 @@ function renderBatchReport(report) {
8376
8529
  lines.push("");
8377
8530
  lines.push("| corpus | trajectory | image | error |");
8378
8531
  lines.push("| --- | --- | --- | --- |");
8379
- for (const failure of report.pullFailures) {
8532
+ for (const failure2 of report.pullFailures) {
8380
8533
  lines.push(
8381
- `| ${failure.corpus} | ${failure.trajId} | \`${failure.image}\` | ${failure.error.replaceAll("|", "\\|")} |`
8534
+ `| ${failure2.corpus} | ${failure2.trajId} | \`${failure2.image}\` | ${failure2.error.replaceAll("|", "\\|")} |`
8382
8535
  );
8383
8536
  }
8384
8537
  }
@@ -9444,13 +9597,13 @@ function terminateWindowsProcessTree(child) {
9444
9597
  forceKill(child);
9445
9598
  return Promise.resolve();
9446
9599
  }
9447
- return new Promise((resolve8) => {
9600
+ return new Promise((resolve10) => {
9448
9601
  let finished = false;
9449
9602
  const finish = () => {
9450
9603
  if (finished) return;
9451
9604
  finished = true;
9452
9605
  forceKill(child);
9453
- resolve8();
9606
+ resolve10();
9454
9607
  };
9455
9608
  let killer;
9456
9609
  try {
@@ -9477,7 +9630,7 @@ function signalProcess(pid, signal) {
9477
9630
  }
9478
9631
  }
9479
9632
  function readProcessTableWithPs() {
9480
- return new Promise((resolve8, reject) => {
9633
+ return new Promise((resolve10, reject) => {
9481
9634
  let child;
9482
9635
  try {
9483
9636
  child = spawn("ps", ["-A", "-o", "pid=", "-o", "ppid="], {
@@ -9531,7 +9684,7 @@ function readProcessTableWithPs() {
9531
9684
  reject(new Error("ps: returned no process records"));
9532
9685
  return;
9533
9686
  }
9534
- resolve8(rows);
9687
+ resolve10(rows);
9535
9688
  });
9536
9689
  });
9537
9690
  });
@@ -9541,10 +9694,10 @@ async function readLinuxProcProcessTable() {
9541
9694
  const rows = await Promise.all(
9542
9695
  entries.filter((entry) => entry.isDirectory() && /^\d+$/.test(entry.name)).map(async (entry) => {
9543
9696
  try {
9544
- const stat17 = await readFile7(`/proc/${entry.name}/stat`, "utf8");
9545
- const commandEnd = stat17.lastIndexOf(")");
9697
+ const stat18 = await readFile7(`/proc/${entry.name}/stat`, "utf8");
9698
+ const commandEnd = stat18.lastIndexOf(")");
9546
9699
  if (commandEnd < 0) return void 0;
9547
- const fields = stat17.slice(commandEnd + 1).trim().split(/\s+/);
9700
+ const fields = stat18.slice(commandEnd + 1).trim().split(/\s+/);
9548
9701
  const pid = Number(entry.name);
9549
9702
  const parentPid = Number(fields[1]);
9550
9703
  if (!Number.isSafeInteger(pid) || pid < 1 || !Number.isSafeInteger(parentPid) || parentPid < 0) {
@@ -9606,7 +9759,7 @@ async function terminatePosixProcessTree(child) {
9606
9759
  }
9607
9760
  const rootPid = child.pid;
9608
9761
  const descendants = /* @__PURE__ */ new Map();
9609
- let failure;
9762
+ let failure2;
9610
9763
  try {
9611
9764
  signalProcess(-rootPid, "SIGSTOP");
9612
9765
  signalProcess(rootPid, "SIGSTOP");
@@ -9623,7 +9776,7 @@ async function terminatePosixProcessTree(child) {
9623
9776
  previous = signature;
9624
9777
  }
9625
9778
  } catch (error) {
9626
- failure = error;
9779
+ failure2 = error;
9627
9780
  } finally {
9628
9781
  for (const processEntry of [...descendants.values()].sort(
9629
9782
  (left, right) => right.depth - left.depth
@@ -9631,18 +9784,18 @@ async function terminatePosixProcessTree(child) {
9631
9784
  try {
9632
9785
  signalProcess(processEntry.pid, "SIGKILL");
9633
9786
  } catch (error) {
9634
- failure ??= error;
9787
+ failure2 ??= error;
9635
9788
  }
9636
9789
  }
9637
9790
  try {
9638
9791
  signalProcess(-rootPid, "SIGKILL");
9639
9792
  } catch (error) {
9640
- failure ??= error;
9793
+ failure2 ??= error;
9641
9794
  }
9642
9795
  forceKill(child);
9643
9796
  }
9644
- if (failure) {
9645
- throw failure instanceof Error ? failure : new Error(String(failure));
9797
+ if (failure2) {
9798
+ throw failure2 instanceof Error ? failure2 : new Error(String(failure2));
9646
9799
  }
9647
9800
  }
9648
9801
  function terminateProcessTree(child) {
@@ -9666,7 +9819,7 @@ function runCommand(command, args = [], opts = {}) {
9666
9819
  throw new RangeError("maxBuffer must be a positive safe integer");
9667
9820
  }
9668
9821
  if (opts.signal?.aborted) return Promise.reject(abortError(opts.signal));
9669
- return new Promise((resolve8, reject) => {
9822
+ return new Promise((resolve10, reject) => {
9670
9823
  let child;
9671
9824
  try {
9672
9825
  child = spawn(command, [...args], { detached: process.platform !== "win32" });
@@ -9698,7 +9851,7 @@ function runCommand(command, args = [], opts = {}) {
9698
9851
  } else if (spawnError) {
9699
9852
  reject(spawnError);
9700
9853
  } else {
9701
- resolve8({ stdout: out, stderr: err, code: closeCode });
9854
+ resolve10({ stdout: out, stderr: err, code: closeCode });
9702
9855
  }
9703
9856
  };
9704
9857
  function requestTermination(error) {
@@ -10613,44 +10766,463 @@ function assertPositiveInteger(name, value) {
10613
10766
  }
10614
10767
  }
10615
10768
 
10616
- // src/redact.ts
10617
- import { DEFAULT_REDACTION_RULES, redactValue } from "@tangle-network/agent-eval/traces";
10618
- var CODING_REDACTION_RULES = [
10619
- { id: "github-token", pattern: /\b(?:gh[pousr]_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{22,})\b/g },
10620
- { id: "aws-akid", pattern: /\bAKIA[0-9A-Z]{16}\b/g },
10621
- { id: "slack-token", pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
10622
- { id: "jwt", pattern: /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g },
10623
- { id: "bearer", pattern: /\bBearer\s+[A-Za-z0-9._-]{16,}/gi },
10624
- {
10625
- id: "private-key",
10626
- pattern: /-----BEGIN(?:[A-Z ]+)?PRIVATE KEY-----[\s\S]*?-----END(?:[A-Z ]+)?PRIVATE KEY-----/g
10627
- },
10628
- // Match the whole identifier because underscores prevent a word boundary before a secret suffix.
10629
- {
10630
- id: "assigned-secret",
10631
- pattern: /(?<![A-Za-z0-9_-])(?:[A-Za-z_][A-Za-z0-9_-]*)?(?:api[_-]?key|secret|token|password|passwd|access[_-]?token|client[_-]?secret)(?![A-Za-z0-9_-])\+?=(?:\$\((?:\\.|[^\\\r\n])*\)|\$\{(?:\\.|[^}\\\r\n])*\}|`(?:\\.|[^`\\\r\n])*`|\$?'(?:\\.|[^'\\\r\n])*'|\$?"(?:\\.|[^"\\\r\n])*"|\\[^\r\n]|[^\s;&|<>()"'`\\\r\n])+/gi
10632
- },
10633
- // Config-file secrets (YAML/INI line keys, JSON quoted keys) — the coverage the shell
10634
- // rule above deliberately gave up to spare prose ("Narrative token: x") and type
10635
- // annotations ("(token: string)"). The line anchor is the discriminator: config keys
10636
- // start their line, prose colons never do; JSON keys are safe anywhere because both
10637
- // sides are quoted. Identifiers must END in a secret word, same as the shell rule.
10638
- {
10639
- id: "config-secret",
10640
- pattern: /(?:^[ \t]*["']?(?:[A-Za-z_][A-Za-z0-9_-]*)?(?:api[_-]?key|secret|token|password|passwd|access[_-]?token|client[_-]?secret)(?![A-Za-z0-9_-])["']?[ \t]*[:=][ \t]*|["'](?:[A-Za-z_][A-Za-z0-9_-]*)?(?:api[_-]?key|secret|token|password|passwd|access[_-]?token|client[_-]?secret)["']\s*:\s*)["']?[A-Za-z0-9._\-]{12,}["']?/gim
10641
- },
10642
- // Credentials embedded in URLs — common when a prompt pastes a curl/clone line.
10643
- { id: "url-userinfo", pattern: /\b[a-z][a-z0-9+.-]*:\/\/[^\s/:@]+:[^\s/@]+@/gi },
10644
- {
10645
- id: "url-secret-param",
10646
- pattern: /[?&](?:access[_-]?token|api[_-]?key|apikey|auth|token|secret|password|sig|signature)=[^&\s"'#)]+/gi
10769
+ // src/analyst-engine-prime.ts
10770
+ import { createHash as createHash10 } from "crypto";
10771
+ import { request as httpRequest } from "http";
10772
+ import { request as httpsRequest } from "https";
10773
+ var DEFAULT_BRIDGE_URL = "http://localhost:4181";
10774
+ var DEFAULT_MODEL = "prime/zai/glm-5.2";
10775
+ var DEFAULT_TIMEOUT_MS = 12e5;
10776
+ var DEFAULT_MAX_INLINE_CHARS = 36e4;
10777
+ var DEFAULT_PER_ATTRIBUTE_CHAR_CAP = 1200;
10778
+ var DEFAULT_QUESTION = "Diagnose this trajectory: identify concrete agent failures, wasted work, and unsupported claims.";
10779
+ var MAX_FINDINGS = 10;
10780
+ var MAX_SPAN_IDS_PER_FINDING = 8;
10781
+ var SEVERITIES = /* @__PURE__ */ new Set(["critical", "high", "medium", "low", "info"]);
10782
+ var ANALYST_ID = "prime";
10783
+ function positiveIntegerEnv(name) {
10784
+ const raw = process.env[name];
10785
+ if (raw === void 0 || raw === "") return void 0;
10786
+ const value = Number(raw);
10787
+ if (!Number.isSafeInteger(value) || value < 1) {
10788
+ throw new TypeError(`${name} must be a positive integer, got '${raw}'`);
10647
10789
  }
10648
- ];
10649
- var TRACES_REDACTION_RULES = [...DEFAULT_REDACTION_RULES, ...CODING_REDACTION_RULES];
10650
- function redactSpans(spans, rules = TRACES_REDACTION_RULES) {
10651
- const report = { redactionCount: 0, byRule: {} };
10652
- const out = spans.map((s) => {
10653
- const attributes2 = redactValue(s.attributes, rules, report).value;
10790
+ return value;
10791
+ }
10792
+ function finiteOrNull(value) {
10793
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
10794
+ }
10795
+ function normalizeUsage(raw) {
10796
+ if (!raw || typeof raw !== "object") return { calls: null, tokens: null, estimated: false };
10797
+ const usage = raw;
10798
+ const input = finiteOrNull(usage.prompt_tokens);
10799
+ const output = finiteOrNull(usage.completion_tokens);
10800
+ return {
10801
+ calls: finiteOrNull(usage.model_requests),
10802
+ tokens: input === null && output === null ? null : { input, output },
10803
+ estimated: usage.estimated === true
10804
+ };
10805
+ }
10806
+ function mergeUsage(a, b) {
10807
+ const calls = a.calls !== null && b.calls !== null ? a.calls + b.calls : null;
10808
+ const tokens3 = a.tokens !== null && b.tokens !== null && a.tokens.input !== null && b.tokens.input !== null && a.tokens.output !== null && b.tokens.output !== null ? { input: a.tokens.input + b.tokens.input, output: a.tokens.output + b.tokens.output } : null;
10809
+ return { calls, tokens: tokens3, estimated: a.estimated || b.estimated };
10810
+ }
10811
+ function renderUsage(usage) {
10812
+ const calls = usage.calls === null ? "uncaptured" : String(usage.calls);
10813
+ const input = usage.tokens?.input ?? "uncaptured";
10814
+ const output = usage.tokens?.output ?? "uncaptured";
10815
+ const estimated = usage.estimated ? " (bridge-estimated)" : "";
10816
+ return `usage: calls=${calls} input_tokens=${input} output_tokens=${output}${estimated}; cost=uncaptured (no pricing table)`;
10817
+ }
10818
+ function capAttributeValue(value, cap) {
10819
+ const rendered = JSON.stringify(value);
10820
+ if (rendered === void 0 || rendered.length <= cap) return value;
10821
+ if (typeof value === "string") return `${value.slice(0, cap)}\u2026[truncated ${value.length - cap} chars]`;
10822
+ return `[omitted non-string attribute: ${rendered.length} JSON chars]`;
10823
+ }
10824
+ function capRowAttributes(row, cap) {
10825
+ const attributes2 = row.attributes;
10826
+ if (!attributes2 || typeof attributes2 !== "object" || Array.isArray(attributes2)) return row;
10827
+ const capped = {};
10828
+ for (const [key, value] of Object.entries(attributes2)) {
10829
+ capped[key] = capAttributeValue(value, cap);
10830
+ }
10831
+ return { ...row, attributes: capped };
10832
+ }
10833
+ async function projectArtifact(otlpPath, maxInlineChars, perAttributeCharCap, signal) {
10834
+ const rows = [];
10835
+ const spanTraces = /* @__PURE__ */ new Map();
10836
+ let index = 0;
10837
+ for await (const value of readJsonl(otlpPath, { signal })) {
10838
+ const label = `${otlpPath}:${index + 1}`;
10839
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
10840
+ throw new TypeError(`${label} must contain an object`);
10841
+ }
10842
+ const row = value;
10843
+ if (typeof row.trace_id !== "string" || row.trace_id.length === 0) {
10844
+ throw new TypeError(`${label}.trace_id must be a non-empty string`);
10845
+ }
10846
+ if (typeof row.span_id !== "string" || row.span_id.length === 0) {
10847
+ throw new TypeError(`${label}.span_id must be a non-empty string`);
10848
+ }
10849
+ rows.push(row);
10850
+ const traces = spanTraces.get(row.span_id) ?? /* @__PURE__ */ new Set();
10851
+ traces.add(row.trace_id);
10852
+ spanTraces.set(row.span_id, traces);
10853
+ index += 1;
10854
+ }
10855
+ if (rows.length === 0) throw new Error(`${otlpPath} contains no spans`);
10856
+ const traceCount = new Set(rows.map((row) => row.trace_id)).size;
10857
+ let projected = rows;
10858
+ let rendered = JSON.stringify(projected);
10859
+ const delivery = {
10860
+ mode: "inline-json",
10861
+ perAttributeCharCap: null,
10862
+ renderedChars: rendered.length
10863
+ };
10864
+ if (rendered.length > maxInlineChars) {
10865
+ projected = rows.map((row) => capRowAttributes(row, perAttributeCharCap));
10866
+ rendered = JSON.stringify(projected);
10867
+ delivery.perAttributeCharCap = perAttributeCharCap;
10868
+ delivery.renderedChars = rendered.length;
10869
+ }
10870
+ if (rendered.length > maxInlineChars) {
10871
+ throw new Error(
10872
+ `trajectory renders to ${rendered.length} chars even at per-attribute cap ${perAttributeCharCap} (budget ${maxInlineChars}); inline delivery impossible`
10873
+ );
10874
+ }
10875
+ return { rows: projected, rendered, delivery, traceCount, spanTraces };
10876
+ }
10877
+ var OUTPUT_CONTRACT_LINES = [
10878
+ "OUTPUT CONTRACT (you have no trace tools and no REPL):",
10879
+ "You are a one-shot analyst. Every fact you need is in the TRAJECTORY JSON below.",
10880
+ "Do not run shell commands, do not read or write files, do not use any tools.",
10881
+ "Reply with EXACTLY one fenced ```json code block and no other fenced block. The JSON object has exactly two fields:",
10882
+ ' "answer": string \u2014 ONE short sentence (max 300 chars) summarizing your verdict.',
10883
+ ' "findings": array (possibly empty) of finding rows, each exactly:',
10884
+ ' {"span_ids": [string, ...],',
10885
+ ' "severity": "critical"|"high"|"medium"|"low"|"info",',
10886
+ ' "area": string (short kebab-case topic, max 40 chars),',
10887
+ ' "claim": string (ONE short sentence, max 200 chars),',
10888
+ ' "action": string (optional, ONE short imperative sentence, max 200 chars),',
10889
+ ' "confidence": number 0..1}',
10890
+ "Do NOT include a rationale field. Keep every string SHORT \u2014 long strings get corrupted in transport and void your work.",
10891
+ `Report at most ${MAX_FINDINGS} findings; a finding cites 1..${MAX_SPAN_IDS_PER_FINDING} span_ids, each copied VERBATIM from a span in the trajectory below.`,
10892
+ '"findings" is [] only for a clean trajectory.'
10893
+ ];
10894
+ function buildPrompt(question, projection) {
10895
+ return [
10896
+ `QUESTION: ${question}`,
10897
+ "",
10898
+ ...OUTPUT_CONTRACT_LINES,
10899
+ "",
10900
+ `TRAJECTORY (${projection.traceCount} trace(s); ${projection.rows.length} spans; full OpenInference span projection as JSON):`,
10901
+ projection.rendered
10902
+ ].join("\n");
10903
+ }
10904
+ function buildRepairPrompt(parseDefect, previousReply) {
10905
+ return [
10906
+ "Your previous reply to a trace-analysis task was structurally malformed and could not be parsed",
10907
+ `(${parseDefect}). Below is your previous reply verbatim. Re-emit ONLY the corrected JSON \u2014 one`,
10908
+ "fenced ```json block, no other text, no tools. The JSON object has exactly two fields:",
10909
+ ' "answer": string (ONE short sentence, max 300 chars)',
10910
+ ' "findings": array (possibly empty) of {"span_ids": [string, ...],',
10911
+ ' "severity": "critical"|"high"|"medium"|"low"|"info", "area": string (max 40 chars),',
10912
+ ' "claim": string (max 200 chars), "action": string (optional, max 200 chars), "confidence": number 0..1}',
10913
+ "No rationale field. Keep every string SHORT. Preserve the span ids and verdicts of your previous reply exactly; shorten prose freely.",
10914
+ "",
10915
+ "PREVIOUS REPLY:",
10916
+ previousReply
10917
+ ].join("\n");
10918
+ }
10919
+ function tryParseObject(text2) {
10920
+ try {
10921
+ const value = JSON.parse(text2.trim());
10922
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
10923
+ } catch {
10924
+ return null;
10925
+ }
10926
+ }
10927
+ function extractJsonObject(text2) {
10928
+ const direct = tryParseObject(text2);
10929
+ if (direct) return direct;
10930
+ const fenced = [...text2.matchAll(/```(?:json)?\s*\n?([\s\S]*?)```/g)];
10931
+ for (let i = fenced.length - 1; i >= 0; i -= 1) {
10932
+ const candidate = tryParseObject(fenced[i][1]);
10933
+ if (candidate) return candidate;
10934
+ }
10935
+ const start = text2.indexOf("{");
10936
+ const end = text2.lastIndexOf("}");
10937
+ if (start >= 0 && end > start) {
10938
+ const candidate = tryParseObject(text2.slice(start, end + 1));
10939
+ if (candidate) return candidate;
10940
+ }
10941
+ return null;
10942
+ }
10943
+ function parseDefectOf(parsed) {
10944
+ if (parsed === null) return "no parseable JSON object";
10945
+ if (!Array.isArray(parsed.findings)) return 'JSON has no "findings" array';
10946
+ return null;
10947
+ }
10948
+ function findingRowDefect(row, spanTraces) {
10949
+ if (!row || typeof row !== "object" || Array.isArray(row)) return "row is not an object";
10950
+ const record2 = row;
10951
+ if (!Array.isArray(record2.span_ids) || record2.span_ids.length === 0) {
10952
+ return "span_ids must be a non-empty array";
10953
+ }
10954
+ const spanIds = [];
10955
+ for (const id of record2.span_ids) {
10956
+ if (typeof id !== "string" || id.length === 0) return "span_ids must contain non-empty strings";
10957
+ const traces = spanTraces.get(id);
10958
+ if (!traces) return `span_id '${id}' is not in the trajectory`;
10959
+ if (traces.size > 1) return `span_id '${id}' is ambiguous across ${traces.size} traces`;
10960
+ if (!spanIds.includes(id)) spanIds.push(id);
10961
+ }
10962
+ if (spanIds.length > MAX_SPAN_IDS_PER_FINDING) {
10963
+ return `span_ids cites ${spanIds.length} distinct spans (cap ${MAX_SPAN_IDS_PER_FINDING})`;
10964
+ }
10965
+ if (typeof record2.severity !== "string" || !SEVERITIES.has(record2.severity)) {
10966
+ return "severity outside the analyst severity enum";
10967
+ }
10968
+ if (typeof record2.area !== "string" || record2.area.trim().length === 0 || record2.area.length > 200) {
10969
+ return "area must be a 1-200 char string";
10970
+ }
10971
+ if (typeof record2.claim !== "string" || record2.claim.trim().length === 0 || record2.claim.length > 2e3) {
10972
+ return "claim must be a 1-2000 char string";
10973
+ }
10974
+ if (record2.action !== void 0 && (typeof record2.action !== "string" || record2.action.trim().length === 0 || record2.action.length > 2e3)) {
10975
+ return "action must be a 1-2000 char string when present";
10976
+ }
10977
+ if (typeof record2.confidence !== "number" || !Number.isFinite(record2.confidence) || record2.confidence < 0 || record2.confidence > 1) {
10978
+ return "confidence must be 0..1";
10979
+ }
10980
+ return {
10981
+ spanIds,
10982
+ severity: record2.severity,
10983
+ area: record2.area.trim(),
10984
+ claim: record2.claim.trim(),
10985
+ ...typeof record2.action === "string" ? { action: record2.action.trim() } : {},
10986
+ confidence: record2.confidence
10987
+ };
10988
+ }
10989
+ function toAnalystFinding(row, index, spanTraces, model, producedAt) {
10990
+ const uris = row.spanIds.map((spanId) => {
10991
+ const traces = spanTraces.get(spanId);
10992
+ return spanEvidenceUri([...traces][0], spanId);
10993
+ });
10994
+ const findingId = `prime-${createHash10("sha256").update(`${index}
10995
+ ${row.claim}
10996
+ ${uris.join("\n")}`).digest("hex").slice(0, 12)}`;
10997
+ return {
10998
+ schema_version: "1.0.0",
10999
+ finding_id: findingId,
11000
+ analyst_id: ANALYST_ID,
11001
+ produced_at: producedAt,
11002
+ severity: row.severity,
11003
+ area: row.area,
11004
+ claim: row.claim,
11005
+ evidence_refs: uris.map((uri) => ({ kind: "span", uri })),
11006
+ confidence: row.confidence,
11007
+ ...row.action ? { recommended_action: row.action } : {},
11008
+ metadata: { engine: ANALYST_ID, model }
11009
+ };
11010
+ }
11011
+ var httpJsonTransport = ({ url, body, signal }) => {
11012
+ const target = new URL(url);
11013
+ if (target.protocol !== "http:" && target.protocol !== "https:") {
11014
+ throw new TypeError(`bridge URL must be http: or https:, got ${target.protocol}`);
11015
+ }
11016
+ const requestFn = target.protocol === "https:" ? httpsRequest : httpRequest;
11017
+ const payload = JSON.stringify(body);
11018
+ return new Promise((resolve10, reject) => {
11019
+ const req = requestFn(
11020
+ {
11021
+ hostname: target.hostname,
11022
+ port: target.port,
11023
+ path: `${target.pathname}${target.search}`,
11024
+ method: "POST",
11025
+ headers: { "content-type": "application/json", "content-length": Buffer.byteLength(payload) },
11026
+ signal
11027
+ },
11028
+ (res) => {
11029
+ const chunks = [];
11030
+ res.on("data", (chunk) => chunks.push(chunk));
11031
+ res.on("end", () => resolve10({ status: res.statusCode ?? 0, text: Buffer.concat(chunks).toString("utf8") }));
11032
+ res.on("error", reject);
11033
+ }
11034
+ );
11035
+ req.on("error", reject);
11036
+ req.end(payload);
11037
+ });
11038
+ };
11039
+ var PrimeBridgeError = class extends Error {
11040
+ };
11041
+ async function callBridge(transport, url, model, content, timeoutMs, callerSignal, turn) {
11042
+ const controller = new AbortController();
11043
+ const timer = setTimeout(
11044
+ () => controller.abort(new Error(`${turn}bridge call exceeded ${timeoutMs}ms`)),
11045
+ timeoutMs
11046
+ );
11047
+ const onCallerAbort = () => controller.abort(callerSignal.reason);
11048
+ callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
11049
+ if (callerSignal?.aborted) onCallerAbort();
11050
+ let response;
11051
+ try {
11052
+ response = await transport({
11053
+ url,
11054
+ body: { model, messages: [{ role: "user", content }] },
11055
+ signal: controller.signal
11056
+ });
11057
+ } catch (error) {
11058
+ const reason = controller.signal.aborted ? controller.signal.reason : error;
11059
+ throw new PrimeBridgeError(
11060
+ `${turn}bridge transport failure: ${reason instanceof Error ? reason.message : String(reason)}`
11061
+ );
11062
+ } finally {
11063
+ clearTimeout(timer);
11064
+ callerSignal?.removeEventListener("abort", onCallerAbort);
11065
+ }
11066
+ if (response.status !== 200) {
11067
+ throw new PrimeBridgeError(`${turn}bridge HTTP ${response.status}: ${response.text.slice(0, 500)}`);
11068
+ }
11069
+ let parsed;
11070
+ try {
11071
+ parsed = JSON.parse(response.text);
11072
+ } catch {
11073
+ throw new PrimeBridgeError(`${turn}bridge returned unparseable JSON (${response.text.length} bytes)`);
11074
+ }
11075
+ const record2 = parsed;
11076
+ const replyContent = record2.choices?.[0]?.message?.content;
11077
+ if (typeof replyContent !== "string" || replyContent.length === 0) {
11078
+ throw new PrimeBridgeError(`${turn}bridge reply carries no message content`);
11079
+ }
11080
+ return { content: replyContent, usage: normalizeUsage(record2.usage) };
11081
+ }
11082
+ function failure(output, error) {
11083
+ return { analyzer: ANALYST_ID, kind: "report", ok: false, output, error };
11084
+ }
11085
+ function primeAnalyzer(opts = {}) {
11086
+ const bridgeUrl = (opts.bridgeUrl ?? process.env.TRACES_PRIME_BRIDGE_URL ?? DEFAULT_BRIDGE_URL).replace(/\/+$/, "");
11087
+ const model = opts.model ?? process.env.TRACES_PRIME_MODEL ?? DEFAULT_MODEL;
11088
+ const timeoutMs = opts.timeoutMs ?? positiveIntegerEnv("TRACES_PRIME_TIMEOUT_MS") ?? DEFAULT_TIMEOUT_MS;
11089
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1) {
11090
+ throw new RangeError("timeoutMs must be a positive safe integer");
11091
+ }
11092
+ const maxInlineChars = opts.maxInlineChars ?? DEFAULT_MAX_INLINE_CHARS;
11093
+ const perAttributeCharCap = opts.perAttributeCharCap ?? DEFAULT_PER_ATTRIBUTE_CHAR_CAP;
11094
+ if (!Number.isSafeInteger(maxInlineChars) || maxInlineChars < 1) {
11095
+ throw new RangeError("maxInlineChars must be a positive safe integer");
11096
+ }
11097
+ if (!Number.isSafeInteger(perAttributeCharCap) || perAttributeCharCap < 1) {
11098
+ throw new RangeError("perAttributeCharCap must be a positive safe integer");
11099
+ }
11100
+ const repairEnabled = opts.repair ?? true;
11101
+ const transport = opts.transport ?? httpJsonTransport;
11102
+ const url = `${bridgeUrl}/v1/chat/completions`;
11103
+ return {
11104
+ name: ANALYST_ID,
11105
+ async analyze(otlpPath, analyzeOpts = {}) {
11106
+ let projection;
11107
+ try {
11108
+ projection = await projectArtifact(otlpPath, maxInlineChars, perAttributeCharCap, analyzeOpts.signal);
11109
+ } catch (error) {
11110
+ return failure("", error instanceof Error ? error.message : String(error));
11111
+ }
11112
+ const question = analyzeOpts.prompt ?? opts.defaultPrompt ?? DEFAULT_QUESTION;
11113
+ const deliveryLine = `delivery: ${projection.delivery.mode} (${projection.delivery.renderedChars} chars, per-attribute cap ${projection.delivery.perAttributeCharCap ?? "none"})`;
11114
+ let reply;
11115
+ try {
11116
+ reply = await callBridge(
11117
+ transport,
11118
+ url,
11119
+ model,
11120
+ buildPrompt(question, projection),
11121
+ timeoutMs,
11122
+ analyzeOpts.signal,
11123
+ ""
11124
+ );
11125
+ } catch (error) {
11126
+ if (!(error instanceof PrimeBridgeError)) throw error;
11127
+ return failure("", `${error.message}; ${deliveryLine}`);
11128
+ }
11129
+ let usage = reply.usage;
11130
+ let parsed = extractJsonObject(reply.content);
11131
+ let parseDefect = parseDefectOf(parsed);
11132
+ let repairAttempted = false;
11133
+ if (parseDefect !== null && repairEnabled) {
11134
+ repairAttempted = true;
11135
+ let repairReply;
11136
+ try {
11137
+ repairReply = await callBridge(
11138
+ transport,
11139
+ url,
11140
+ model,
11141
+ buildRepairPrompt(parseDefect, reply.content),
11142
+ timeoutMs,
11143
+ analyzeOpts.signal,
11144
+ "repair-turn "
11145
+ );
11146
+ } catch (error) {
11147
+ if (!(error instanceof PrimeBridgeError)) throw error;
11148
+ return failure(reply.content, `${error.message}; ${renderUsage(usage)}`);
11149
+ }
11150
+ usage = mergeUsage(usage, repairReply.usage);
11151
+ parsed = extractJsonObject(repairReply.content);
11152
+ parseDefect = parseDefectOf(parsed);
11153
+ }
11154
+ if (parseDefect !== null) {
11155
+ return failure(
11156
+ reply.content,
11157
+ `${parseDefect} in prime reply${repairAttempted ? " even after the bounded repair turn" : ""}; ` + renderUsage(usage)
11158
+ );
11159
+ }
11160
+ const rows = parsed.findings.slice(0, MAX_FINDINGS);
11161
+ const overflow = parsed.findings.length - rows.length;
11162
+ const producedAt = (/* @__PURE__ */ new Date()).toISOString();
11163
+ const rejected = [];
11164
+ const findings = [];
11165
+ rows.forEach((row, index) => {
11166
+ const decoded = findingRowDefect(row, projection.spanTraces);
11167
+ if (typeof decoded === "string") {
11168
+ rejected.push(`rejected[${index}]: ${decoded}`);
11169
+ return;
11170
+ }
11171
+ findings.push(toAnalystFinding(decoded, index, projection.spanTraces, model, producedAt));
11172
+ });
11173
+ const answer = typeof parsed.answer === "string" ? parsed.answer : null;
11174
+ const output = [
11175
+ ...answer ? [`answer: ${answer}`] : [],
11176
+ `findings: ${findings.length} mapped, ${rejected.length} rejected` + (overflow > 0 ? `, ${overflow} over the ${MAX_FINDINGS}-finding cap dropped` : ""),
11177
+ ...rejected,
11178
+ deliveryLine,
11179
+ `repair: ${repairAttempted ? "attempted (succeeded)" : "not needed"}`,
11180
+ renderUsage(usage),
11181
+ ...findings.length === 0 ? ["zero findings \u2014 an honest null, not a failure"] : []
11182
+ ].join("\n");
11183
+ return { analyzer: ANALYST_ID, kind: "findings", ok: true, output, findings };
11184
+ }
11185
+ };
11186
+ }
11187
+
11188
+ // src/redact.ts
11189
+ import { DEFAULT_REDACTION_RULES, redactValue } from "@tangle-network/agent-eval/traces";
11190
+ var CODING_REDACTION_RULES = [
11191
+ { id: "github-token", pattern: /\b(?:gh[pousr]_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{22,})\b/g },
11192
+ { id: "aws-akid", pattern: /\bAKIA[0-9A-Z]{16}\b/g },
11193
+ { id: "slack-token", pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
11194
+ { id: "jwt", pattern: /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g },
11195
+ { id: "bearer", pattern: /\bBearer\s+[A-Za-z0-9._-]{16,}/gi },
11196
+ {
11197
+ id: "private-key",
11198
+ pattern: /-----BEGIN(?:[A-Z ]+)?PRIVATE KEY-----[\s\S]*?-----END(?:[A-Z ]+)?PRIVATE KEY-----/g
11199
+ },
11200
+ // Match the whole identifier because underscores prevent a word boundary before a secret suffix.
11201
+ {
11202
+ id: "assigned-secret",
11203
+ pattern: /(?<![A-Za-z0-9_-])(?:[A-Za-z_][A-Za-z0-9_-]*)?(?:api[_-]?key|secret|token|password|passwd|access[_-]?token|client[_-]?secret)(?![A-Za-z0-9_-])\+?=(?:\$\((?:\\.|[^\\\r\n])*\)|\$\{(?:\\.|[^}\\\r\n])*\}|`(?:\\.|[^`\\\r\n])*`|\$?'(?:\\.|[^'\\\r\n])*'|\$?"(?:\\.|[^"\\\r\n])*"|\\[^\r\n]|[^\s;&|<>()"'`\\\r\n])+/gi
11204
+ },
11205
+ // Config-file secrets (YAML/INI line keys, JSON quoted keys) — the coverage the shell
11206
+ // rule above deliberately gave up to spare prose ("Narrative token: x") and type
11207
+ // annotations ("(token: string)"). The line anchor is the discriminator: config keys
11208
+ // start their line, prose colons never do; JSON keys are safe anywhere because both
11209
+ // sides are quoted. Identifiers must END in a secret word, same as the shell rule.
11210
+ {
11211
+ id: "config-secret",
11212
+ pattern: /(?:^[ \t]*["']?(?:[A-Za-z_][A-Za-z0-9_-]*)?(?:api[_-]?key|secret|token|password|passwd|access[_-]?token|client[_-]?secret)(?![A-Za-z0-9_-])["']?[ \t]*[:=][ \t]*|["'](?:[A-Za-z_][A-Za-z0-9_-]*)?(?:api[_-]?key|secret|token|password|passwd|access[_-]?token|client[_-]?secret)["']\s*:\s*)["']?[A-Za-z0-9._\-]{12,}["']?/gim
11213
+ },
11214
+ // Credentials embedded in URLs — common when a prompt pastes a curl/clone line.
11215
+ { id: "url-userinfo", pattern: /\b[a-z][a-z0-9+.-]*:\/\/[^\s/:@]+:[^\s/@]+@/gi },
11216
+ {
11217
+ id: "url-secret-param",
11218
+ pattern: /[?&](?:access[_-]?token|api[_-]?key|apikey|auth|token|secret|password|sig|signature)=[^&\s"'#)]+/gi
11219
+ }
11220
+ ];
11221
+ var TRACES_REDACTION_RULES = [...DEFAULT_REDACTION_RULES, ...CODING_REDACTION_RULES];
11222
+ function redactSpans(spans, rules = TRACES_REDACTION_RULES) {
11223
+ const report = { redactionCount: 0, byRule: {} };
11224
+ const out = spans.map((s) => {
11225
+ const attributes2 = redactValue(s.attributes, rules, report).value;
10654
11226
  normalizeToolIoAttributes(attributes2);
10655
11227
  let status = s.status;
10656
11228
  if (status.message) {
@@ -10703,7 +11275,7 @@ async function applyRedactor(spans, redactor) {
10703
11275
  }
10704
11276
 
10705
11277
  // src/file-export.ts
10706
- import { createHash as createHash10 } from "crypto";
11278
+ import { createHash as createHash11 } from "crypto";
10707
11279
  import { mkdtemp as mkdtemp4, readFile as readFile9, writeFile as writeFile5 } from "fs/promises";
10708
11280
  import { tmpdir as tmpdir5 } from "os";
10709
11281
  import { join as join24 } from "path";
@@ -10741,7 +11313,7 @@ function stableJson(value) {
10741
11313
  });
10742
11314
  }
10743
11315
  function hashId(value, chars = 16) {
10744
- return createHash10("sha256").update(stableJson(value)).digest("hex").slice(0, chars);
11316
+ return createHash11("sha256").update(stableJson(value)).digest("hex").slice(0, chars);
10745
11317
  }
10746
11318
  function isoTime2(value) {
10747
11319
  if (typeof value === "string") {
@@ -10902,12 +11474,12 @@ function policyEvidenceToSpans(records) {
10902
11474
  "traces.provenance.evidence_kind": record2.provenance.evidenceKind,
10903
11475
  "traces.provenance.otlp_path": record2.provenance.otlpPath,
10904
11476
  "traces.provenance.source_sha256": record2.provenance.sourceSha256,
10905
- [ATTR2.SUBJECT_KEY]: record2.repo.subjectKey,
10906
- [ATTR2.GIT_REPOSITORY]: record2.repo.repository,
10907
- [ATTR2.GIT_BRANCH_NAME]: record2.repo.branch,
10908
- [ATTR2.GIT_COMMIT]: record2.repo.commit,
10909
- [ATTR2.CWD]: record2.repo.cwd ?? record2.session.cwd,
10910
- [ATTR2.REPO_RESOLUTION_SOURCE]: record2.repo.resolutionSource
11477
+ [ATTR.SUBJECT_KEY]: record2.repo.subjectKey,
11478
+ [ATTR.GIT_REPOSITORY]: record2.repo.repository,
11479
+ [ATTR.GIT_BRANCH_NAME]: record2.repo.branch,
11480
+ [ATTR.GIT_COMMIT]: record2.repo.commit,
11481
+ [ATTR.CWD]: record2.repo.cwd ?? record2.session.cwd,
11482
+ [ATTR.REPO_RESOLUTION_SOURCE]: record2.repo.resolutionSource
10911
11483
  };
10912
11484
  return span({
10913
11485
  traceId,
@@ -11004,6 +11576,54 @@ function eventKind(row, type) {
11004
11576
  if (lowered.includes("llm") || lowered === "token") return { kind: "LLM" };
11005
11577
  return { kind: "CHAIN" };
11006
11578
  }
11579
+ function sandboxEventsEnvironment(context, rows) {
11580
+ const ready = rows.find((row) => eventType(row) === "runtime.ready");
11581
+ const readyData = ready && isObject5(ready.data) ? ready.data : void 0;
11582
+ const pick = (keys) => findStringKey(context, keys, 2) ?? (readyData ? findStringKey(readyData, keys, 2) : void 0);
11583
+ const image = pick(["image"]);
11584
+ const imageDigest = pick(["imageDigest", "image_digest"]);
11585
+ const sandboxId = pick(["sandboxId", "sandbox_id"]);
11586
+ const cwd = pick(["cwd", "workspaceRoot", "workspace_root"]);
11587
+ if (!image && !imageDigest && !sandboxId && !cwd) return null;
11588
+ return {
11589
+ ...image ? { image } : {},
11590
+ ...imageDigest ? { imageDigest } : {},
11591
+ ...sandboxId ? { sandboxId } : {},
11592
+ cwd: cwd ?? null
11593
+ };
11594
+ }
11595
+ function llmEventUsage(row) {
11596
+ const num = (keys) => {
11597
+ for (const key of keys) {
11598
+ const direct = numberValue(row[key]);
11599
+ if (direct !== void 0) return direct;
11600
+ for (const nest of ["data", "usage", "tokenUsage"]) {
11601
+ const container = row[nest];
11602
+ if (!isObject5(container)) continue;
11603
+ const value = numberValue(container[key]);
11604
+ if (value !== void 0) return value;
11605
+ for (const inner of ["usage", "tokenUsage"]) {
11606
+ const deep = container[inner];
11607
+ if (isObject5(deep)) {
11608
+ const deepValue = numberValue(deep[key]);
11609
+ if (deepValue !== void 0) return deepValue;
11610
+ }
11611
+ }
11612
+ }
11613
+ }
11614
+ return void 0;
11615
+ };
11616
+ const model = findStringKey(row, ["model"], 3);
11617
+ const inputTokens = num(["prompt_tokens", "input_tokens", "inputTokens"]);
11618
+ const outputTokens = num(["completion_tokens", "output_tokens", "outputTokens"]);
11619
+ const costUsd = num(["totalCostUsd", "total_cost_usd", "costUsd", "cost_usd"]);
11620
+ return {
11621
+ ...model ? { model } : {},
11622
+ ...inputTokens !== void 0 ? { inputTokens } : {},
11623
+ ...outputTokens !== void 0 ? { outputTokens } : {},
11624
+ ...costUsd !== void 0 ? { costUsd } : {}
11625
+ };
11626
+ }
11007
11627
  function sandboxEventsToSpans(rows, wrapper) {
11008
11628
  const context = wrapper ?? {};
11009
11629
  const sessionId = findStringKey(context, ["session_id", "sessionId"], 3) ?? findStringKey(rows, ["session_id", "sessionId"], 4);
@@ -11074,13 +11694,15 @@ function sandboxEventsToSpans(rows, wrapper) {
11074
11694
  tool: kind === "TOOL" ? tool ?? type : null,
11075
11695
  step: index,
11076
11696
  content: capText(stableJson(row)),
11077
- extra
11697
+ extra,
11698
+ ...kind === "LLM" ? llmEventUsage(row) : {}
11078
11699
  }));
11079
11700
  });
11701
+ stampEnvironmentAttrs(spans, sandboxEventsEnvironment(context, rows) ?? void 0);
11080
11702
  if (!sessionId) return spans;
11081
11703
  return spans.map((item) => ({
11082
11704
  ...item,
11083
- attributes: { ...item.attributes, [ATTR2.SESSION_ID]: sessionId }
11705
+ attributes: { ...item.attributes, [ATTR.SESSION_ID]: sessionId }
11084
11706
  }));
11085
11707
  }
11086
11708
  function statusCode2(value) {
@@ -11159,7 +11781,7 @@ function intelligenceSpansToSpans(rows) {
11159
11781
  };
11160
11782
  copyDefined(extra, {
11161
11783
  "traces.row.id": stringValue3(row.id),
11162
- [ATTR2.SESSION_ID]: sessionId,
11784
+ [ATTR.SESSION_ID]: sessionId,
11163
11785
  "session.id": sessionId,
11164
11786
  "run.id": stringValue3(row.run_id),
11165
11787
  "thread.id": stringValue3(row.thread_id),
@@ -12534,6 +13156,47 @@ var SEVERITY_BADGE2 = {
12534
13156
  low: "\u{1F535} LOW",
12535
13157
  info: "\u2139\uFE0F INFO"
12536
13158
  };
13159
+ function sessionReportSource(ref, spans, sessionIdOverride) {
13160
+ const root = spans.find((item) => item.parent_span_id === null) ?? spans[0];
13161
+ const prompt = spans.find(
13162
+ (item) => item.name === "user.prompt" && item.attributes[ACTOR_ATTR] === "human"
13163
+ ) ?? spans.find((item) => item.name === "user.prompt") ?? spans.find(
13164
+ (item) => item.attributes["span.type"] === "interaction" && typeof item.attributes.content === "string"
13165
+ );
13166
+ const content = typeof prompt?.attributes.content === "string" ? prompt.attributes.content : "";
13167
+ const firstLine = content.split(/\r?\n/, 1)[0].trim();
13168
+ const subject = firstLine.length > 240 ? `${firstLine.slice(0, 240)}\u2026 [+${firstLine.length - 240} chars]` : firstLine;
13169
+ const role = root?.attributes["traces.session.role"];
13170
+ const parentSessionId = root?.attributes["traces.parent_session_id"];
13171
+ const relationship = describeSessionRelationship(ref, spans);
13172
+ const corruptionDigest2 = root?.attributes[ATTR.CORRUPTION_DIGEST];
13173
+ const sessionId = [
13174
+ sessionIdOverride,
13175
+ root ? sessionIdFromAttributes(root.attributes) : void 0,
13176
+ root?.trace_id,
13177
+ ref.sessionId
13178
+ ].find((value) => typeof value === "string" && value.length > 0);
13179
+ return {
13180
+ sessionId,
13181
+ path: ref.path,
13182
+ subject,
13183
+ role: role === "operator" || role === "child" ? role : "unknown",
13184
+ ...typeof parentSessionId === "string" ? { parentSessionId } : {},
13185
+ childSessionIds: relationship.childSessionIds,
13186
+ ...relationship.depth !== void 0 ? { depth: relationship.depth } : {},
13187
+ ...relationship.agentNickname ? { agentNickname: relationship.agentNickname } : {},
13188
+ ...relationship.agentRole ? { agentRole: relationship.agentRole } : {},
13189
+ ...relationship.agentPath ? { agentPath: relationship.agentPath } : {},
13190
+ ...relationship.taskScope ? { taskScope: relationship.taskScope } : {},
13191
+ ...relationship.turnId ? { turnId: relationship.turnId } : {},
13192
+ integrity: ref.integrity?.status ?? "complete",
13193
+ ...ref.integrity ? {
13194
+ corruptionCount: ref.integrity.corruptions.length,
13195
+ ...typeof corruptionDigest2 === "string" ? { corruptionDigest: corruptionDigest2 } : {},
13196
+ corruptions: ref.integrity.corruptions.slice(0, CORRUPTION_RECEIPT_DISPLAY_LIMIT)
13197
+ } : {}
13198
+ };
13199
+ }
12537
13200
  function deterministicSummaryText(summary) {
12538
13201
  const parts = [];
12539
13202
  if (summary.stuckLoops > 0) parts.push(`${summary.stuckLoops} stuck loop(s)`);
@@ -12912,11 +13575,27 @@ function renderPipelines(pr, unavailable) {
12912
13575
  byTool.set(name, aggregate);
12913
13576
  }
12914
13577
  }
12915
- const failureFollowUp = errorCalls > 0 ? `; ${followedFailures}/${errorCalls} failed calls followed by another same-tool call (${(followedFailures / errorCalls * 100).toFixed(0)}%)` : "";
13578
+ const followUps = pr.failureFollowUps;
13579
+ let failureFollowUp = "";
13580
+ if (followUps && followUps.failures > 0) {
13581
+ const split = [
13582
+ `${followUps.blind} blind (identical args)`,
13583
+ `${followUps.adapted} adapted (changed args)`
13584
+ ];
13585
+ if (followUps.argsUnknown > 0) split.push(`${followUps.argsUnknown} not comparable`);
13586
+ const succeeded = followUps.followed > 0 ? `; ${followUps.followUpSucceeded}/${followUps.followed} follow-ups succeeded` : "";
13587
+ failureFollowUp = `; ${followUps.followed}/${followUps.failures} failed calls followed by another same-tool call (${(followUps.followed / followUps.failures * 100).toFixed(0)}%) \u2014 ${split.join(", ")}${succeeded}`;
13588
+ } else if (errorCalls > 0) {
13589
+ failureFollowUp = `; ${followedFailures}/${errorCalls} failed calls followed by another same-tool call (${(followedFailures / errorCalls * 100).toFixed(0)}%)`;
13590
+ }
12916
13591
  const duplicateSummary = callsWithCapturedArgs > 0 ? `${duplicateCalls}/${callsWithCapturedArgs} captured calls repeated exactly` : "exact repeats unavailable";
12917
13592
  lines.push(
12918
13593
  `- **Tool use:** ${totalCalls} calls across ${runsWithTools.length}/${pr.toolUse.length} traces; ${callsWithCapturedArgs}/${totalCalls} arguments captured; ${duplicateSummary}; ${errorCalls}/${totalCalls} failed${failureFollowUp}`
12919
13594
  );
13595
+ if (followUps && followUps.blind > 0) {
13596
+ const byTool2 = Object.entries(followUps.blindByTool).sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0])).map(([name, count2]) => `\`${tableCell2(name)}\` \xD7${count2}`);
13597
+ lines.push(` - \u{1F501} Blind retries (same args re-sent after a failure): ${byTool2.join(", ")}`);
13598
+ }
12920
13599
  if (byTool.size > 0) {
12921
13600
  lines.push("");
12922
13601
  lines.push("| Tool | Calls | Args captured | Exact repeats | Errors | Mean latency |");
@@ -13684,27 +14363,27 @@ async function saveReport(path, report) {
13684
14363
  }
13685
14364
 
13686
14365
  // src/live.ts
13687
- import { createHash as createHash11 } from "crypto";
14366
+ import { createHash as createHash12 } from "crypto";
13688
14367
  var VERIFY_RE = /\b(test|tests|vitest|jest|pytest|go test|cargo test|tsc|typecheck|lint|biome|eslint|build|check:invariants|preflight)\b/i;
13689
14368
  var CHANGE_RE = /\b(apply_patch|edit|write|create|delete|rename|replace|update|git apply|sed -i|perl -pi|tee\s|cat\s+>)/i;
13690
14369
  var CLAIM_DONE_RE = /\b(done|fixed|complete|completed|implemented|merged|verified|all green|tests pass|ready|works now)\b/i;
13691
- var sleep = (ms2, signal) => new Promise((resolve8) => {
14370
+ var sleep = (ms2, signal) => new Promise((resolve10) => {
13692
14371
  if (signal?.aborted) {
13693
- resolve8();
14372
+ resolve10();
13694
14373
  return;
13695
14374
  }
13696
14375
  const onAbort = () => {
13697
14376
  clearTimeout(t);
13698
- resolve8();
14377
+ resolve10();
13699
14378
  };
13700
14379
  const t = setTimeout(() => {
13701
14380
  signal?.removeEventListener("abort", onAbort);
13702
- resolve8();
14381
+ resolve10();
13703
14382
  }, ms2);
13704
14383
  signal?.addEventListener("abort", onAbort, { once: true });
13705
14384
  });
13706
14385
  function hash(parts) {
13707
- return createHash11("sha256").update(parts.join("\0")).digest("hex").slice(0, 16);
14386
+ return createHash12("sha256").update(parts.join("\0")).digest("hex").slice(0, 16);
13708
14387
  }
13709
14388
  function asString(value) {
13710
14389
  if (typeof value === "string" && value.trim()) return value;
@@ -15174,13 +15853,13 @@ function aborted(signal) {
15174
15853
  return signal !== void 0 && signal.aborted;
15175
15854
  }
15176
15855
  function sleep2(msDelay, signal) {
15177
- return new Promise((resolve8) => {
15178
- const timer = setTimeout(resolve8, msDelay);
15856
+ return new Promise((resolve10) => {
15857
+ const timer = setTimeout(resolve10, msDelay);
15179
15858
  signal?.addEventListener(
15180
15859
  "abort",
15181
15860
  () => {
15182
15861
  clearTimeout(timer);
15183
- resolve8();
15862
+ resolve10();
15184
15863
  },
15185
15864
  { once: true }
15186
15865
  );
@@ -15520,11 +16199,11 @@ async function collectSessionWorkflow(adapter, seeds, options = {}) {
15520
16199
  }
15521
16200
 
15522
16201
  // src/session-selection.ts
15523
- import { createHash as createHash12 } from "crypto";
16202
+ import { createHash as createHash13 } from "crypto";
15524
16203
  import { createReadStream as createReadStream2 } from "fs";
15525
16204
  async function fileSha256(path, signal) {
15526
16205
  signal?.throwIfAborted();
15527
- const hash2 = createHash12("sha256");
16206
+ const hash2 = createHash13("sha256");
15528
16207
  try {
15529
16208
  for await (const chunk of createReadStream2(path, { signal })) {
15530
16209
  signal?.throwIfAborted();
@@ -15552,7 +16231,7 @@ async function sourceSnapshot(adapter, ref, signal) {
15552
16231
  signal?.throwIfAborted();
15553
16232
  files.push({ path, sha256: await fileSha256(path, signal) });
15554
16233
  }
15555
- const sha2562 = files.length === 1 ? files[0].sha256 : createHash12("sha256").update(JSON.stringify(files)).digest("hex");
16234
+ const sha2562 = files.length === 1 ? files[0].sha256 : createHash13("sha256").update(JSON.stringify(files)).digest("hex");
15556
16235
  return { sha256: sha2562, files };
15557
16236
  }
15558
16237
  function summarizeWorkflows(workflows) {
@@ -15813,14 +16492,30 @@ async function collectContextRoot(root) {
15813
16492
  const summary = await summarizeFile(join31(evolve, name), "evolve-jsonl");
15814
16493
  if (summary) files.push(summary);
15815
16494
  }
15816
- for (const name of ["scorecard.json"]) {
16495
+ for (const name of ["scorecard.json", "current.json"]) {
15817
16496
  const summary = await summarizeFile(join31(evolve, name), "evolve-json");
15818
16497
  if (summary) files.push(summary);
15819
16498
  }
15820
16499
  files.push(...await walkMarkdown(join31(evolve, "reflections"), "reflection"));
15821
16500
  files.push(...await walkMarkdown(join31(evolve, "handoffs"), "handoff"));
16501
+ for (const name of await listEvolveRootFiles(evolve)) {
16502
+ if (/^handoff-.*\.md$/.test(name)) {
16503
+ const summary = await summarizeFile(join31(evolve, name), "handoff");
16504
+ if (summary) files.push(summary);
16505
+ }
16506
+ }
16507
+ const progress = await summarizeFile(join31(evolve, "progress.md"), "other");
16508
+ if (progress) files.push(progress);
15822
16509
  return { root, files: files.sort((a, b) => a.path.localeCompare(b.path)) };
15823
16510
  }
16511
+ async function listEvolveRootFiles(evolve) {
16512
+ try {
16513
+ const entries = await readdir18(evolve, { withFileTypes: true });
16514
+ return entries.filter((entry) => entry.isFile()).map((entry) => entry.name).sort();
16515
+ } catch {
16516
+ return [];
16517
+ }
16518
+ }
15824
16519
  async function collectContextIndex(records) {
15825
16520
  const rootSet = /* @__PURE__ */ new Set();
15826
16521
  for (const record2 of records) {
@@ -15869,14 +16564,641 @@ async function writeSessionIndexFile(index, outPath) {
15869
16564
  return path;
15870
16565
  }
15871
16566
 
16567
+ // src/bundle.ts
16568
+ import { createHash as createHash14 } from "crypto";
16569
+ import { readFileSync as readFileSync5 } from "fs";
16570
+ import { cp, mkdir as mkdir5, readdir as readdir19, readFile as readFile14, stat as stat17, writeFile as writeFile9 } from "fs/promises";
16571
+ import { join as join32, relative as relative5, resolve as resolve8, sep as sep2 } from "path";
16572
+ var SESSION_WINDOW_PAD_MS = 15 * 60 * 1e3;
16573
+ var KNOWN_LIMITS = [
16574
+ "'what was decided' is prose-only: handoff, progress, and reflection files are copied as markdown with no structured decision extraction",
16575
+ "'what was measured' is split between experiments.jsonl free-text fields and progress/handoff markdown tables",
16576
+ "session-to-ledger join is by timestamp/cwd inference: ledger rows are window-sliced, not linked by transcript path",
16577
+ "progress.md is copied whole; no session-dated section extraction is performed",
16578
+ "open pull-request state is not captured: the bundler makes no network calls"
16579
+ ];
16580
+ function sha256Hex(bytes) {
16581
+ return createHash14("sha256").update(bytes).digest("hex");
16582
+ }
16583
+ function bundlePath(...parts) {
16584
+ return parts.join("/");
16585
+ }
16586
+ async function statOrNull(path) {
16587
+ try {
16588
+ return await stat17(path);
16589
+ } catch (error) {
16590
+ if (isMissingPathError(error)) return null;
16591
+ if (error.code === "ENOTDIR") return null;
16592
+ throw error;
16593
+ }
16594
+ }
16595
+ function sliceJsonlByWindow(text2, windowStartMs, windowEndMs) {
16596
+ const kept = [];
16597
+ let totalRows = 0;
16598
+ let unparseableTsRows = 0;
16599
+ for (const line of text2.split("\n")) {
16600
+ if (!line.trim()) continue;
16601
+ totalRows += 1;
16602
+ let tsMs = Number.NaN;
16603
+ try {
16604
+ const row = JSON.parse(line);
16605
+ const ts = row !== null && typeof row === "object" && !Array.isArray(row) ? row.ts : void 0;
16606
+ if (typeof ts === "string") tsMs = Date.parse(ts);
16607
+ } catch {
16608
+ }
16609
+ if (Number.isNaN(tsMs)) {
16610
+ unparseableTsRows += 1;
16611
+ continue;
16612
+ }
16613
+ if (tsMs >= windowStartMs && tsMs <= windowEndMs) kept.push(line);
16614
+ }
16615
+ return { kept, totalRows, unparseableTsRows };
16616
+ }
16617
+ async function runGitLog(cwd, sinceIso, untilIso) {
16618
+ const { execFile: execFile2 } = await import("child_process");
16619
+ const { promisify: promisify2 } = await import("util");
16620
+ const run = promisify2(execFile2);
16621
+ try {
16622
+ const { stdout } = await run(
16623
+ "git",
16624
+ ["-C", cwd, "log", `--since=${sinceIso}`, `--until=${untilIso}`, "--date=iso-strict", "--pretty=format:%H %ad %an %s"],
16625
+ { timeout: 5e3 }
16626
+ );
16627
+ return { succeeded: true, stdout };
16628
+ } catch (error) {
16629
+ return { succeeded: false, error: error instanceof Error ? error.message.split("\n", 1)[0] : String(error) };
16630
+ }
16631
+ }
16632
+ async function listSessionBundleFiles(root) {
16633
+ const out = [];
16634
+ const pending = [resolve8(root)];
16635
+ while (pending.length > 0) {
16636
+ const dir = pending.pop();
16637
+ for (const entry of await readdir19(dir, { withFileTypes: true })) {
16638
+ const path = join32(dir, entry.name);
16639
+ if (entry.isDirectory()) pending.push(path);
16640
+ else if (entry.isFile()) out.push(relative5(resolve8(root), path).split(sep2).join("/"));
16641
+ }
16642
+ }
16643
+ return out.sort();
16644
+ }
16645
+ async function sliceLedgerJsonl(ctx, name) {
16646
+ const source = join32(ctx.evolveDir, name);
16647
+ const s = await statOrNull(source);
16648
+ if (!s?.isFile()) {
16649
+ ctx.absent.push({ path: bundlePath("ledger", name), reason: `no ${name} at ${source}` });
16650
+ return;
16651
+ }
16652
+ const text2 = await readFile14(source, "utf8");
16653
+ const target = join32(ctx.ledgerDir, name);
16654
+ if (ctx.windowStartMs === null || ctx.windowEndMs === null) {
16655
+ await writeFile9(target, text2, "utf8");
16656
+ ctx.slices.push({ path: bundlePath("ledger", name), source, rule: "copied-whole" });
16657
+ return;
16658
+ }
16659
+ const slice = sliceJsonlByWindow(text2, ctx.windowStartMs, ctx.windowEndMs);
16660
+ await writeFile9(target, slice.kept.length > 0 ? `${slice.kept.join("\n")}
16661
+ ` : "", "utf8");
16662
+ ctx.slices.push({
16663
+ path: bundlePath("ledger", name),
16664
+ source,
16665
+ rule: "session-window-rows",
16666
+ totalRows: slice.totalRows,
16667
+ keptRows: slice.kept.length,
16668
+ unparseableTsRows: slice.unparseableTsRows
16669
+ });
16670
+ }
16671
+ async function copyLedgerWhole(ctx, name) {
16672
+ const source = join32(ctx.evolveDir, name);
16673
+ const s = await statOrNull(source);
16674
+ if (!s?.isFile()) {
16675
+ ctx.absent.push({ path: bundlePath("ledger", name), reason: `no ${name} at ${source}` });
16676
+ return;
16677
+ }
16678
+ await cp(source, join32(ctx.ledgerDir, name));
16679
+ ctx.slices.push({ path: bundlePath("ledger", name), source, rule: "copied-whole" });
16680
+ }
16681
+ async function copyLatestHandoff(ctx) {
16682
+ let names;
16683
+ try {
16684
+ names = (await readdir19(ctx.evolveDir, { withFileTypes: true })).filter((entry) => entry.isFile() && /^handoff-.*\.md$/.test(entry.name)).map((entry) => entry.name).sort();
16685
+ } catch {
16686
+ names = [];
16687
+ }
16688
+ const latest = names[names.length - 1];
16689
+ if (!latest) {
16690
+ ctx.absent.push({
16691
+ path: bundlePath("ledger", "handoff-*.md"),
16692
+ reason: `no flat handoff-*.md files in ${ctx.evolveDir}`
16693
+ });
16694
+ return;
16695
+ }
16696
+ const source = join32(ctx.evolveDir, latest);
16697
+ await cp(source, join32(ctx.ledgerDir, latest));
16698
+ ctx.slices.push({ path: bundlePath("ledger", latest), source, rule: "latest-by-name" });
16699
+ }
16700
+ async function copyWindowReflections(ctx) {
16701
+ const reflectionsDir = join32(ctx.evolveDir, "reflections");
16702
+ const s = await statOrNull(reflectionsDir);
16703
+ if (!s?.isDirectory()) {
16704
+ ctx.absent.push({
16705
+ path: bundlePath("ledger", "reflections"),
16706
+ reason: `no reflections directory at ${reflectionsDir}`
16707
+ });
16708
+ return;
16709
+ }
16710
+ const names = (await readdir19(reflectionsDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name).sort();
16711
+ const windowed = ctx.windowStartMs === null || ctx.windowEndMs === null ? names : names.filter((name) => {
16712
+ const date = name.match(/(\d{4}-\d{2}-\d{2})/)?.[1];
16713
+ if (!date) return false;
16714
+ const startDate = new Date(ctx.windowStartMs).toISOString().slice(0, 10);
16715
+ const endDate = new Date(ctx.windowEndMs).toISOString().slice(0, 10);
16716
+ return date >= startDate && date <= endDate;
16717
+ });
16718
+ if (windowed.length === 0) {
16719
+ ctx.absent.push({
16720
+ path: bundlePath("ledger", "reflections"),
16721
+ reason: `no reflections dated inside the session window in ${reflectionsDir}`
16722
+ });
16723
+ return;
16724
+ }
16725
+ await mkdir5(join32(ctx.ledgerDir, "reflections"), { recursive: true });
16726
+ const rule = ctx.windowStartMs === null ? "copied-whole" : "session-window-dated-files";
16727
+ for (const name of windowed) {
16728
+ const source = join32(reflectionsDir, name);
16729
+ await cp(source, join32(ctx.ledgerDir, "reflections", name));
16730
+ ctx.slices.push({ path: bundlePath("ledger", "reflections", name), source, rule });
16731
+ }
16732
+ }
16733
+ async function assembleSessionBundle(opts) {
16734
+ const { adapter, ref } = opts;
16735
+ const generatedAt = opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
16736
+ let transcriptBytes;
16737
+ try {
16738
+ transcriptBytes = await readFile14(ref.path);
16739
+ } catch (error) {
16740
+ if (isMissingPathError(error)) {
16741
+ throw new Error(
16742
+ `session transcript not found at ${ref.path} \u2014 a bundle cannot be assembled without its transcript`
16743
+ );
16744
+ }
16745
+ throw error;
16746
+ }
16747
+ const transcriptSha256 = sha256Hex(transcriptBytes);
16748
+ const spans = await parseSession(adapter, ref, { signal: opts.signal });
16749
+ const outDir = resolve8(opts.outDir);
16750
+ await mkdir5(outDir, { recursive: true });
16751
+ if ((await readdir19(outDir)).length > 0) {
16752
+ throw new Error(
16753
+ `bundle output directory ${outDir} is not empty \u2014 pass a new directory so one bundle holds exactly one session`
16754
+ );
16755
+ }
16756
+ const absent = [];
16757
+ const slices = [];
16758
+ await mkdir5(join32(outDir, "session"), { recursive: true });
16759
+ await writeFile9(join32(outDir, "session", "transcript.jsonl"), transcriptBytes);
16760
+ const subagentsSource = join32(ref.path.replace(/\.jsonl$/, ""), "subagents");
16761
+ if ((await statOrNull(subagentsSource))?.isDirectory()) {
16762
+ await cp(subagentsSource, join32(outDir, "session", "subagents"), { recursive: true });
16763
+ } else {
16764
+ absent.push({ path: bundlePath("session", "subagents"), reason: `no subagents directory at ${subagentsSource}` });
16765
+ }
16766
+ await mkdir5(join32(outDir, "derived"), { recursive: true });
16767
+ const otlpPath = join32(outDir, "derived", "trace.otlp.jsonl");
16768
+ const investigation = await runTraceInvestigation({
16769
+ spans,
16770
+ harness: ref.harness,
16771
+ sources: [sessionReportSource(ref, spans)],
16772
+ cwds: ref.cwd ? [ref.cwd] : [],
16773
+ minLoopOccurrences: opts.minLoopOccurrences,
16774
+ otlpOutPath: otlpPath,
16775
+ generatedAt,
16776
+ signal: opts.signal,
16777
+ log: opts.log
16778
+ });
16779
+ await writeFile9(join32(outDir, "derived", "report.md"), investigation.report, "utf8");
16780
+ const evidence3 = await buildPolicyEvidenceRecord(ref, spans, {
16781
+ generatedAt,
16782
+ minLoopOccurrences: opts.minLoopOccurrences,
16783
+ // Bundle-relative pointer: the bundle must stay internally valid when moved.
16784
+ otlpPath: bundlePath("derived", "trace.otlp.jsonl"),
16785
+ sourceSha256: transcriptSha256
16786
+ });
16787
+ await writeFile9(join32(outDir, "derived", "evidence.jsonl"), serializePolicyEvidence([evidence3]), "utf8");
16788
+ const index = await buildSessionIndexFromRows([{ ref, spans }], {
16789
+ generatedAt,
16790
+ minLoopOccurrences: opts.minLoopOccurrences,
16791
+ selection: { command: "bundle", harness: ref.harness, session: ref.sessionId }
16792
+ });
16793
+ await writeFile9(join32(outDir, "derived", "session-index.json"), serializeSessionIndex(index), "utf8");
16794
+ const { firstSpanAt, lastSpanAt } = evidence3.metrics;
16795
+ const windowStartMs = firstSpanAt !== null ? Date.parse(firstSpanAt) - SESSION_WINDOW_PAD_MS : null;
16796
+ const windowEndMs = lastSpanAt !== null ? Date.parse(lastSpanAt) + SESSION_WINDOW_PAD_MS : null;
16797
+ const contextRoot = await findContextRoot(ref.cwd);
16798
+ if (!contextRoot) {
16799
+ absent.push({ path: "ledger", reason: `no context root found from session cwd ${ref.cwd ?? "(unknown)"}` });
16800
+ } else {
16801
+ const evolveDir = join32(contextRoot, ".evolve");
16802
+ if (!(await statOrNull(evolveDir))?.isDirectory()) {
16803
+ absent.push({ path: "ledger", reason: `no .evolve ledger at ${evolveDir}` });
16804
+ } else {
16805
+ const ledgerDir = join32(outDir, "ledger");
16806
+ await mkdir5(ledgerDir, { recursive: true });
16807
+ const ctx = { evolveDir, ledgerDir, windowStartMs, windowEndMs, slices, absent };
16808
+ await sliceLedgerJsonl(ctx, "experiments.jsonl");
16809
+ await sliceLedgerJsonl(ctx, "skill-runs.jsonl");
16810
+ await copyLedgerWhole(ctx, "current.json");
16811
+ await copyLedgerWhole(ctx, "scorecard.json");
16812
+ await copyLedgerWhole(ctx, "progress.md");
16813
+ await copyLatestHandoff(ctx);
16814
+ await copyWindowReflections(ctx);
16815
+ }
16816
+ }
16817
+ const gitLogPath = bundlePath("repo", "git-log.txt");
16818
+ if (!ref.cwd) {
16819
+ absent.push({ path: gitLogPath, reason: "session has no recorded cwd to read git history from" });
16820
+ } else if (windowStartMs === null || windowEndMs === null) {
16821
+ absent.push({ path: gitLogPath, reason: "session window unavailable (spans carry no timestamps)" });
16822
+ } else {
16823
+ const log = await runGitLog(ref.cwd, new Date(windowStartMs).toISOString(), new Date(windowEndMs).toISOString());
16824
+ if (log.succeeded) {
16825
+ await mkdir5(join32(outDir, "repo"), { recursive: true });
16826
+ await writeFile9(join32(outDir, "repo", "git-log.txt"), log.stdout ? `${log.stdout}
16827
+ ` : "", "utf8");
16828
+ } else {
16829
+ absent.push({ path: gitLogPath, reason: `git log failed in ${ref.cwd}: ${log.error}` });
16830
+ }
16831
+ }
16832
+ const files = await hashSessionBundleFiles(outDir);
16833
+ const manifest = {
16834
+ schemaVersion: 2,
16835
+ kind: "traces.session_bundle",
16836
+ view: "full",
16837
+ createdAt: generatedAt,
16838
+ provenance: {
16839
+ sessionId: ref.sessionId,
16840
+ harness: ref.harness,
16841
+ cwd: ref.cwd,
16842
+ transcriptPath: resolve8(ref.path),
16843
+ transcriptSha256,
16844
+ contextRoot,
16845
+ tracesVersion: tracesPackageVersion(),
16846
+ sessionWindow: { firstSpanAt, lastSpanAt, padMs: SESSION_WINDOW_PAD_MS }
16847
+ },
16848
+ files,
16849
+ absent,
16850
+ // The full view is the whole record: it drops nothing, and says so rather
16851
+ // than leaving a reader to infer completeness from a missing field.
16852
+ excluded: [],
16853
+ ledgerSlices: slices,
16854
+ knownLimits: KNOWN_LIMITS
16855
+ };
16856
+ const manifestPath = await writeSessionBundleManifest(outDir, manifest);
16857
+ return { directory: outDir, manifestPath, manifest };
16858
+ }
16859
+ async function hashSessionBundleFiles(root) {
16860
+ const files = [];
16861
+ for (const path of await listSessionBundleFiles(root)) {
16862
+ const bytes = await readFile14(join32(root, path));
16863
+ files.push({ path, bytes: bytes.length, sha256: sha256Hex(bytes) });
16864
+ }
16865
+ return files;
16866
+ }
16867
+ async function writeSessionBundleManifest(outDir, manifest) {
16868
+ const manifestPath = join32(outDir, "manifest.json");
16869
+ await writeFile9(manifestPath, `${JSON.stringify(manifest, null, 2)}
16870
+ `, "utf8");
16871
+ return manifestPath;
16872
+ }
16873
+ async function readSessionBundleManifest(bundleDir) {
16874
+ const path = join32(resolve8(bundleDir), "manifest.json");
16875
+ let text2;
16876
+ try {
16877
+ text2 = await readFile14(path, "utf8");
16878
+ } catch (error) {
16879
+ if (isMissingPathError(error)) {
16880
+ throw new Error(`no manifest.json at ${path} \u2014 ${bundleDir} is not a traces session bundle`);
16881
+ }
16882
+ throw error;
16883
+ }
16884
+ let parsed;
16885
+ try {
16886
+ parsed = JSON.parse(text2);
16887
+ } catch (error) {
16888
+ throw new Error(`${path} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
16889
+ }
16890
+ const manifest = parsed;
16891
+ if (manifest?.kind !== "traces.session_bundle") {
16892
+ throw new Error(`${path} has kind ${JSON.stringify(manifest?.kind)}, expected "traces.session_bundle"`);
16893
+ }
16894
+ if (manifest.schemaVersion !== 2) {
16895
+ throw new Error(
16896
+ `${path} is bundle schemaVersion ${JSON.stringify(manifest.schemaVersion)}; this traces reads 2. Re-run \`traces bundle\` to assemble it with the view-aware manifest.`
16897
+ );
16898
+ }
16899
+ if (manifest.view !== "full" && manifest.view !== "evidence-only") {
16900
+ throw new Error(`${path} has view ${JSON.stringify(manifest.view)}, expected "full" or "evidence-only"`);
16901
+ }
16902
+ return manifest;
16903
+ }
16904
+ var cachedVersion;
16905
+ function tracesPackageVersion() {
16906
+ if (cachedVersion) return cachedVersion;
16907
+ const pkg = JSON.parse(
16908
+ readFileSync5(new URL("../package.json", import.meta.url), "utf8")
16909
+ );
16910
+ if (typeof pkg.version !== "string" || !pkg.version) throw new Error("package.json is missing version");
16911
+ cachedVersion = pkg.version;
16912
+ return cachedVersion;
16913
+ }
16914
+
16915
+ // src/bundle-view.ts
16916
+ import { createHash as createHash15 } from "crypto";
16917
+ import { mkdir as mkdir6, readdir as readdir20, readFile as readFile15, writeFile as writeFile10 } from "fs/promises";
16918
+ import { dirname as dirname9, join as join33, resolve as resolve9 } from "path";
16919
+ var EVIDENCE_ONLY_ALLOW_LIST = [
16920
+ {
16921
+ path: "derived/session-index.json",
16922
+ owned: true,
16923
+ carries: "per-session counts, tool names, models, and nearby-context file metadata"
16924
+ },
16925
+ {
16926
+ path: "derived/evidence.jsonl",
16927
+ owned: true,
16928
+ carries: "execution statistics, token and tool totals, loop and error counts"
16929
+ },
16930
+ {
16931
+ path: "ledger/current.json",
16932
+ owned: false,
16933
+ carries: "the repo ledger current-state record"
16934
+ },
16935
+ {
16936
+ path: "ledger/scorecard.json",
16937
+ owned: false,
16938
+ carries: "the repo ledger scorecard rows"
16939
+ },
16940
+ {
16941
+ path: "ledger/skill-runs.jsonl",
16942
+ owned: false,
16943
+ carries: "skill-run rows: skill name, timestamp, and outcome fields"
16944
+ }
16945
+ ];
16946
+ var EXCLUSION_RULES = [
16947
+ {
16948
+ matches: (path) => path === "session/transcript.jsonl" || path.startsWith("session/"),
16949
+ rule: "session-source",
16950
+ reason: "raw session text: the transcript and any subagent transcript hold every word the session wrote, including the shell call that authored an earlier report"
16951
+ },
16952
+ {
16953
+ matches: (path) => path === "derived/report.md",
16954
+ rule: "session-text-derived",
16955
+ reason: "the deterministic report quotes the session verbatim: the first prompt line and evidence excerpts per finding"
16956
+ },
16957
+ {
16958
+ matches: (path) => path === "derived/trace.otlp.jsonl",
16959
+ rule: "session-text-derived",
16960
+ reason: "spans carry input.value, output.value and content: the full prompt and response text of every turn"
16961
+ },
16962
+ {
16963
+ matches: (path) => path === "ledger/progress.md",
16964
+ rule: "authored-prose",
16965
+ reason: "progress notes are written at session close and restate what the session concluded"
16966
+ },
16967
+ {
16968
+ matches: (path) => /^ledger\/handoff-.*\.md$/.test(path),
16969
+ rule: "authored-prose",
16970
+ reason: "a handoff is the session conclusion in prose"
16971
+ },
16972
+ {
16973
+ matches: (path) => path.startsWith("ledger/reflections/"),
16974
+ rule: "authored-prose",
16975
+ reason: "a reflection is the session conclusion in prose"
16976
+ },
16977
+ {
16978
+ matches: (path) => path === "ledger/experiments.jsonl",
16979
+ rule: "authored-prose",
16980
+ reason: "experiment rows carry free-text verdict and note fields that restate what the session concluded"
16981
+ },
16982
+ {
16983
+ matches: (path) => path === "repo/git-log.txt",
16984
+ rule: "authored-prose",
16985
+ reason: "commit subjects are prose the session authored"
16986
+ },
16987
+ {
16988
+ matches: () => true,
16989
+ rule: "not-allow-listed",
16990
+ reason: "the evidence-only view carries an explicit allow-list of counted artifacts, and this path is not on it"
16991
+ }
16992
+ ];
16993
+ var SIGNATURE_WORDS = 8;
16994
+ var MIN_WORD_TOKENS = 7;
16995
+ function scrubIdentifiers(line) {
16996
+ return line.replace(/[a-z][a-z0-9+.-]*:\/\/\S*/gi, " ").replace(/\/?(?:[\w.@+~-]+\/)+[\w.@+~-]*/g, " ").replace(/\b[0-9a-f-]{8,}\b/gi, " ");
16997
+ }
16998
+ var PACKAGE_CONSTANT_PROSE = [POLICY_EVIDENCE_NOTE];
16999
+ function stripPackageConstants(text2) {
17000
+ let out = text2;
17001
+ for (const constant of PACKAGE_CONSTANT_PROSE) out = out.split(constant).join(" ");
17002
+ return out;
17003
+ }
17004
+ function collectJsonStrings(value, into) {
17005
+ if (typeof value === "string") into.push(value);
17006
+ else if (Array.isArray(value)) for (const item of value) collectJsonStrings(item, into);
17007
+ else if (value !== null && typeof value === "object") {
17008
+ for (const item of Object.values(value)) collectJsonStrings(item, into);
17009
+ }
17010
+ }
17011
+ function proseFragments(line) {
17012
+ const trimmed = line.trim();
17013
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
17014
+ try {
17015
+ const parsed = JSON.parse(trimmed);
17016
+ const strings = [];
17017
+ collectJsonStrings(parsed, strings);
17018
+ return strings;
17019
+ } catch {
17020
+ }
17021
+ }
17022
+ return [line];
17023
+ }
17024
+ function sessionTextSignatures(text2) {
17025
+ const out = /* @__PURE__ */ new Set();
17026
+ for (const line of stripPackageConstants(text2).split("\n")) {
17027
+ for (const fragment of proseFragments(line)) {
17028
+ const words = scrubIdentifiers(fragment).toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().split(" ").filter(Boolean);
17029
+ for (let i = 0; i + SIGNATURE_WORDS <= words.length; i++) {
17030
+ const window = words.slice(i, i + SIGNATURE_WORDS);
17031
+ if (window.filter((word) => /^[a-z]+$/.test(word)).length < MIN_WORD_TOKENS) continue;
17032
+ out.add(window.join(" "));
17033
+ }
17034
+ }
17035
+ }
17036
+ return out;
17037
+ }
17038
+ async function findSessionTextLeaks(opts) {
17039
+ const index = /* @__PURE__ */ new Map();
17040
+ const carried = [];
17041
+ for (const path of opts.carriedPaths) {
17042
+ const signatures = sessionTextSignatures(await readFile15(join33(opts.viewDir, path), "utf8"));
17043
+ carried.push({ path, signatures: signatures.size });
17044
+ for (const signature of signatures) if (!index.has(signature)) index.set(signature, path);
17045
+ }
17046
+ const hits = /* @__PURE__ */ new Map();
17047
+ const comparedSources = [];
17048
+ let sourceSignatures = 0;
17049
+ for (const sourcePath of opts.excludedPaths) {
17050
+ const signatures = sessionTextSignatures(await readFile15(join33(opts.sourceDir, sourcePath), "utf8"));
17051
+ comparedSources.push(sourcePath);
17052
+ sourceSignatures += signatures.size;
17053
+ for (const signature of signatures) {
17054
+ const viewPath = index.get(signature);
17055
+ if (viewPath === void 0) continue;
17056
+ const hit = hits.get(viewPath) ?? { matched: /* @__PURE__ */ new Set(), sources: /* @__PURE__ */ new Set(), example: signature };
17057
+ hit.matched.add(signature);
17058
+ hit.sources.add(sourcePath);
17059
+ hits.set(viewPath, hit);
17060
+ }
17061
+ }
17062
+ const leaks = [...hits].map(([viewPath, hit]) => ({
17063
+ viewPath,
17064
+ sources: [...hit.sources].sort(),
17065
+ signatures: hit.matched.size,
17066
+ example: hit.example
17067
+ }));
17068
+ return { leaks, carried, comparedSources, sourceSignatures };
17069
+ }
17070
+ function classifyEvidenceOnlyPath(path) {
17071
+ const allowed = EVIDENCE_ONLY_ALLOW_LIST.find((entry) => entry.path === path);
17072
+ if (allowed) return { carried: true, owned: allowed.owned, carries: allowed.carries };
17073
+ const rule = EXCLUSION_RULES.find((entry) => entry.matches(path));
17074
+ return { carried: false, rule: rule.rule, reason: rule.reason };
17075
+ }
17076
+ var EVIDENCE_ONLY_LIMITS = [
17077
+ "the exclusion is verbatim-tight, not paraphrase-tight: a conclusion restated in different words inside a carried artifact is not detectable by content signatures",
17078
+ "ledger prose (progress, handoffs, reflections) and experiment free-text fields are excluded whole; a measured experiment result is readable only from the full bundle, under audit rules",
17079
+ "the view carries counts, names, identifiers, timestamps and hashes; a question that needs the words of a turn cannot be answered from it"
17080
+ ];
17081
+ async function projectSessionBundle(opts) {
17082
+ if (opts.view !== "evidence-only") {
17083
+ throw new Error(
17084
+ `unsupported bundle view ${JSON.stringify(opts.view)}. The full view IS the bundle \`traces bundle\` assembles, so there is nothing to project into it; the projectable view is "evidence-only".`
17085
+ );
17086
+ }
17087
+ const generatedAt = opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
17088
+ const sourceDir = resolve9(opts.bundleDir);
17089
+ const source = await readSessionBundleManifest(sourceDir);
17090
+ if (source.view !== "full") {
17091
+ throw new Error(
17092
+ `${sourceDir} is already the ${source.view} view \u2014 project from the full bundle, not from a view of it`
17093
+ );
17094
+ }
17095
+ const outDir = resolve9(opts.outDir);
17096
+ await mkdir6(outDir, { recursive: true });
17097
+ if ((await readdir20(outDir)).length > 0) {
17098
+ throw new Error(
17099
+ `view output directory ${outDir} is not empty \u2014 pass a new directory so one directory holds exactly one view`
17100
+ );
17101
+ }
17102
+ const sourceHashes = new Map(source.files.map((file) => [file.path, file]));
17103
+ const carriedPaths = [];
17104
+ const carries = /* @__PURE__ */ new Map();
17105
+ const excluded = [];
17106
+ const candidates = [];
17107
+ for (const path of await listSessionBundleFiles(sourceDir)) {
17108
+ if (path === "manifest.json") continue;
17109
+ const bytes = await readFile15(join33(sourceDir, path));
17110
+ const sha2562 = createHash15("sha256").update(bytes).digest("hex");
17111
+ const recorded = sourceHashes.get(path);
17112
+ if (!recorded) {
17113
+ throw new Error(
17114
+ `${join33(sourceDir, path)} is not listed in the bundle manifest \u2014 the bundle changed after assembly and cannot be projected; re-run \`traces bundle\``
17115
+ );
17116
+ }
17117
+ if (recorded.sha256 !== sha2562) {
17118
+ throw new Error(
17119
+ `${join33(sourceDir, path)} does not match its manifest hash (${recorded.sha256} recorded, ${sha2562} on disk) \u2014 the bundle changed after assembly and cannot be projected; re-run \`traces bundle\``
17120
+ );
17121
+ }
17122
+ const verdict = classifyEvidenceOnlyPath(path);
17123
+ if (!verdict.carried) {
17124
+ excluded.push({ path, bytes: bytes.length, sha256: sha2562, rule: verdict.rule, reason: verdict.reason });
17125
+ continue;
17126
+ }
17127
+ candidates.push({ path, bytes, sha256: sha2562, carries: verdict.carries, owned: verdict.owned });
17128
+ }
17129
+ const report = await findSessionTextLeaks({
17130
+ viewDir: sourceDir,
17131
+ sourceDir,
17132
+ carriedPaths: candidates.map((candidate) => candidate.path),
17133
+ excludedPaths: excluded.map((entry) => entry.path)
17134
+ });
17135
+ const leakByPath = new Map(report.leaks.map((leak) => [leak.viewPath, leak]));
17136
+ const ownedLeak = report.leaks.find((leak) => candidates.some((c) => c.path === leak.viewPath && c.owned));
17137
+ if (ownedLeak) {
17138
+ throw new Error(
17139
+ `${ownedLeak.viewPath} repeats ${ownedLeak.signatures} ${SIGNATURE_WORDS}-word run(s) found in ${ownedLeak.sources.join(", ")}, for example "${ownedLeak.example}". That artifact is derived by traces and must carry counts only, so this is a defect in the derivation, not a property of the session. No view was written.`
17140
+ );
17141
+ }
17142
+ for (const candidate of candidates) {
17143
+ const leak = leakByPath.get(candidate.path);
17144
+ if (leak) {
17145
+ excluded.push({
17146
+ path: candidate.path,
17147
+ bytes: candidate.bytes.length,
17148
+ sha256: candidate.sha256,
17149
+ rule: "content-signature",
17150
+ reason: `the content check found ${leak.signatures} ${SIGNATURE_WORDS}-word run(s) this file shares with ${leak.sources.join(", ")}; carrying it would put back text the view removes`,
17151
+ contentMatch: { signatures: leak.signatures, sources: leak.sources }
17152
+ });
17153
+ continue;
17154
+ }
17155
+ const target = join33(outDir, candidate.path);
17156
+ await mkdir6(dirname9(target), { recursive: true });
17157
+ await writeFile10(target, candidate.bytes);
17158
+ carriedPaths.push(candidate.path);
17159
+ carries.set(candidate.path, candidate.carries);
17160
+ }
17161
+ const leakCheck = {
17162
+ signatureWords: SIGNATURE_WORDS,
17163
+ comparedSources: report.comparedSources,
17164
+ sourceSignatures: report.sourceSignatures,
17165
+ carried: report.carried.filter((entry) => carries.has(entry.path)).map((entry) => ({
17166
+ path: entry.path,
17167
+ carries: carries.get(entry.path),
17168
+ signatures: entry.signatures
17169
+ })),
17170
+ matches: 0
17171
+ };
17172
+ const sourceManifestBytes = await readFile15(join33(sourceDir, "manifest.json"));
17173
+ const manifest = {
17174
+ schemaVersion: 2,
17175
+ kind: "traces.session_bundle",
17176
+ view: "evidence-only",
17177
+ createdAt: generatedAt,
17178
+ provenance: source.provenance,
17179
+ files: await hashSessionBundleFiles(outDir),
17180
+ absent: source.absent,
17181
+ excluded,
17182
+ ledgerSlices: source.ledgerSlices.filter((slice) => carriedPaths.includes(slice.path)),
17183
+ knownLimits: [...source.knownLimits, ...EVIDENCE_ONLY_LIMITS],
17184
+ projection: {
17185
+ sourceDirectory: sourceDir,
17186
+ sourceManifestSha256: createHash15("sha256").update(sourceManifestBytes).digest("hex"),
17187
+ leakCheck
17188
+ }
17189
+ };
17190
+ const manifestPath = await writeSessionBundleManifest(outDir, manifest);
17191
+ return { directory: outDir, manifestPath, manifest, leakCheck };
17192
+ }
17193
+
15872
17194
  // src/upload-state.ts
15873
- import { mkdir as mkdir5, readFile as readFile14, writeFile as writeFile9 } from "fs/promises";
17195
+ import { mkdir as mkdir7, readFile as readFile16, writeFile as writeFile11 } from "fs/promises";
15874
17196
  import { homedir as homedir12 } from "os";
15875
- import { dirname as dirname9, join as join32 } from "path";
17197
+ import { dirname as dirname10, join as join34 } from "path";
15876
17198
  import { contentHash } from "@tangle-network/agent-eval";
15877
17199
  function stateFile() {
15878
- const base = process.env.XDG_STATE_HOME ?? join32(homedir12(), ".local", "state");
15879
- return join32(base, "tangle-traces", "uploaded.json");
17200
+ const base = process.env.XDG_STATE_HOME ?? join34(homedir12(), ".local", "state");
17201
+ return join34(base, "tangle-traces", "uploaded.json");
15880
17202
  }
15881
17203
  function uploadKey(harness, sessionId) {
15882
17204
  return `${harness}:${sessionId}`;
@@ -15889,15 +17211,15 @@ function outboundHash(events, identity) {
15889
17211
  }
15890
17212
  async function loadState() {
15891
17213
  try {
15892
- return JSON.parse(await readFile14(stateFile(), "utf8"));
17214
+ return JSON.parse(await readFile16(stateFile(), "utf8"));
15893
17215
  } catch {
15894
17216
  return {};
15895
17217
  }
15896
17218
  }
15897
17219
  async function saveState(state) {
15898
17220
  const f = stateFile();
15899
- await mkdir5(dirname9(f), { recursive: true });
15900
- await writeFile9(f, `${JSON.stringify(state, null, 2)}
17221
+ await mkdir7(dirname10(f), { recursive: true });
17222
+ await writeFile11(f, `${JSON.stringify(state, null, 2)}
15901
17223
  `, "utf8");
15902
17224
  }
15903
17225
  function alreadyUploaded(state, harness, sessionId, hash2) {
@@ -15905,10 +17227,10 @@ function alreadyUploaded(state, harness, sessionId, hash2) {
15905
17227
  }
15906
17228
 
15907
17229
  // src/upload.ts
15908
- import { readFile as readFile15 } from "fs/promises";
17230
+ import { readFile as readFile17 } from "fs/promises";
15909
17231
  import { createRequire as createRequire2 } from "module";
15910
17232
  import { hostname as hostname2 } from "os";
15911
- import { basename as basename15, join as join33 } from "path";
17233
+ import { basename as basename15, join as join35 } from "path";
15912
17234
  import { hostedClientFromEnv } from "@tangle-network/agent-eval/hosted";
15913
17235
  import { REDACTION_VERSION } from "@tangle-network/agent-eval/traces";
15914
17236
  var require2 = createRequire2(import.meta.url);
@@ -15932,7 +17254,7 @@ async function planUpload(opts = {}) {
15932
17254
  async function gitBranch(cwd) {
15933
17255
  if (!cwd) return void 0;
15934
17256
  try {
15935
- const head = await readFile15(join33(cwd, ".git", "HEAD"), "utf8");
17257
+ const head = await readFile17(join35(cwd, ".git", "HEAD"), "utf8");
15936
17258
  const m = head.match(/^ref:\s*refs\/heads\/(.+)$/m);
15937
17259
  return m?.[1]?.trim();
15938
17260
  } catch {
@@ -15941,16 +17263,16 @@ async function gitBranch(cwd) {
15941
17263
  }
15942
17264
  async function sessionMeta(item) {
15943
17265
  const meta = {
15944
- [ATTR2.HARNESS]: item.ref.harness,
15945
- [ATTR2.SESSION_FILE]: basename15(item.ref.path),
15946
- [ATTR2.HOST]: hostname2(),
15947
- [ATTR2.UPLOADER]: `tangle-traces@${TRACES_VERSION}`,
15948
- [ATTR2.REDACTION_VERSION]: REDACTION_VERSION,
15949
- [ATTR2.REDACTION_COUNT]: item.redaction.redactionCount
15950
- };
15951
- if (item.ref.cwd) meta[ATTR2.CWD] = item.ref.cwd;
17266
+ [ATTR.HARNESS]: item.ref.harness,
17267
+ [ATTR.SESSION_FILE]: basename15(item.ref.path),
17268
+ [ATTR.HOST]: hostname2(),
17269
+ [ATTR.UPLOADER]: `tangle-traces@${TRACES_VERSION}`,
17270
+ [ATTR.REDACTION_VERSION]: REDACTION_VERSION,
17271
+ [ATTR.REDACTION_COUNT]: item.redaction.redactionCount
17272
+ };
17273
+ if (item.ref.cwd) meta[ATTR.CWD] = item.ref.cwd;
15952
17274
  const branch = await gitBranch(item.ref.cwd);
15953
- if (branch) meta[ATTR2.GIT_BRANCH] = branch;
17275
+ if (branch) meta[ATTR.GIT_BRANCH] = branch;
15954
17276
  return meta;
15955
17277
  }
15956
17278
  var msToNano = (epochMs) => (BigInt(epochMs) * 1000000n).toString();
@@ -15961,8 +17283,8 @@ function toTraceSpanEvents(spans, rootMeta) {
15961
17283
  if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") attributes2[k] = v;
15962
17284
  else if (v != null) attributes2[k] = JSON.stringify(v);
15963
17285
  }
15964
- attributes2[ATTR2.SESSION_ID] = s.trace_id;
15965
- attributes2[ATTR2.INGEST_SOURCE] = INGEST_SOURCE_CLI;
17286
+ attributes2[ATTR.SESSION_ID] = s.trace_id;
17287
+ attributes2[ATTR.INGEST_SOURCE] = INGEST_SOURCE_CLI;
15966
17288
  if (s.parent_span_id === null) Object.assign(attributes2, rootMeta);
15967
17289
  const startMs = parseIsoToEpochMs(s.start_time);
15968
17290
  const endMs = Math.max(startMs, parseIsoToEpochMs(s.end_time));
@@ -16021,7 +17343,7 @@ async function prepareOutbound(plan, opts, uploadedAt) {
16021
17343
  const key = uploadKey(item.ref.harness, item.ref.sessionId);
16022
17344
  const previous = plan.state[key];
16023
17345
  const meta = await sessionMeta(item);
16024
- const eventsAt = (timestamp) => toTraceSpanEvents(item.spans, { ...meta, [ATTR2.UPLOADED_AT]: timestamp });
17346
+ const eventsAt = (timestamp) => toTraceSpanEvents(item.spans, { ...meta, [ATTR.UPLOADED_AT]: timestamp });
16025
17347
  let eventTimestamp2 = previous?.uploadedAt ?? uploadedAt;
16026
17348
  let events = eventsAt(eventTimestamp2);
16027
17349
  let hash2 = outboundHash(events, identity);
@@ -16099,6 +17421,15 @@ async function executeUpload(plan, opts = {}) {
16099
17421
 
16100
17422
  export {
16101
17423
  appendAll,
17424
+ ATTR,
17425
+ INGEST_SOURCE_CLI,
17426
+ environmentAttributes,
17427
+ stampEnvironmentAttrs,
17428
+ environmentFromSpanAttributes,
17429
+ DEFAULT_HARNESS,
17430
+ sessionIdFromAttributes,
17431
+ indexSessionIdsByTrace,
17432
+ stampSessionIdentity,
16102
17433
  parseIsoToEpochMs,
16103
17434
  parseSince,
16104
17435
  resolveOtlpInputFiles,
@@ -16115,18 +17446,13 @@ export {
16115
17446
  capText,
16116
17447
  ACTOR_ATTR,
16117
17448
  userPromptSpan,
16118
- ATTR2 as ATTR,
16119
- INGEST_SOURCE_CLI,
16120
- DEFAULT_HARNESS,
16121
- sessionIdFromAttributes,
16122
- indexSessionIdsByTrace,
16123
- stampSessionIdentity,
16124
17449
  isChatTrajectoryMessage,
16125
17450
  isChatTrajectory,
16126
17451
  chatTrajectoryToSpans,
16127
17452
  importCodeTraceBench,
16128
17453
  summarizeOtlpExecution,
16129
17454
  summarizeSpanExecution,
17455
+ classifyFailureFollowUps,
16130
17456
  toRuntimeStore,
16131
17457
  runPipelines,
16132
17458
  recordSessionCorruption,
@@ -16162,6 +17488,7 @@ export {
16162
17488
  selectAdapters,
16163
17489
  normalizeRemote,
16164
17490
  equivalentGitCwds,
17491
+ cwdMatchesSelectionResolved,
16165
17492
  cwdMatchesSelection,
16166
17493
  resolveRepoAttrs,
16167
17494
  resolveSessionRepoAttrs,
@@ -16173,6 +17500,7 @@ export {
16173
17500
  scanSessions,
16174
17501
  SessionWorkflowError,
16175
17502
  describeSessionRelationship,
17503
+ POLICY_EVIDENCE_NOTE,
16176
17504
  buildPolicyEvidenceRecord,
16177
17505
  collectPolicyEvidence,
16178
17506
  serializePolicyEvidence,
@@ -16233,6 +17561,8 @@ export {
16233
17561
  commandRedactor,
16234
17562
  writeHodoscopeInput,
16235
17563
  hodoscopeAnalyzer,
17564
+ httpJsonTransport,
17565
+ primeAnalyzer,
16236
17566
  CODING_REDACTION_RULES,
16237
17567
  TRACES_REDACTION_RULES,
16238
17568
  redactSpans,
@@ -16263,7 +17593,7 @@ export {
16263
17593
  analyzeSteeringChain,
16264
17594
  REACTIONS,
16265
17595
  analyzeReactions,
16266
- CORRUPTION_RECEIPT_DISPLAY_LIMIT,
17596
+ sessionReportSource,
16267
17597
  buildTraceFindingPacket,
16268
17598
  loadTracesConfig,
16269
17599
  mergeTracesConfig,
@@ -16320,10 +17650,20 @@ export {
16320
17650
  collectSessionWorkflow,
16321
17651
  collectSessionSelection,
16322
17652
  buildSessionIndex,
17653
+ findContextRoot,
16323
17654
  buildSessionIndexFromRows,
16324
17655
  collectSessionIndex,
16325
17656
  serializeSessionIndex,
16326
17657
  writeSessionIndexFile,
17658
+ listSessionBundleFiles,
17659
+ assembleSessionBundle,
17660
+ hashSessionBundleFiles,
17661
+ writeSessionBundleManifest,
17662
+ readSessionBundleManifest,
17663
+ sessionTextSignatures,
17664
+ findSessionTextLeaks,
17665
+ classifyEvidenceOnlyPath,
17666
+ projectSessionBundle,
16327
17667
  uploadKey,
16328
17668
  sessionHash,
16329
17669
  outboundHash,