@tangle-network/traces 0.11.5 → 0.11.6

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) {
@@ -1907,10 +1960,10 @@ function sessionJsonlOptions(ref, options = {}) {
1907
1960
  function sessionIntegrityAttributes(ref) {
1908
1961
  if (!ref.integrity) return {};
1909
1962
  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"
1963
+ [ATTR.SESSION_INTEGRITY]: ref.integrity.status,
1964
+ [ATTR.CORRUPTION_COUNT]: ref.integrity.corruptions.length,
1965
+ [ATTR.CORRUPTION_DIGEST]: corruptionDigest(ref.integrity.corruptions),
1966
+ [ATTR.RAW_SOURCE_RETENTION]: "local_source_only"
1914
1967
  };
1915
1968
  }
1916
1969
  function corruptionDigest(receipts) {
@@ -1951,15 +2004,15 @@ function stampSessionIntegrity(ref, spans) {
1951
2004
  status: "ERROR",
1952
2005
  service: typeof parent.attributes["service.name"] === "string" ? parent.attributes["service.name"] : ref.harness,
1953
2006
  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"
2007
+ [ATTR.SESSION_INTEGRITY]: ref.integrity.status,
2008
+ [ATTR.CORRUPTION_RECEIPT_VERSION]: receipt.receiptVersion,
2009
+ [ATTR.CORRUPTION_RECEIPT_KIND]: receipt.kind,
2010
+ [ATTR.CORRUPTION_SOURCE_PATH]: receipt.sourcePath,
2011
+ [ATTR.CORRUPTION_LINE_NUMBER]: receipt.lineNumber,
2012
+ [ATTR.CORRUPTION_BYTE_OFFSET]: receipt.byteOffset,
2013
+ [ATTR.CORRUPTION_BYTE_LENGTH]: receipt.byteLength,
2014
+ [ATTR.CORRUPTION_SHA256]: receipt.sha256,
2015
+ [ATTR.RAW_SOURCE_RETENTION]: "local_source_only"
1963
2016
  }
1964
2017
  }));
1965
2018
  existingSpanIds.add(spanId);
@@ -6236,7 +6289,7 @@ function extractAbsolutePaths(spans) {
6236
6289
  }
6237
6290
  async function resolveRepoAttrs(cwd) {
6238
6291
  if (!cwd) return {};
6239
- const attrs = { [ATTR2.CWD]: cwd };
6292
+ const attrs = { [ATTR.CWD]: cwd };
6240
6293
  const basename16 = (() => {
6241
6294
  const trimmed = cwd.replace(/\/+$/g, "");
6242
6295
  const idx = trimmed.lastIndexOf("/");
@@ -6249,17 +6302,17 @@ async function resolveRepoAttrs(cwd) {
6249
6302
  const { remote, branch, commit } = await readGit(gitRoot);
6250
6303
  const normalized = remote ? normalizeRemote(remote) : null;
6251
6304
  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;
6305
+ attrs[ATTR.SUBJECT_KEY] = normalized;
6306
+ attrs[ATTR.GIT_REPOSITORY] = normalized;
6307
+ if (branch) attrs[ATTR.GIT_BRANCH_NAME] = branch;
6308
+ if (commit) attrs[ATTR.GIT_COMMIT] = commit;
6256
6309
  return attrs;
6257
6310
  }
6258
- if (branch) attrs[ATTR2.GIT_BRANCH_NAME] = branch;
6259
- if (commit) attrs[ATTR2.GIT_COMMIT] = commit;
6311
+ if (branch) attrs[ATTR.GIT_BRANCH_NAME] = branch;
6312
+ if (commit) attrs[ATTR.GIT_COMMIT] = commit;
6260
6313
  } catch {
6261
6314
  }
6262
- attrs[ATTR2.SUBJECT_KEY] = basename16;
6315
+ attrs[ATTR.SUBJECT_KEY] = basename16;
6263
6316
  return attrs;
6264
6317
  }
6265
6318
  async function repoCandidateForPath(path) {
@@ -6290,12 +6343,12 @@ async function resolveSessionRepoAttrs(cwd, spans) {
6290
6343
  resolved.push({ ...candidate, attrs });
6291
6344
  }
6292
6345
  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];
6346
+ const withGit = resolved.filter((candidate) => candidate.attrs[ATTR.GIT_REPOSITORY]).sort(byScore);
6347
+ const selected = withGit[0] ?? resolved.filter((candidate) => candidate.attrs[ATTR.SUBJECT_KEY]).sort(byScore)[0];
6295
6348
  if (!selected) return { attrs: {}, cwd: null, source: "none" };
6296
6349
  return {
6297
- attrs: { ...selected.attrs, [ATTR2.REPO_RESOLUTION_SOURCE]: selected.source },
6298
- cwd: selected.attrs[ATTR2.CWD] ?? selected.cwd,
6350
+ attrs: { ...selected.attrs, [ATTR.REPO_RESOLUTION_SOURCE]: selected.source },
6351
+ cwd: selected.attrs[ATTR.CWD] ?? selected.cwd,
6299
6352
  source: selected.source
6300
6353
  };
6301
6354
  }
@@ -6323,9 +6376,9 @@ async function resolutionForWorkdirEvidence(evidence3, cache) {
6323
6376
  return null;
6324
6377
  }
6325
6378
  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,
6379
+ const resolved = attrs[ATTR.SUBJECT_KEY] ? {
6380
+ attrs: { ...attrs, [ATTR.REPO_RESOLUTION_SOURCE]: evidence3.source },
6381
+ cwd: attrs[ATTR.CWD] ?? cwd,
6329
6382
  source: evidence3.source
6330
6383
  } : null;
6331
6384
  cache.set(evidence3.path, resolved);
@@ -6348,7 +6401,7 @@ async function resolveSpanWorkdirRepoAttrs(span2, cache) {
6348
6401
  }
6349
6402
  const byScore = (a, b) => b.score - a.score || a.order - b.order;
6350
6403
  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];
6404
+ 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
6405
  if (!selected) return null;
6353
6406
  return selected.repo;
6354
6407
  }
@@ -6364,7 +6417,7 @@ async function stampSpanWorkdirRepoAttrs(spans) {
6364
6417
  async function awaitWithAbort(operation, signal) {
6365
6418
  if (!signal) return operation;
6366
6419
  signal.throwIfAborted();
6367
- return new Promise((resolve8, reject) => {
6420
+ return new Promise((resolve10, reject) => {
6368
6421
  const stop = () => {
6369
6422
  signal.removeEventListener("abort", stop);
6370
6423
  reject(signal.reason);
@@ -6373,7 +6426,7 @@ async function awaitWithAbort(operation, signal) {
6373
6426
  operation.then(
6374
6427
  (value) => {
6375
6428
  signal.removeEventListener("abort", stop);
6376
- resolve8(value);
6429
+ resolve10(value);
6377
6430
  },
6378
6431
  (error) => {
6379
6432
  signal.removeEventListener("abort", stop);
@@ -6392,6 +6445,8 @@ async function parseSession(adapter, ref, options = {}) {
6392
6445
  if (spans.length === 0) throw new EmptySessionError(ref.path);
6393
6446
  stampSessionIntegrity(ref, spans);
6394
6447
  stampSessionIdentity(spans, ref.sessionId);
6448
+ stampEnvironmentAttrs(spans, ref.environment);
6449
+ if (ref.cwd === null && ref.environment?.cwd) ref.cwd = ref.environment.cwd;
6395
6450
  const repo = await resolveSessionRepoAttrs(ref.cwd, spans);
6396
6451
  if (repo.cwd) ref.cwd = repo.cwd;
6397
6452
  stampRepoAttrs(spans, repo.attrs);
@@ -6553,6 +6608,7 @@ import {
6553
6608
  OPENINFERENCE_SPAN_KIND as OPENINFERENCE_SPAN_KIND2,
6554
6609
  TOOL_NAME as TOOL_NAME3
6555
6610
  } from "@tangle-network/agent-eval/trace-attributes";
6611
+ var POLICY_EVIDENCE_NOTE = "This is normalized coding-agent session evidence for downstream policy mining; it is not an eval campaign cell.";
6556
6612
  function stringAttr(span2, key) {
6557
6613
  const value = span2.attributes[key];
6558
6614
  return typeof value === "string" && value.length > 0 ? value : void 0;
@@ -6563,12 +6619,12 @@ function spanKind(span2) {
6563
6619
  function repoFromSpans(spans) {
6564
6620
  const attrs = {};
6565
6621
  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);
6622
+ attrs.subjectKey ??= stringAttr(span2, ATTR.SUBJECT_KEY);
6623
+ attrs.repository ??= stringAttr(span2, ATTR.GIT_REPOSITORY);
6624
+ attrs.branch ??= stringAttr(span2, ATTR.GIT_BRANCH_NAME);
6625
+ attrs.commit ??= stringAttr(span2, ATTR.GIT_COMMIT);
6626
+ attrs.cwd ??= stringAttr(span2, ATTR.CWD);
6627
+ attrs.resolutionSource ??= stringAttr(span2, ATTR.REPO_RESOLUTION_SOURCE);
6572
6628
  if (attrs.subjectKey && attrs.repository && attrs.branch && attrs.commit && attrs.cwd && attrs.resolutionSource) break;
6573
6629
  }
6574
6630
  return attrs;
@@ -6657,7 +6713,7 @@ async function buildPolicyEvidenceRecord(ref, spans, opts = {}) {
6657
6713
  ...opts.otlpPath ? { otlpPath: opts.otlpPath } : {},
6658
6714
  ...opts.sourceSha256 ? { sourceSha256: opts.sourceSha256 } : {},
6659
6715
  notCampaignCell: true,
6660
- note: "This is normalized coding-agent session evidence for downstream policy mining; it is not an eval campaign cell."
6716
+ note: POLICY_EVIDENCE_NOTE
6661
6717
  }
6662
6718
  };
6663
6719
  }
@@ -7181,11 +7237,11 @@ function parseCorpusFlag(value) {
7181
7237
  if (eq <= 0) throw new Error(`--corpus must be name=<labels>::<prepared>, got: ${value}`);
7182
7238
  const name = value.slice(0, eq);
7183
7239
  const rest = value.slice(eq + 1);
7184
- const sep2 = rest.indexOf("::");
7185
- if (sep2 <= 0 || sep2 === rest.length - 2) {
7240
+ const sep3 = rest.indexOf("::");
7241
+ if (sep3 <= 0 || sep3 === rest.length - 2) {
7186
7242
  throw new Error(`--corpus must be name=<labels>::<prepared>, got: ${value}`);
7187
7243
  }
7188
- return { name, labelsPath: rest.slice(0, sep2), preparedDir: rest.slice(sep2 + 2) };
7244
+ return { name, labelsPath: rest.slice(0, sep3), preparedDir: rest.slice(sep3 + 2) };
7189
7245
  }
7190
7246
  var SUBMIT_ACTION_SIGNATURE = "COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT";
7191
7247
  function isSubmitAction(action) {
@@ -7211,7 +7267,7 @@ function goldIncorrectSteps(entry) {
7211
7267
  function findRawTrajFiles(sweRawDir) {
7212
7268
  if (!existsSync2(sweRawDir)) return [];
7213
7269
  const entries = readdirSync2(sweRawDir, { recursive: true, encoding: "utf8" });
7214
- return entries.filter((relative5) => relative5.endsWith(".traj.json")).map((relative5) => join20(sweRawDir, relative5)).sort();
7270
+ return entries.filter((relative6) => relative6.endsWith(".traj.json")).map((relative6) => join20(sweRawDir, relative6)).sort();
7215
7271
  }
7216
7272
  function cwdFromPwdObservation(steps) {
7217
7273
  for (const step of steps) {
@@ -8376,9 +8432,9 @@ function renderBatchReport(report) {
8376
8432
  lines.push("");
8377
8433
  lines.push("| corpus | trajectory | image | error |");
8378
8434
  lines.push("| --- | --- | --- | --- |");
8379
- for (const failure of report.pullFailures) {
8435
+ for (const failure2 of report.pullFailures) {
8380
8436
  lines.push(
8381
- `| ${failure.corpus} | ${failure.trajId} | \`${failure.image}\` | ${failure.error.replaceAll("|", "\\|")} |`
8437
+ `| ${failure2.corpus} | ${failure2.trajId} | \`${failure2.image}\` | ${failure2.error.replaceAll("|", "\\|")} |`
8382
8438
  );
8383
8439
  }
8384
8440
  }
@@ -9444,13 +9500,13 @@ function terminateWindowsProcessTree(child) {
9444
9500
  forceKill(child);
9445
9501
  return Promise.resolve();
9446
9502
  }
9447
- return new Promise((resolve8) => {
9503
+ return new Promise((resolve10) => {
9448
9504
  let finished = false;
9449
9505
  const finish = () => {
9450
9506
  if (finished) return;
9451
9507
  finished = true;
9452
9508
  forceKill(child);
9453
- resolve8();
9509
+ resolve10();
9454
9510
  };
9455
9511
  let killer;
9456
9512
  try {
@@ -9477,7 +9533,7 @@ function signalProcess(pid, signal) {
9477
9533
  }
9478
9534
  }
9479
9535
  function readProcessTableWithPs() {
9480
- return new Promise((resolve8, reject) => {
9536
+ return new Promise((resolve10, reject) => {
9481
9537
  let child;
9482
9538
  try {
9483
9539
  child = spawn("ps", ["-A", "-o", "pid=", "-o", "ppid="], {
@@ -9531,7 +9587,7 @@ function readProcessTableWithPs() {
9531
9587
  reject(new Error("ps: returned no process records"));
9532
9588
  return;
9533
9589
  }
9534
- resolve8(rows);
9590
+ resolve10(rows);
9535
9591
  });
9536
9592
  });
9537
9593
  });
@@ -9541,10 +9597,10 @@ async function readLinuxProcProcessTable() {
9541
9597
  const rows = await Promise.all(
9542
9598
  entries.filter((entry) => entry.isDirectory() && /^\d+$/.test(entry.name)).map(async (entry) => {
9543
9599
  try {
9544
- const stat17 = await readFile7(`/proc/${entry.name}/stat`, "utf8");
9545
- const commandEnd = stat17.lastIndexOf(")");
9600
+ const stat18 = await readFile7(`/proc/${entry.name}/stat`, "utf8");
9601
+ const commandEnd = stat18.lastIndexOf(")");
9546
9602
  if (commandEnd < 0) return void 0;
9547
- const fields = stat17.slice(commandEnd + 1).trim().split(/\s+/);
9603
+ const fields = stat18.slice(commandEnd + 1).trim().split(/\s+/);
9548
9604
  const pid = Number(entry.name);
9549
9605
  const parentPid = Number(fields[1]);
9550
9606
  if (!Number.isSafeInteger(pid) || pid < 1 || !Number.isSafeInteger(parentPid) || parentPid < 0) {
@@ -9606,7 +9662,7 @@ async function terminatePosixProcessTree(child) {
9606
9662
  }
9607
9663
  const rootPid = child.pid;
9608
9664
  const descendants = /* @__PURE__ */ new Map();
9609
- let failure;
9665
+ let failure2;
9610
9666
  try {
9611
9667
  signalProcess(-rootPid, "SIGSTOP");
9612
9668
  signalProcess(rootPid, "SIGSTOP");
@@ -9623,7 +9679,7 @@ async function terminatePosixProcessTree(child) {
9623
9679
  previous = signature;
9624
9680
  }
9625
9681
  } catch (error) {
9626
- failure = error;
9682
+ failure2 = error;
9627
9683
  } finally {
9628
9684
  for (const processEntry of [...descendants.values()].sort(
9629
9685
  (left, right) => right.depth - left.depth
@@ -9631,18 +9687,18 @@ async function terminatePosixProcessTree(child) {
9631
9687
  try {
9632
9688
  signalProcess(processEntry.pid, "SIGKILL");
9633
9689
  } catch (error) {
9634
- failure ??= error;
9690
+ failure2 ??= error;
9635
9691
  }
9636
9692
  }
9637
9693
  try {
9638
9694
  signalProcess(-rootPid, "SIGKILL");
9639
9695
  } catch (error) {
9640
- failure ??= error;
9696
+ failure2 ??= error;
9641
9697
  }
9642
9698
  forceKill(child);
9643
9699
  }
9644
- if (failure) {
9645
- throw failure instanceof Error ? failure : new Error(String(failure));
9700
+ if (failure2) {
9701
+ throw failure2 instanceof Error ? failure2 : new Error(String(failure2));
9646
9702
  }
9647
9703
  }
9648
9704
  function terminateProcessTree(child) {
@@ -9666,7 +9722,7 @@ function runCommand(command, args = [], opts = {}) {
9666
9722
  throw new RangeError("maxBuffer must be a positive safe integer");
9667
9723
  }
9668
9724
  if (opts.signal?.aborted) return Promise.reject(abortError(opts.signal));
9669
- return new Promise((resolve8, reject) => {
9725
+ return new Promise((resolve10, reject) => {
9670
9726
  let child;
9671
9727
  try {
9672
9728
  child = spawn(command, [...args], { detached: process.platform !== "win32" });
@@ -9698,7 +9754,7 @@ function runCommand(command, args = [], opts = {}) {
9698
9754
  } else if (spawnError) {
9699
9755
  reject(spawnError);
9700
9756
  } else {
9701
- resolve8({ stdout: out, stderr: err, code: closeCode });
9757
+ resolve10({ stdout: out, stderr: err, code: closeCode });
9702
9758
  }
9703
9759
  };
9704
9760
  function requestTermination(error) {
@@ -10613,71 +10669,490 @@ function assertPositiveInteger(name, value) {
10613
10669
  }
10614
10670
  }
10615
10671
 
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
10672
+ // src/analyst-engine-prime.ts
10673
+ import { createHash as createHash10 } from "crypto";
10674
+ import { request as httpRequest } from "http";
10675
+ import { request as httpsRequest } from "https";
10676
+ var DEFAULT_BRIDGE_URL = "http://localhost:4181";
10677
+ var DEFAULT_MODEL = "prime/zai/glm-5.2";
10678
+ var DEFAULT_TIMEOUT_MS = 12e5;
10679
+ var DEFAULT_MAX_INLINE_CHARS = 36e4;
10680
+ var DEFAULT_PER_ATTRIBUTE_CHAR_CAP = 1200;
10681
+ var DEFAULT_QUESTION = "Diagnose this trajectory: identify concrete agent failures, wasted work, and unsupported claims.";
10682
+ var MAX_FINDINGS = 10;
10683
+ var MAX_SPAN_IDS_PER_FINDING = 8;
10684
+ var SEVERITIES = /* @__PURE__ */ new Set(["critical", "high", "medium", "low", "info"]);
10685
+ var ANALYST_ID = "prime";
10686
+ function positiveIntegerEnv(name) {
10687
+ const raw = process.env[name];
10688
+ if (raw === void 0 || raw === "") return void 0;
10689
+ const value = Number(raw);
10690
+ if (!Number.isSafeInteger(value) || value < 1) {
10691
+ throw new TypeError(`${name} must be a positive integer, got '${raw}'`);
10647
10692
  }
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;
10654
- normalizeToolIoAttributes(attributes2);
10655
- let status = s.status;
10656
- if (status.message) {
10657
- const m = redactValue(status.message, rules, report).value;
10658
- if (typeof m === "string" && m !== status.message) status = { ...status, message: m };
10659
- }
10660
- return { ...s, attributes: attributes2, status };
10661
- });
10662
- return { spans: out, report };
10693
+ return value;
10663
10694
  }
10664
- async function applyRedactor(spans, redactor) {
10665
- const capturedKeys = ["content", ...TOOL_IO_VALUE_KEYS];
10666
- const fields = [];
10667
- const texts = [];
10668
- spans.forEach((s, i) => {
10669
- for (const key of capturedKeys) {
10670
- const value = s.attributes[key];
10671
- if (typeof value === "string" && value.length > 0) {
10672
- fields.push({ spanIndex: i, key });
10673
- texts.push(value);
10674
- }
10675
- }
10676
- if (s.status.message) {
10677
- fields.push({ spanIndex: i, key: "status.message" });
10678
- texts.push(s.status.message);
10679
- }
10680
- });
10695
+ function finiteOrNull(value) {
10696
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
10697
+ }
10698
+ function normalizeUsage(raw) {
10699
+ if (!raw || typeof raw !== "object") return { calls: null, tokens: null, estimated: false };
10700
+ const usage = raw;
10701
+ const input = finiteOrNull(usage.prompt_tokens);
10702
+ const output = finiteOrNull(usage.completion_tokens);
10703
+ return {
10704
+ calls: finiteOrNull(usage.model_requests),
10705
+ tokens: input === null && output === null ? null : { input, output },
10706
+ estimated: usage.estimated === true
10707
+ };
10708
+ }
10709
+ function mergeUsage(a, b) {
10710
+ const calls = a.calls !== null && b.calls !== null ? a.calls + b.calls : null;
10711
+ 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;
10712
+ return { calls, tokens: tokens3, estimated: a.estimated || b.estimated };
10713
+ }
10714
+ function renderUsage(usage) {
10715
+ const calls = usage.calls === null ? "uncaptured" : String(usage.calls);
10716
+ const input = usage.tokens?.input ?? "uncaptured";
10717
+ const output = usage.tokens?.output ?? "uncaptured";
10718
+ const estimated = usage.estimated ? " (bridge-estimated)" : "";
10719
+ return `usage: calls=${calls} input_tokens=${input} output_tokens=${output}${estimated}; cost=uncaptured (no pricing table)`;
10720
+ }
10721
+ function capAttributeValue(value, cap) {
10722
+ const rendered = JSON.stringify(value);
10723
+ if (rendered === void 0 || rendered.length <= cap) return value;
10724
+ if (typeof value === "string") return `${value.slice(0, cap)}\u2026[truncated ${value.length - cap} chars]`;
10725
+ return `[omitted non-string attribute: ${rendered.length} JSON chars]`;
10726
+ }
10727
+ function capRowAttributes(row, cap) {
10728
+ const attributes2 = row.attributes;
10729
+ if (!attributes2 || typeof attributes2 !== "object" || Array.isArray(attributes2)) return row;
10730
+ const capped = {};
10731
+ for (const [key, value] of Object.entries(attributes2)) {
10732
+ capped[key] = capAttributeValue(value, cap);
10733
+ }
10734
+ return { ...row, attributes: capped };
10735
+ }
10736
+ async function projectArtifact(otlpPath, maxInlineChars, perAttributeCharCap, signal) {
10737
+ const rows = [];
10738
+ const spanTraces = /* @__PURE__ */ new Map();
10739
+ let index = 0;
10740
+ for await (const value of readJsonl(otlpPath, { signal })) {
10741
+ const label = `${otlpPath}:${index + 1}`;
10742
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
10743
+ throw new TypeError(`${label} must contain an object`);
10744
+ }
10745
+ const row = value;
10746
+ if (typeof row.trace_id !== "string" || row.trace_id.length === 0) {
10747
+ throw new TypeError(`${label}.trace_id must be a non-empty string`);
10748
+ }
10749
+ if (typeof row.span_id !== "string" || row.span_id.length === 0) {
10750
+ throw new TypeError(`${label}.span_id must be a non-empty string`);
10751
+ }
10752
+ rows.push(row);
10753
+ const traces = spanTraces.get(row.span_id) ?? /* @__PURE__ */ new Set();
10754
+ traces.add(row.trace_id);
10755
+ spanTraces.set(row.span_id, traces);
10756
+ index += 1;
10757
+ }
10758
+ if (rows.length === 0) throw new Error(`${otlpPath} contains no spans`);
10759
+ const traceCount = new Set(rows.map((row) => row.trace_id)).size;
10760
+ let projected = rows;
10761
+ let rendered = JSON.stringify(projected);
10762
+ const delivery = {
10763
+ mode: "inline-json",
10764
+ perAttributeCharCap: null,
10765
+ renderedChars: rendered.length
10766
+ };
10767
+ if (rendered.length > maxInlineChars) {
10768
+ projected = rows.map((row) => capRowAttributes(row, perAttributeCharCap));
10769
+ rendered = JSON.stringify(projected);
10770
+ delivery.perAttributeCharCap = perAttributeCharCap;
10771
+ delivery.renderedChars = rendered.length;
10772
+ }
10773
+ if (rendered.length > maxInlineChars) {
10774
+ throw new Error(
10775
+ `trajectory renders to ${rendered.length} chars even at per-attribute cap ${perAttributeCharCap} (budget ${maxInlineChars}); inline delivery impossible`
10776
+ );
10777
+ }
10778
+ return { rows: projected, rendered, delivery, traceCount, spanTraces };
10779
+ }
10780
+ var OUTPUT_CONTRACT_LINES = [
10781
+ "OUTPUT CONTRACT (you have no trace tools and no REPL):",
10782
+ "You are a one-shot analyst. Every fact you need is in the TRAJECTORY JSON below.",
10783
+ "Do not run shell commands, do not read or write files, do not use any tools.",
10784
+ "Reply with EXACTLY one fenced ```json code block and no other fenced block. The JSON object has exactly two fields:",
10785
+ ' "answer": string \u2014 ONE short sentence (max 300 chars) summarizing your verdict.',
10786
+ ' "findings": array (possibly empty) of finding rows, each exactly:',
10787
+ ' {"span_ids": [string, ...],',
10788
+ ' "severity": "critical"|"high"|"medium"|"low"|"info",',
10789
+ ' "area": string (short kebab-case topic, max 40 chars),',
10790
+ ' "claim": string (ONE short sentence, max 200 chars),',
10791
+ ' "action": string (optional, ONE short imperative sentence, max 200 chars),',
10792
+ ' "confidence": number 0..1}',
10793
+ "Do NOT include a rationale field. Keep every string SHORT \u2014 long strings get corrupted in transport and void your work.",
10794
+ `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.`,
10795
+ '"findings" is [] only for a clean trajectory.'
10796
+ ];
10797
+ function buildPrompt(question, projection) {
10798
+ return [
10799
+ `QUESTION: ${question}`,
10800
+ "",
10801
+ ...OUTPUT_CONTRACT_LINES,
10802
+ "",
10803
+ `TRAJECTORY (${projection.traceCount} trace(s); ${projection.rows.length} spans; full OpenInference span projection as JSON):`,
10804
+ projection.rendered
10805
+ ].join("\n");
10806
+ }
10807
+ function buildRepairPrompt(parseDefect, previousReply) {
10808
+ return [
10809
+ "Your previous reply to a trace-analysis task was structurally malformed and could not be parsed",
10810
+ `(${parseDefect}). Below is your previous reply verbatim. Re-emit ONLY the corrected JSON \u2014 one`,
10811
+ "fenced ```json block, no other text, no tools. The JSON object has exactly two fields:",
10812
+ ' "answer": string (ONE short sentence, max 300 chars)',
10813
+ ' "findings": array (possibly empty) of {"span_ids": [string, ...],',
10814
+ ' "severity": "critical"|"high"|"medium"|"low"|"info", "area": string (max 40 chars),',
10815
+ ' "claim": string (max 200 chars), "action": string (optional, max 200 chars), "confidence": number 0..1}',
10816
+ "No rationale field. Keep every string SHORT. Preserve the span ids and verdicts of your previous reply exactly; shorten prose freely.",
10817
+ "",
10818
+ "PREVIOUS REPLY:",
10819
+ previousReply
10820
+ ].join("\n");
10821
+ }
10822
+ function tryParseObject(text2) {
10823
+ try {
10824
+ const value = JSON.parse(text2.trim());
10825
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
10826
+ } catch {
10827
+ return null;
10828
+ }
10829
+ }
10830
+ function extractJsonObject(text2) {
10831
+ const direct = tryParseObject(text2);
10832
+ if (direct) return direct;
10833
+ const fenced = [...text2.matchAll(/```(?:json)?\s*\n?([\s\S]*?)```/g)];
10834
+ for (let i = fenced.length - 1; i >= 0; i -= 1) {
10835
+ const candidate = tryParseObject(fenced[i][1]);
10836
+ if (candidate) return candidate;
10837
+ }
10838
+ const start = text2.indexOf("{");
10839
+ const end = text2.lastIndexOf("}");
10840
+ if (start >= 0 && end > start) {
10841
+ const candidate = tryParseObject(text2.slice(start, end + 1));
10842
+ if (candidate) return candidate;
10843
+ }
10844
+ return null;
10845
+ }
10846
+ function parseDefectOf(parsed) {
10847
+ if (parsed === null) return "no parseable JSON object";
10848
+ if (!Array.isArray(parsed.findings)) return 'JSON has no "findings" array';
10849
+ return null;
10850
+ }
10851
+ function findingRowDefect(row, spanTraces) {
10852
+ if (!row || typeof row !== "object" || Array.isArray(row)) return "row is not an object";
10853
+ const record2 = row;
10854
+ if (!Array.isArray(record2.span_ids) || record2.span_ids.length === 0) {
10855
+ return "span_ids must be a non-empty array";
10856
+ }
10857
+ const spanIds = [];
10858
+ for (const id of record2.span_ids) {
10859
+ if (typeof id !== "string" || id.length === 0) return "span_ids must contain non-empty strings";
10860
+ const traces = spanTraces.get(id);
10861
+ if (!traces) return `span_id '${id}' is not in the trajectory`;
10862
+ if (traces.size > 1) return `span_id '${id}' is ambiguous across ${traces.size} traces`;
10863
+ if (!spanIds.includes(id)) spanIds.push(id);
10864
+ }
10865
+ if (spanIds.length > MAX_SPAN_IDS_PER_FINDING) {
10866
+ return `span_ids cites ${spanIds.length} distinct spans (cap ${MAX_SPAN_IDS_PER_FINDING})`;
10867
+ }
10868
+ if (typeof record2.severity !== "string" || !SEVERITIES.has(record2.severity)) {
10869
+ return "severity outside the analyst severity enum";
10870
+ }
10871
+ if (typeof record2.area !== "string" || record2.area.trim().length === 0 || record2.area.length > 200) {
10872
+ return "area must be a 1-200 char string";
10873
+ }
10874
+ if (typeof record2.claim !== "string" || record2.claim.trim().length === 0 || record2.claim.length > 2e3) {
10875
+ return "claim must be a 1-2000 char string";
10876
+ }
10877
+ if (record2.action !== void 0 && (typeof record2.action !== "string" || record2.action.trim().length === 0 || record2.action.length > 2e3)) {
10878
+ return "action must be a 1-2000 char string when present";
10879
+ }
10880
+ if (typeof record2.confidence !== "number" || !Number.isFinite(record2.confidence) || record2.confidence < 0 || record2.confidence > 1) {
10881
+ return "confidence must be 0..1";
10882
+ }
10883
+ return {
10884
+ spanIds,
10885
+ severity: record2.severity,
10886
+ area: record2.area.trim(),
10887
+ claim: record2.claim.trim(),
10888
+ ...typeof record2.action === "string" ? { action: record2.action.trim() } : {},
10889
+ confidence: record2.confidence
10890
+ };
10891
+ }
10892
+ function toAnalystFinding(row, index, spanTraces, model, producedAt) {
10893
+ const uris = row.spanIds.map((spanId) => {
10894
+ const traces = spanTraces.get(spanId);
10895
+ return spanEvidenceUri([...traces][0], spanId);
10896
+ });
10897
+ const findingId = `prime-${createHash10("sha256").update(`${index}
10898
+ ${row.claim}
10899
+ ${uris.join("\n")}`).digest("hex").slice(0, 12)}`;
10900
+ return {
10901
+ schema_version: "1.0.0",
10902
+ finding_id: findingId,
10903
+ analyst_id: ANALYST_ID,
10904
+ produced_at: producedAt,
10905
+ severity: row.severity,
10906
+ area: row.area,
10907
+ claim: row.claim,
10908
+ evidence_refs: uris.map((uri) => ({ kind: "span", uri })),
10909
+ confidence: row.confidence,
10910
+ ...row.action ? { recommended_action: row.action } : {},
10911
+ metadata: { engine: ANALYST_ID, model }
10912
+ };
10913
+ }
10914
+ var httpJsonTransport = ({ url, body, signal }) => {
10915
+ const target = new URL(url);
10916
+ if (target.protocol !== "http:" && target.protocol !== "https:") {
10917
+ throw new TypeError(`bridge URL must be http: or https:, got ${target.protocol}`);
10918
+ }
10919
+ const requestFn = target.protocol === "https:" ? httpsRequest : httpRequest;
10920
+ const payload = JSON.stringify(body);
10921
+ return new Promise((resolve10, reject) => {
10922
+ const req = requestFn(
10923
+ {
10924
+ hostname: target.hostname,
10925
+ port: target.port,
10926
+ path: `${target.pathname}${target.search}`,
10927
+ method: "POST",
10928
+ headers: { "content-type": "application/json", "content-length": Buffer.byteLength(payload) },
10929
+ signal
10930
+ },
10931
+ (res) => {
10932
+ const chunks = [];
10933
+ res.on("data", (chunk) => chunks.push(chunk));
10934
+ res.on("end", () => resolve10({ status: res.statusCode ?? 0, text: Buffer.concat(chunks).toString("utf8") }));
10935
+ res.on("error", reject);
10936
+ }
10937
+ );
10938
+ req.on("error", reject);
10939
+ req.end(payload);
10940
+ });
10941
+ };
10942
+ var PrimeBridgeError = class extends Error {
10943
+ };
10944
+ async function callBridge(transport, url, model, content, timeoutMs, callerSignal, turn) {
10945
+ const controller = new AbortController();
10946
+ const timer = setTimeout(
10947
+ () => controller.abort(new Error(`${turn}bridge call exceeded ${timeoutMs}ms`)),
10948
+ timeoutMs
10949
+ );
10950
+ const onCallerAbort = () => controller.abort(callerSignal.reason);
10951
+ callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
10952
+ if (callerSignal?.aborted) onCallerAbort();
10953
+ let response;
10954
+ try {
10955
+ response = await transport({
10956
+ url,
10957
+ body: { model, messages: [{ role: "user", content }] },
10958
+ signal: controller.signal
10959
+ });
10960
+ } catch (error) {
10961
+ const reason = controller.signal.aborted ? controller.signal.reason : error;
10962
+ throw new PrimeBridgeError(
10963
+ `${turn}bridge transport failure: ${reason instanceof Error ? reason.message : String(reason)}`
10964
+ );
10965
+ } finally {
10966
+ clearTimeout(timer);
10967
+ callerSignal?.removeEventListener("abort", onCallerAbort);
10968
+ }
10969
+ if (response.status !== 200) {
10970
+ throw new PrimeBridgeError(`${turn}bridge HTTP ${response.status}: ${response.text.slice(0, 500)}`);
10971
+ }
10972
+ let parsed;
10973
+ try {
10974
+ parsed = JSON.parse(response.text);
10975
+ } catch {
10976
+ throw new PrimeBridgeError(`${turn}bridge returned unparseable JSON (${response.text.length} bytes)`);
10977
+ }
10978
+ const record2 = parsed;
10979
+ const replyContent = record2.choices?.[0]?.message?.content;
10980
+ if (typeof replyContent !== "string" || replyContent.length === 0) {
10981
+ throw new PrimeBridgeError(`${turn}bridge reply carries no message content`);
10982
+ }
10983
+ return { content: replyContent, usage: normalizeUsage(record2.usage) };
10984
+ }
10985
+ function failure(output, error) {
10986
+ return { analyzer: ANALYST_ID, kind: "report", ok: false, output, error };
10987
+ }
10988
+ function primeAnalyzer(opts = {}) {
10989
+ const bridgeUrl = (opts.bridgeUrl ?? process.env.TRACES_PRIME_BRIDGE_URL ?? DEFAULT_BRIDGE_URL).replace(/\/+$/, "");
10990
+ const model = opts.model ?? process.env.TRACES_PRIME_MODEL ?? DEFAULT_MODEL;
10991
+ const timeoutMs = opts.timeoutMs ?? positiveIntegerEnv("TRACES_PRIME_TIMEOUT_MS") ?? DEFAULT_TIMEOUT_MS;
10992
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1) {
10993
+ throw new RangeError("timeoutMs must be a positive safe integer");
10994
+ }
10995
+ const maxInlineChars = opts.maxInlineChars ?? DEFAULT_MAX_INLINE_CHARS;
10996
+ const perAttributeCharCap = opts.perAttributeCharCap ?? DEFAULT_PER_ATTRIBUTE_CHAR_CAP;
10997
+ if (!Number.isSafeInteger(maxInlineChars) || maxInlineChars < 1) {
10998
+ throw new RangeError("maxInlineChars must be a positive safe integer");
10999
+ }
11000
+ if (!Number.isSafeInteger(perAttributeCharCap) || perAttributeCharCap < 1) {
11001
+ throw new RangeError("perAttributeCharCap must be a positive safe integer");
11002
+ }
11003
+ const repairEnabled = opts.repair ?? true;
11004
+ const transport = opts.transport ?? httpJsonTransport;
11005
+ const url = `${bridgeUrl}/v1/chat/completions`;
11006
+ return {
11007
+ name: ANALYST_ID,
11008
+ async analyze(otlpPath, analyzeOpts = {}) {
11009
+ let projection;
11010
+ try {
11011
+ projection = await projectArtifact(otlpPath, maxInlineChars, perAttributeCharCap, analyzeOpts.signal);
11012
+ } catch (error) {
11013
+ return failure("", error instanceof Error ? error.message : String(error));
11014
+ }
11015
+ const question = analyzeOpts.prompt ?? opts.defaultPrompt ?? DEFAULT_QUESTION;
11016
+ const deliveryLine = `delivery: ${projection.delivery.mode} (${projection.delivery.renderedChars} chars, per-attribute cap ${projection.delivery.perAttributeCharCap ?? "none"})`;
11017
+ let reply;
11018
+ try {
11019
+ reply = await callBridge(
11020
+ transport,
11021
+ url,
11022
+ model,
11023
+ buildPrompt(question, projection),
11024
+ timeoutMs,
11025
+ analyzeOpts.signal,
11026
+ ""
11027
+ );
11028
+ } catch (error) {
11029
+ if (!(error instanceof PrimeBridgeError)) throw error;
11030
+ return failure("", `${error.message}; ${deliveryLine}`);
11031
+ }
11032
+ let usage = reply.usage;
11033
+ let parsed = extractJsonObject(reply.content);
11034
+ let parseDefect = parseDefectOf(parsed);
11035
+ let repairAttempted = false;
11036
+ if (parseDefect !== null && repairEnabled) {
11037
+ repairAttempted = true;
11038
+ let repairReply;
11039
+ try {
11040
+ repairReply = await callBridge(
11041
+ transport,
11042
+ url,
11043
+ model,
11044
+ buildRepairPrompt(parseDefect, reply.content),
11045
+ timeoutMs,
11046
+ analyzeOpts.signal,
11047
+ "repair-turn "
11048
+ );
11049
+ } catch (error) {
11050
+ if (!(error instanceof PrimeBridgeError)) throw error;
11051
+ return failure(reply.content, `${error.message}; ${renderUsage(usage)}`);
11052
+ }
11053
+ usage = mergeUsage(usage, repairReply.usage);
11054
+ parsed = extractJsonObject(repairReply.content);
11055
+ parseDefect = parseDefectOf(parsed);
11056
+ }
11057
+ if (parseDefect !== null) {
11058
+ return failure(
11059
+ reply.content,
11060
+ `${parseDefect} in prime reply${repairAttempted ? " even after the bounded repair turn" : ""}; ` + renderUsage(usage)
11061
+ );
11062
+ }
11063
+ const rows = parsed.findings.slice(0, MAX_FINDINGS);
11064
+ const overflow = parsed.findings.length - rows.length;
11065
+ const producedAt = (/* @__PURE__ */ new Date()).toISOString();
11066
+ const rejected = [];
11067
+ const findings = [];
11068
+ rows.forEach((row, index) => {
11069
+ const decoded = findingRowDefect(row, projection.spanTraces);
11070
+ if (typeof decoded === "string") {
11071
+ rejected.push(`rejected[${index}]: ${decoded}`);
11072
+ return;
11073
+ }
11074
+ findings.push(toAnalystFinding(decoded, index, projection.spanTraces, model, producedAt));
11075
+ });
11076
+ const answer = typeof parsed.answer === "string" ? parsed.answer : null;
11077
+ const output = [
11078
+ ...answer ? [`answer: ${answer}`] : [],
11079
+ `findings: ${findings.length} mapped, ${rejected.length} rejected` + (overflow > 0 ? `, ${overflow} over the ${MAX_FINDINGS}-finding cap dropped` : ""),
11080
+ ...rejected,
11081
+ deliveryLine,
11082
+ `repair: ${repairAttempted ? "attempted (succeeded)" : "not needed"}`,
11083
+ renderUsage(usage),
11084
+ ...findings.length === 0 ? ["zero findings \u2014 an honest null, not a failure"] : []
11085
+ ].join("\n");
11086
+ return { analyzer: ANALYST_ID, kind: "findings", ok: true, output, findings };
11087
+ }
11088
+ };
11089
+ }
11090
+
11091
+ // src/redact.ts
11092
+ import { DEFAULT_REDACTION_RULES, redactValue } from "@tangle-network/agent-eval/traces";
11093
+ var CODING_REDACTION_RULES = [
11094
+ { id: "github-token", pattern: /\b(?:gh[pousr]_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{22,})\b/g },
11095
+ { id: "aws-akid", pattern: /\bAKIA[0-9A-Z]{16}\b/g },
11096
+ { id: "slack-token", pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
11097
+ { id: "jwt", pattern: /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g },
11098
+ { id: "bearer", pattern: /\bBearer\s+[A-Za-z0-9._-]{16,}/gi },
11099
+ {
11100
+ id: "private-key",
11101
+ pattern: /-----BEGIN(?:[A-Z ]+)?PRIVATE KEY-----[\s\S]*?-----END(?:[A-Z ]+)?PRIVATE KEY-----/g
11102
+ },
11103
+ // Match the whole identifier because underscores prevent a word boundary before a secret suffix.
11104
+ {
11105
+ id: "assigned-secret",
11106
+ 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
11107
+ },
11108
+ // Config-file secrets (YAML/INI line keys, JSON quoted keys) — the coverage the shell
11109
+ // rule above deliberately gave up to spare prose ("Narrative token: x") and type
11110
+ // annotations ("(token: string)"). The line anchor is the discriminator: config keys
11111
+ // start their line, prose colons never do; JSON keys are safe anywhere because both
11112
+ // sides are quoted. Identifiers must END in a secret word, same as the shell rule.
11113
+ {
11114
+ id: "config-secret",
11115
+ 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
11116
+ },
11117
+ // Credentials embedded in URLs — common when a prompt pastes a curl/clone line.
11118
+ { id: "url-userinfo", pattern: /\b[a-z][a-z0-9+.-]*:\/\/[^\s/:@]+:[^\s/@]+@/gi },
11119
+ {
11120
+ id: "url-secret-param",
11121
+ pattern: /[?&](?:access[_-]?token|api[_-]?key|apikey|auth|token|secret|password|sig|signature)=[^&\s"'#)]+/gi
11122
+ }
11123
+ ];
11124
+ var TRACES_REDACTION_RULES = [...DEFAULT_REDACTION_RULES, ...CODING_REDACTION_RULES];
11125
+ function redactSpans(spans, rules = TRACES_REDACTION_RULES) {
11126
+ const report = { redactionCount: 0, byRule: {} };
11127
+ const out = spans.map((s) => {
11128
+ const attributes2 = redactValue(s.attributes, rules, report).value;
11129
+ normalizeToolIoAttributes(attributes2);
11130
+ let status = s.status;
11131
+ if (status.message) {
11132
+ const m = redactValue(status.message, rules, report).value;
11133
+ if (typeof m === "string" && m !== status.message) status = { ...status, message: m };
11134
+ }
11135
+ return { ...s, attributes: attributes2, status };
11136
+ });
11137
+ return { spans: out, report };
11138
+ }
11139
+ async function applyRedactor(spans, redactor) {
11140
+ const capturedKeys = ["content", ...TOOL_IO_VALUE_KEYS];
11141
+ const fields = [];
11142
+ const texts = [];
11143
+ spans.forEach((s, i) => {
11144
+ for (const key of capturedKeys) {
11145
+ const value = s.attributes[key];
11146
+ if (typeof value === "string" && value.length > 0) {
11147
+ fields.push({ spanIndex: i, key });
11148
+ texts.push(value);
11149
+ }
11150
+ }
11151
+ if (s.status.message) {
11152
+ fields.push({ spanIndex: i, key: "status.message" });
11153
+ texts.push(s.status.message);
11154
+ }
11155
+ });
10681
11156
  const out = spans.map((s) => ({ ...s, attributes: { ...s.attributes } }));
10682
11157
  if (texts.length === 0) {
10683
11158
  for (const span2 of out) normalizeToolIoAttributes(span2.attributes);
@@ -10703,7 +11178,7 @@ async function applyRedactor(spans, redactor) {
10703
11178
  }
10704
11179
 
10705
11180
  // src/file-export.ts
10706
- import { createHash as createHash10 } from "crypto";
11181
+ import { createHash as createHash11 } from "crypto";
10707
11182
  import { mkdtemp as mkdtemp4, readFile as readFile9, writeFile as writeFile5 } from "fs/promises";
10708
11183
  import { tmpdir as tmpdir5 } from "os";
10709
11184
  import { join as join24 } from "path";
@@ -10741,7 +11216,7 @@ function stableJson(value) {
10741
11216
  });
10742
11217
  }
10743
11218
  function hashId(value, chars = 16) {
10744
- return createHash10("sha256").update(stableJson(value)).digest("hex").slice(0, chars);
11219
+ return createHash11("sha256").update(stableJson(value)).digest("hex").slice(0, chars);
10745
11220
  }
10746
11221
  function isoTime2(value) {
10747
11222
  if (typeof value === "string") {
@@ -10902,12 +11377,12 @@ function policyEvidenceToSpans(records) {
10902
11377
  "traces.provenance.evidence_kind": record2.provenance.evidenceKind,
10903
11378
  "traces.provenance.otlp_path": record2.provenance.otlpPath,
10904
11379
  "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
11380
+ [ATTR.SUBJECT_KEY]: record2.repo.subjectKey,
11381
+ [ATTR.GIT_REPOSITORY]: record2.repo.repository,
11382
+ [ATTR.GIT_BRANCH_NAME]: record2.repo.branch,
11383
+ [ATTR.GIT_COMMIT]: record2.repo.commit,
11384
+ [ATTR.CWD]: record2.repo.cwd ?? record2.session.cwd,
11385
+ [ATTR.REPO_RESOLUTION_SOURCE]: record2.repo.resolutionSource
10911
11386
  };
10912
11387
  return span({
10913
11388
  traceId,
@@ -11004,6 +11479,54 @@ function eventKind(row, type) {
11004
11479
  if (lowered.includes("llm") || lowered === "token") return { kind: "LLM" };
11005
11480
  return { kind: "CHAIN" };
11006
11481
  }
11482
+ function sandboxEventsEnvironment(context, rows) {
11483
+ const ready = rows.find((row) => eventType(row) === "runtime.ready");
11484
+ const readyData = ready && isObject5(ready.data) ? ready.data : void 0;
11485
+ const pick = (keys) => findStringKey(context, keys, 2) ?? (readyData ? findStringKey(readyData, keys, 2) : void 0);
11486
+ const image = pick(["image"]);
11487
+ const imageDigest = pick(["imageDigest", "image_digest"]);
11488
+ const sandboxId = pick(["sandboxId", "sandbox_id"]);
11489
+ const cwd = pick(["cwd", "workspaceRoot", "workspace_root"]);
11490
+ if (!image && !imageDigest && !sandboxId && !cwd) return null;
11491
+ return {
11492
+ ...image ? { image } : {},
11493
+ ...imageDigest ? { imageDigest } : {},
11494
+ ...sandboxId ? { sandboxId } : {},
11495
+ cwd: cwd ?? null
11496
+ };
11497
+ }
11498
+ function llmEventUsage(row) {
11499
+ const num = (keys) => {
11500
+ for (const key of keys) {
11501
+ const direct = numberValue(row[key]);
11502
+ if (direct !== void 0) return direct;
11503
+ for (const nest of ["data", "usage", "tokenUsage"]) {
11504
+ const container = row[nest];
11505
+ if (!isObject5(container)) continue;
11506
+ const value = numberValue(container[key]);
11507
+ if (value !== void 0) return value;
11508
+ for (const inner of ["usage", "tokenUsage"]) {
11509
+ const deep = container[inner];
11510
+ if (isObject5(deep)) {
11511
+ const deepValue = numberValue(deep[key]);
11512
+ if (deepValue !== void 0) return deepValue;
11513
+ }
11514
+ }
11515
+ }
11516
+ }
11517
+ return void 0;
11518
+ };
11519
+ const model = findStringKey(row, ["model"], 3);
11520
+ const inputTokens = num(["prompt_tokens", "input_tokens", "inputTokens"]);
11521
+ const outputTokens = num(["completion_tokens", "output_tokens", "outputTokens"]);
11522
+ const costUsd = num(["totalCostUsd", "total_cost_usd", "costUsd", "cost_usd"]);
11523
+ return {
11524
+ ...model ? { model } : {},
11525
+ ...inputTokens !== void 0 ? { inputTokens } : {},
11526
+ ...outputTokens !== void 0 ? { outputTokens } : {},
11527
+ ...costUsd !== void 0 ? { costUsd } : {}
11528
+ };
11529
+ }
11007
11530
  function sandboxEventsToSpans(rows, wrapper) {
11008
11531
  const context = wrapper ?? {};
11009
11532
  const sessionId = findStringKey(context, ["session_id", "sessionId"], 3) ?? findStringKey(rows, ["session_id", "sessionId"], 4);
@@ -11074,13 +11597,15 @@ function sandboxEventsToSpans(rows, wrapper) {
11074
11597
  tool: kind === "TOOL" ? tool ?? type : null,
11075
11598
  step: index,
11076
11599
  content: capText(stableJson(row)),
11077
- extra
11600
+ extra,
11601
+ ...kind === "LLM" ? llmEventUsage(row) : {}
11078
11602
  }));
11079
11603
  });
11604
+ stampEnvironmentAttrs(spans, sandboxEventsEnvironment(context, rows) ?? void 0);
11080
11605
  if (!sessionId) return spans;
11081
11606
  return spans.map((item) => ({
11082
11607
  ...item,
11083
- attributes: { ...item.attributes, [ATTR2.SESSION_ID]: sessionId }
11608
+ attributes: { ...item.attributes, [ATTR.SESSION_ID]: sessionId }
11084
11609
  }));
11085
11610
  }
11086
11611
  function statusCode2(value) {
@@ -11159,7 +11684,7 @@ function intelligenceSpansToSpans(rows) {
11159
11684
  };
11160
11685
  copyDefined(extra, {
11161
11686
  "traces.row.id": stringValue3(row.id),
11162
- [ATTR2.SESSION_ID]: sessionId,
11687
+ [ATTR.SESSION_ID]: sessionId,
11163
11688
  "session.id": sessionId,
11164
11689
  "run.id": stringValue3(row.run_id),
11165
11690
  "thread.id": stringValue3(row.thread_id),
@@ -12534,6 +13059,47 @@ var SEVERITY_BADGE2 = {
12534
13059
  low: "\u{1F535} LOW",
12535
13060
  info: "\u2139\uFE0F INFO"
12536
13061
  };
13062
+ function sessionReportSource(ref, spans, sessionIdOverride) {
13063
+ const root = spans.find((item) => item.parent_span_id === null) ?? spans[0];
13064
+ const prompt = spans.find(
13065
+ (item) => item.name === "user.prompt" && item.attributes[ACTOR_ATTR] === "human"
13066
+ ) ?? spans.find((item) => item.name === "user.prompt") ?? spans.find(
13067
+ (item) => item.attributes["span.type"] === "interaction" && typeof item.attributes.content === "string"
13068
+ );
13069
+ const content = typeof prompt?.attributes.content === "string" ? prompt.attributes.content : "";
13070
+ const firstLine = content.split(/\r?\n/, 1)[0].trim();
13071
+ const subject = firstLine.length > 240 ? `${firstLine.slice(0, 240)}\u2026 [+${firstLine.length - 240} chars]` : firstLine;
13072
+ const role = root?.attributes["traces.session.role"];
13073
+ const parentSessionId = root?.attributes["traces.parent_session_id"];
13074
+ const relationship = describeSessionRelationship(ref, spans);
13075
+ const corruptionDigest2 = root?.attributes[ATTR.CORRUPTION_DIGEST];
13076
+ const sessionId = [
13077
+ sessionIdOverride,
13078
+ root ? sessionIdFromAttributes(root.attributes) : void 0,
13079
+ root?.trace_id,
13080
+ ref.sessionId
13081
+ ].find((value) => typeof value === "string" && value.length > 0);
13082
+ return {
13083
+ sessionId,
13084
+ path: ref.path,
13085
+ subject,
13086
+ role: role === "operator" || role === "child" ? role : "unknown",
13087
+ ...typeof parentSessionId === "string" ? { parentSessionId } : {},
13088
+ childSessionIds: relationship.childSessionIds,
13089
+ ...relationship.depth !== void 0 ? { depth: relationship.depth } : {},
13090
+ ...relationship.agentNickname ? { agentNickname: relationship.agentNickname } : {},
13091
+ ...relationship.agentRole ? { agentRole: relationship.agentRole } : {},
13092
+ ...relationship.agentPath ? { agentPath: relationship.agentPath } : {},
13093
+ ...relationship.taskScope ? { taskScope: relationship.taskScope } : {},
13094
+ ...relationship.turnId ? { turnId: relationship.turnId } : {},
13095
+ integrity: ref.integrity?.status ?? "complete",
13096
+ ...ref.integrity ? {
13097
+ corruptionCount: ref.integrity.corruptions.length,
13098
+ ...typeof corruptionDigest2 === "string" ? { corruptionDigest: corruptionDigest2 } : {},
13099
+ corruptions: ref.integrity.corruptions.slice(0, CORRUPTION_RECEIPT_DISPLAY_LIMIT)
13100
+ } : {}
13101
+ };
13102
+ }
12537
13103
  function deterministicSummaryText(summary) {
12538
13104
  const parts = [];
12539
13105
  if (summary.stuckLoops > 0) parts.push(`${summary.stuckLoops} stuck loop(s)`);
@@ -13684,27 +14250,27 @@ async function saveReport(path, report) {
13684
14250
  }
13685
14251
 
13686
14252
  // src/live.ts
13687
- import { createHash as createHash11 } from "crypto";
14253
+ import { createHash as createHash12 } from "crypto";
13688
14254
  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
14255
  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
14256
  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) => {
14257
+ var sleep = (ms2, signal) => new Promise((resolve10) => {
13692
14258
  if (signal?.aborted) {
13693
- resolve8();
14259
+ resolve10();
13694
14260
  return;
13695
14261
  }
13696
14262
  const onAbort = () => {
13697
14263
  clearTimeout(t);
13698
- resolve8();
14264
+ resolve10();
13699
14265
  };
13700
14266
  const t = setTimeout(() => {
13701
14267
  signal?.removeEventListener("abort", onAbort);
13702
- resolve8();
14268
+ resolve10();
13703
14269
  }, ms2);
13704
14270
  signal?.addEventListener("abort", onAbort, { once: true });
13705
14271
  });
13706
14272
  function hash(parts) {
13707
- return createHash11("sha256").update(parts.join("\0")).digest("hex").slice(0, 16);
14273
+ return createHash12("sha256").update(parts.join("\0")).digest("hex").slice(0, 16);
13708
14274
  }
13709
14275
  function asString(value) {
13710
14276
  if (typeof value === "string" && value.trim()) return value;
@@ -15174,13 +15740,13 @@ function aborted(signal) {
15174
15740
  return signal !== void 0 && signal.aborted;
15175
15741
  }
15176
15742
  function sleep2(msDelay, signal) {
15177
- return new Promise((resolve8) => {
15178
- const timer = setTimeout(resolve8, msDelay);
15743
+ return new Promise((resolve10) => {
15744
+ const timer = setTimeout(resolve10, msDelay);
15179
15745
  signal?.addEventListener(
15180
15746
  "abort",
15181
15747
  () => {
15182
15748
  clearTimeout(timer);
15183
- resolve8();
15749
+ resolve10();
15184
15750
  },
15185
15751
  { once: true }
15186
15752
  );
@@ -15520,11 +16086,11 @@ async function collectSessionWorkflow(adapter, seeds, options = {}) {
15520
16086
  }
15521
16087
 
15522
16088
  // src/session-selection.ts
15523
- import { createHash as createHash12 } from "crypto";
16089
+ import { createHash as createHash13 } from "crypto";
15524
16090
  import { createReadStream as createReadStream2 } from "fs";
15525
16091
  async function fileSha256(path, signal) {
15526
16092
  signal?.throwIfAborted();
15527
- const hash2 = createHash12("sha256");
16093
+ const hash2 = createHash13("sha256");
15528
16094
  try {
15529
16095
  for await (const chunk of createReadStream2(path, { signal })) {
15530
16096
  signal?.throwIfAborted();
@@ -15552,7 +16118,7 @@ async function sourceSnapshot(adapter, ref, signal) {
15552
16118
  signal?.throwIfAborted();
15553
16119
  files.push({ path, sha256: await fileSha256(path, signal) });
15554
16120
  }
15555
- const sha2562 = files.length === 1 ? files[0].sha256 : createHash12("sha256").update(JSON.stringify(files)).digest("hex");
16121
+ const sha2562 = files.length === 1 ? files[0].sha256 : createHash13("sha256").update(JSON.stringify(files)).digest("hex");
15556
16122
  return { sha256: sha2562, files };
15557
16123
  }
15558
16124
  function summarizeWorkflows(workflows) {
@@ -15813,14 +16379,30 @@ async function collectContextRoot(root) {
15813
16379
  const summary = await summarizeFile(join31(evolve, name), "evolve-jsonl");
15814
16380
  if (summary) files.push(summary);
15815
16381
  }
15816
- for (const name of ["scorecard.json"]) {
16382
+ for (const name of ["scorecard.json", "current.json"]) {
15817
16383
  const summary = await summarizeFile(join31(evolve, name), "evolve-json");
15818
16384
  if (summary) files.push(summary);
15819
16385
  }
15820
16386
  files.push(...await walkMarkdown(join31(evolve, "reflections"), "reflection"));
15821
16387
  files.push(...await walkMarkdown(join31(evolve, "handoffs"), "handoff"));
16388
+ for (const name of await listEvolveRootFiles(evolve)) {
16389
+ if (/^handoff-.*\.md$/.test(name)) {
16390
+ const summary = await summarizeFile(join31(evolve, name), "handoff");
16391
+ if (summary) files.push(summary);
16392
+ }
16393
+ }
16394
+ const progress = await summarizeFile(join31(evolve, "progress.md"), "other");
16395
+ if (progress) files.push(progress);
15822
16396
  return { root, files: files.sort((a, b) => a.path.localeCompare(b.path)) };
15823
16397
  }
16398
+ async function listEvolveRootFiles(evolve) {
16399
+ try {
16400
+ const entries = await readdir18(evolve, { withFileTypes: true });
16401
+ return entries.filter((entry) => entry.isFile()).map((entry) => entry.name).sort();
16402
+ } catch {
16403
+ return [];
16404
+ }
16405
+ }
15824
16406
  async function collectContextIndex(records) {
15825
16407
  const rootSet = /* @__PURE__ */ new Set();
15826
16408
  for (const record2 of records) {
@@ -15869,14 +16451,641 @@ async function writeSessionIndexFile(index, outPath) {
15869
16451
  return path;
15870
16452
  }
15871
16453
 
16454
+ // src/bundle.ts
16455
+ import { createHash as createHash14 } from "crypto";
16456
+ import { readFileSync as readFileSync5 } from "fs";
16457
+ import { cp, mkdir as mkdir5, readdir as readdir19, readFile as readFile14, stat as stat17, writeFile as writeFile9 } from "fs/promises";
16458
+ import { join as join32, relative as relative5, resolve as resolve8, sep as sep2 } from "path";
16459
+ var SESSION_WINDOW_PAD_MS = 15 * 60 * 1e3;
16460
+ var KNOWN_LIMITS = [
16461
+ "'what was decided' is prose-only: handoff, progress, and reflection files are copied as markdown with no structured decision extraction",
16462
+ "'what was measured' is split between experiments.jsonl free-text fields and progress/handoff markdown tables",
16463
+ "session-to-ledger join is by timestamp/cwd inference: ledger rows are window-sliced, not linked by transcript path",
16464
+ "progress.md is copied whole; no session-dated section extraction is performed",
16465
+ "open pull-request state is not captured: the bundler makes no network calls"
16466
+ ];
16467
+ function sha256Hex(bytes) {
16468
+ return createHash14("sha256").update(bytes).digest("hex");
16469
+ }
16470
+ function bundlePath(...parts) {
16471
+ return parts.join("/");
16472
+ }
16473
+ async function statOrNull(path) {
16474
+ try {
16475
+ return await stat17(path);
16476
+ } catch (error) {
16477
+ if (isMissingPathError(error)) return null;
16478
+ if (error.code === "ENOTDIR") return null;
16479
+ throw error;
16480
+ }
16481
+ }
16482
+ function sliceJsonlByWindow(text2, windowStartMs, windowEndMs) {
16483
+ const kept = [];
16484
+ let totalRows = 0;
16485
+ let unparseableTsRows = 0;
16486
+ for (const line of text2.split("\n")) {
16487
+ if (!line.trim()) continue;
16488
+ totalRows += 1;
16489
+ let tsMs = Number.NaN;
16490
+ try {
16491
+ const row = JSON.parse(line);
16492
+ const ts = row !== null && typeof row === "object" && !Array.isArray(row) ? row.ts : void 0;
16493
+ if (typeof ts === "string") tsMs = Date.parse(ts);
16494
+ } catch {
16495
+ }
16496
+ if (Number.isNaN(tsMs)) {
16497
+ unparseableTsRows += 1;
16498
+ continue;
16499
+ }
16500
+ if (tsMs >= windowStartMs && tsMs <= windowEndMs) kept.push(line);
16501
+ }
16502
+ return { kept, totalRows, unparseableTsRows };
16503
+ }
16504
+ async function runGitLog(cwd, sinceIso, untilIso) {
16505
+ const { execFile: execFile2 } = await import("child_process");
16506
+ const { promisify: promisify2 } = await import("util");
16507
+ const run = promisify2(execFile2);
16508
+ try {
16509
+ const { stdout } = await run(
16510
+ "git",
16511
+ ["-C", cwd, "log", `--since=${sinceIso}`, `--until=${untilIso}`, "--date=iso-strict", "--pretty=format:%H %ad %an %s"],
16512
+ { timeout: 5e3 }
16513
+ );
16514
+ return { succeeded: true, stdout };
16515
+ } catch (error) {
16516
+ return { succeeded: false, error: error instanceof Error ? error.message.split("\n", 1)[0] : String(error) };
16517
+ }
16518
+ }
16519
+ async function listSessionBundleFiles(root) {
16520
+ const out = [];
16521
+ const pending = [resolve8(root)];
16522
+ while (pending.length > 0) {
16523
+ const dir = pending.pop();
16524
+ for (const entry of await readdir19(dir, { withFileTypes: true })) {
16525
+ const path = join32(dir, entry.name);
16526
+ if (entry.isDirectory()) pending.push(path);
16527
+ else if (entry.isFile()) out.push(relative5(resolve8(root), path).split(sep2).join("/"));
16528
+ }
16529
+ }
16530
+ return out.sort();
16531
+ }
16532
+ async function sliceLedgerJsonl(ctx, name) {
16533
+ const source = join32(ctx.evolveDir, name);
16534
+ const s = await statOrNull(source);
16535
+ if (!s?.isFile()) {
16536
+ ctx.absent.push({ path: bundlePath("ledger", name), reason: `no ${name} at ${source}` });
16537
+ return;
16538
+ }
16539
+ const text2 = await readFile14(source, "utf8");
16540
+ const target = join32(ctx.ledgerDir, name);
16541
+ if (ctx.windowStartMs === null || ctx.windowEndMs === null) {
16542
+ await writeFile9(target, text2, "utf8");
16543
+ ctx.slices.push({ path: bundlePath("ledger", name), source, rule: "copied-whole" });
16544
+ return;
16545
+ }
16546
+ const slice = sliceJsonlByWindow(text2, ctx.windowStartMs, ctx.windowEndMs);
16547
+ await writeFile9(target, slice.kept.length > 0 ? `${slice.kept.join("\n")}
16548
+ ` : "", "utf8");
16549
+ ctx.slices.push({
16550
+ path: bundlePath("ledger", name),
16551
+ source,
16552
+ rule: "session-window-rows",
16553
+ totalRows: slice.totalRows,
16554
+ keptRows: slice.kept.length,
16555
+ unparseableTsRows: slice.unparseableTsRows
16556
+ });
16557
+ }
16558
+ async function copyLedgerWhole(ctx, name) {
16559
+ const source = join32(ctx.evolveDir, name);
16560
+ const s = await statOrNull(source);
16561
+ if (!s?.isFile()) {
16562
+ ctx.absent.push({ path: bundlePath("ledger", name), reason: `no ${name} at ${source}` });
16563
+ return;
16564
+ }
16565
+ await cp(source, join32(ctx.ledgerDir, name));
16566
+ ctx.slices.push({ path: bundlePath("ledger", name), source, rule: "copied-whole" });
16567
+ }
16568
+ async function copyLatestHandoff(ctx) {
16569
+ let names;
16570
+ try {
16571
+ names = (await readdir19(ctx.evolveDir, { withFileTypes: true })).filter((entry) => entry.isFile() && /^handoff-.*\.md$/.test(entry.name)).map((entry) => entry.name).sort();
16572
+ } catch {
16573
+ names = [];
16574
+ }
16575
+ const latest = names[names.length - 1];
16576
+ if (!latest) {
16577
+ ctx.absent.push({
16578
+ path: bundlePath("ledger", "handoff-*.md"),
16579
+ reason: `no flat handoff-*.md files in ${ctx.evolveDir}`
16580
+ });
16581
+ return;
16582
+ }
16583
+ const source = join32(ctx.evolveDir, latest);
16584
+ await cp(source, join32(ctx.ledgerDir, latest));
16585
+ ctx.slices.push({ path: bundlePath("ledger", latest), source, rule: "latest-by-name" });
16586
+ }
16587
+ async function copyWindowReflections(ctx) {
16588
+ const reflectionsDir = join32(ctx.evolveDir, "reflections");
16589
+ const s = await statOrNull(reflectionsDir);
16590
+ if (!s?.isDirectory()) {
16591
+ ctx.absent.push({
16592
+ path: bundlePath("ledger", "reflections"),
16593
+ reason: `no reflections directory at ${reflectionsDir}`
16594
+ });
16595
+ return;
16596
+ }
16597
+ const names = (await readdir19(reflectionsDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name).sort();
16598
+ const windowed = ctx.windowStartMs === null || ctx.windowEndMs === null ? names : names.filter((name) => {
16599
+ const date = name.match(/(\d{4}-\d{2}-\d{2})/)?.[1];
16600
+ if (!date) return false;
16601
+ const startDate = new Date(ctx.windowStartMs).toISOString().slice(0, 10);
16602
+ const endDate = new Date(ctx.windowEndMs).toISOString().slice(0, 10);
16603
+ return date >= startDate && date <= endDate;
16604
+ });
16605
+ if (windowed.length === 0) {
16606
+ ctx.absent.push({
16607
+ path: bundlePath("ledger", "reflections"),
16608
+ reason: `no reflections dated inside the session window in ${reflectionsDir}`
16609
+ });
16610
+ return;
16611
+ }
16612
+ await mkdir5(join32(ctx.ledgerDir, "reflections"), { recursive: true });
16613
+ const rule = ctx.windowStartMs === null ? "copied-whole" : "session-window-dated-files";
16614
+ for (const name of windowed) {
16615
+ const source = join32(reflectionsDir, name);
16616
+ await cp(source, join32(ctx.ledgerDir, "reflections", name));
16617
+ ctx.slices.push({ path: bundlePath("ledger", "reflections", name), source, rule });
16618
+ }
16619
+ }
16620
+ async function assembleSessionBundle(opts) {
16621
+ const { adapter, ref } = opts;
16622
+ const generatedAt = opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
16623
+ let transcriptBytes;
16624
+ try {
16625
+ transcriptBytes = await readFile14(ref.path);
16626
+ } catch (error) {
16627
+ if (isMissingPathError(error)) {
16628
+ throw new Error(
16629
+ `session transcript not found at ${ref.path} \u2014 a bundle cannot be assembled without its transcript`
16630
+ );
16631
+ }
16632
+ throw error;
16633
+ }
16634
+ const transcriptSha256 = sha256Hex(transcriptBytes);
16635
+ const spans = await parseSession(adapter, ref, { signal: opts.signal });
16636
+ const outDir = resolve8(opts.outDir);
16637
+ await mkdir5(outDir, { recursive: true });
16638
+ if ((await readdir19(outDir)).length > 0) {
16639
+ throw new Error(
16640
+ `bundle output directory ${outDir} is not empty \u2014 pass a new directory so one bundle holds exactly one session`
16641
+ );
16642
+ }
16643
+ const absent = [];
16644
+ const slices = [];
16645
+ await mkdir5(join32(outDir, "session"), { recursive: true });
16646
+ await writeFile9(join32(outDir, "session", "transcript.jsonl"), transcriptBytes);
16647
+ const subagentsSource = join32(ref.path.replace(/\.jsonl$/, ""), "subagents");
16648
+ if ((await statOrNull(subagentsSource))?.isDirectory()) {
16649
+ await cp(subagentsSource, join32(outDir, "session", "subagents"), { recursive: true });
16650
+ } else {
16651
+ absent.push({ path: bundlePath("session", "subagents"), reason: `no subagents directory at ${subagentsSource}` });
16652
+ }
16653
+ await mkdir5(join32(outDir, "derived"), { recursive: true });
16654
+ const otlpPath = join32(outDir, "derived", "trace.otlp.jsonl");
16655
+ const investigation = await runTraceInvestigation({
16656
+ spans,
16657
+ harness: ref.harness,
16658
+ sources: [sessionReportSource(ref, spans)],
16659
+ cwds: ref.cwd ? [ref.cwd] : [],
16660
+ minLoopOccurrences: opts.minLoopOccurrences,
16661
+ otlpOutPath: otlpPath,
16662
+ generatedAt,
16663
+ signal: opts.signal,
16664
+ log: opts.log
16665
+ });
16666
+ await writeFile9(join32(outDir, "derived", "report.md"), investigation.report, "utf8");
16667
+ const evidence3 = await buildPolicyEvidenceRecord(ref, spans, {
16668
+ generatedAt,
16669
+ minLoopOccurrences: opts.minLoopOccurrences,
16670
+ // Bundle-relative pointer: the bundle must stay internally valid when moved.
16671
+ otlpPath: bundlePath("derived", "trace.otlp.jsonl"),
16672
+ sourceSha256: transcriptSha256
16673
+ });
16674
+ await writeFile9(join32(outDir, "derived", "evidence.jsonl"), serializePolicyEvidence([evidence3]), "utf8");
16675
+ const index = await buildSessionIndexFromRows([{ ref, spans }], {
16676
+ generatedAt,
16677
+ minLoopOccurrences: opts.minLoopOccurrences,
16678
+ selection: { command: "bundle", harness: ref.harness, session: ref.sessionId }
16679
+ });
16680
+ await writeFile9(join32(outDir, "derived", "session-index.json"), serializeSessionIndex(index), "utf8");
16681
+ const { firstSpanAt, lastSpanAt } = evidence3.metrics;
16682
+ const windowStartMs = firstSpanAt !== null ? Date.parse(firstSpanAt) - SESSION_WINDOW_PAD_MS : null;
16683
+ const windowEndMs = lastSpanAt !== null ? Date.parse(lastSpanAt) + SESSION_WINDOW_PAD_MS : null;
16684
+ const contextRoot = await findContextRoot(ref.cwd);
16685
+ if (!contextRoot) {
16686
+ absent.push({ path: "ledger", reason: `no context root found from session cwd ${ref.cwd ?? "(unknown)"}` });
16687
+ } else {
16688
+ const evolveDir = join32(contextRoot, ".evolve");
16689
+ if (!(await statOrNull(evolveDir))?.isDirectory()) {
16690
+ absent.push({ path: "ledger", reason: `no .evolve ledger at ${evolveDir}` });
16691
+ } else {
16692
+ const ledgerDir = join32(outDir, "ledger");
16693
+ await mkdir5(ledgerDir, { recursive: true });
16694
+ const ctx = { evolveDir, ledgerDir, windowStartMs, windowEndMs, slices, absent };
16695
+ await sliceLedgerJsonl(ctx, "experiments.jsonl");
16696
+ await sliceLedgerJsonl(ctx, "skill-runs.jsonl");
16697
+ await copyLedgerWhole(ctx, "current.json");
16698
+ await copyLedgerWhole(ctx, "scorecard.json");
16699
+ await copyLedgerWhole(ctx, "progress.md");
16700
+ await copyLatestHandoff(ctx);
16701
+ await copyWindowReflections(ctx);
16702
+ }
16703
+ }
16704
+ const gitLogPath = bundlePath("repo", "git-log.txt");
16705
+ if (!ref.cwd) {
16706
+ absent.push({ path: gitLogPath, reason: "session has no recorded cwd to read git history from" });
16707
+ } else if (windowStartMs === null || windowEndMs === null) {
16708
+ absent.push({ path: gitLogPath, reason: "session window unavailable (spans carry no timestamps)" });
16709
+ } else {
16710
+ const log = await runGitLog(ref.cwd, new Date(windowStartMs).toISOString(), new Date(windowEndMs).toISOString());
16711
+ if (log.succeeded) {
16712
+ await mkdir5(join32(outDir, "repo"), { recursive: true });
16713
+ await writeFile9(join32(outDir, "repo", "git-log.txt"), log.stdout ? `${log.stdout}
16714
+ ` : "", "utf8");
16715
+ } else {
16716
+ absent.push({ path: gitLogPath, reason: `git log failed in ${ref.cwd}: ${log.error}` });
16717
+ }
16718
+ }
16719
+ const files = await hashSessionBundleFiles(outDir);
16720
+ const manifest = {
16721
+ schemaVersion: 2,
16722
+ kind: "traces.session_bundle",
16723
+ view: "full",
16724
+ createdAt: generatedAt,
16725
+ provenance: {
16726
+ sessionId: ref.sessionId,
16727
+ harness: ref.harness,
16728
+ cwd: ref.cwd,
16729
+ transcriptPath: resolve8(ref.path),
16730
+ transcriptSha256,
16731
+ contextRoot,
16732
+ tracesVersion: tracesPackageVersion(),
16733
+ sessionWindow: { firstSpanAt, lastSpanAt, padMs: SESSION_WINDOW_PAD_MS }
16734
+ },
16735
+ files,
16736
+ absent,
16737
+ // The full view is the whole record: it drops nothing, and says so rather
16738
+ // than leaving a reader to infer completeness from a missing field.
16739
+ excluded: [],
16740
+ ledgerSlices: slices,
16741
+ knownLimits: KNOWN_LIMITS
16742
+ };
16743
+ const manifestPath = await writeSessionBundleManifest(outDir, manifest);
16744
+ return { directory: outDir, manifestPath, manifest };
16745
+ }
16746
+ async function hashSessionBundleFiles(root) {
16747
+ const files = [];
16748
+ for (const path of await listSessionBundleFiles(root)) {
16749
+ const bytes = await readFile14(join32(root, path));
16750
+ files.push({ path, bytes: bytes.length, sha256: sha256Hex(bytes) });
16751
+ }
16752
+ return files;
16753
+ }
16754
+ async function writeSessionBundleManifest(outDir, manifest) {
16755
+ const manifestPath = join32(outDir, "manifest.json");
16756
+ await writeFile9(manifestPath, `${JSON.stringify(manifest, null, 2)}
16757
+ `, "utf8");
16758
+ return manifestPath;
16759
+ }
16760
+ async function readSessionBundleManifest(bundleDir) {
16761
+ const path = join32(resolve8(bundleDir), "manifest.json");
16762
+ let text2;
16763
+ try {
16764
+ text2 = await readFile14(path, "utf8");
16765
+ } catch (error) {
16766
+ if (isMissingPathError(error)) {
16767
+ throw new Error(`no manifest.json at ${path} \u2014 ${bundleDir} is not a traces session bundle`);
16768
+ }
16769
+ throw error;
16770
+ }
16771
+ let parsed;
16772
+ try {
16773
+ parsed = JSON.parse(text2);
16774
+ } catch (error) {
16775
+ throw new Error(`${path} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
16776
+ }
16777
+ const manifest = parsed;
16778
+ if (manifest?.kind !== "traces.session_bundle") {
16779
+ throw new Error(`${path} has kind ${JSON.stringify(manifest?.kind)}, expected "traces.session_bundle"`);
16780
+ }
16781
+ if (manifest.schemaVersion !== 2) {
16782
+ throw new Error(
16783
+ `${path} is bundle schemaVersion ${JSON.stringify(manifest.schemaVersion)}; this traces reads 2. Re-run \`traces bundle\` to assemble it with the view-aware manifest.`
16784
+ );
16785
+ }
16786
+ if (manifest.view !== "full" && manifest.view !== "evidence-only") {
16787
+ throw new Error(`${path} has view ${JSON.stringify(manifest.view)}, expected "full" or "evidence-only"`);
16788
+ }
16789
+ return manifest;
16790
+ }
16791
+ var cachedVersion;
16792
+ function tracesPackageVersion() {
16793
+ if (cachedVersion) return cachedVersion;
16794
+ const pkg = JSON.parse(
16795
+ readFileSync5(new URL("../package.json", import.meta.url), "utf8")
16796
+ );
16797
+ if (typeof pkg.version !== "string" || !pkg.version) throw new Error("package.json is missing version");
16798
+ cachedVersion = pkg.version;
16799
+ return cachedVersion;
16800
+ }
16801
+
16802
+ // src/bundle-view.ts
16803
+ import { createHash as createHash15 } from "crypto";
16804
+ import { mkdir as mkdir6, readdir as readdir20, readFile as readFile15, writeFile as writeFile10 } from "fs/promises";
16805
+ import { dirname as dirname9, join as join33, resolve as resolve9 } from "path";
16806
+ var EVIDENCE_ONLY_ALLOW_LIST = [
16807
+ {
16808
+ path: "derived/session-index.json",
16809
+ owned: true,
16810
+ carries: "per-session counts, tool names, models, and nearby-context file metadata"
16811
+ },
16812
+ {
16813
+ path: "derived/evidence.jsonl",
16814
+ owned: true,
16815
+ carries: "execution statistics, token and tool totals, loop and error counts"
16816
+ },
16817
+ {
16818
+ path: "ledger/current.json",
16819
+ owned: false,
16820
+ carries: "the repo ledger current-state record"
16821
+ },
16822
+ {
16823
+ path: "ledger/scorecard.json",
16824
+ owned: false,
16825
+ carries: "the repo ledger scorecard rows"
16826
+ },
16827
+ {
16828
+ path: "ledger/skill-runs.jsonl",
16829
+ owned: false,
16830
+ carries: "skill-run rows: skill name, timestamp, and outcome fields"
16831
+ }
16832
+ ];
16833
+ var EXCLUSION_RULES = [
16834
+ {
16835
+ matches: (path) => path === "session/transcript.jsonl" || path.startsWith("session/"),
16836
+ rule: "session-source",
16837
+ 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"
16838
+ },
16839
+ {
16840
+ matches: (path) => path === "derived/report.md",
16841
+ rule: "session-text-derived",
16842
+ reason: "the deterministic report quotes the session verbatim: the first prompt line and evidence excerpts per finding"
16843
+ },
16844
+ {
16845
+ matches: (path) => path === "derived/trace.otlp.jsonl",
16846
+ rule: "session-text-derived",
16847
+ reason: "spans carry input.value, output.value and content: the full prompt and response text of every turn"
16848
+ },
16849
+ {
16850
+ matches: (path) => path === "ledger/progress.md",
16851
+ rule: "authored-prose",
16852
+ reason: "progress notes are written at session close and restate what the session concluded"
16853
+ },
16854
+ {
16855
+ matches: (path) => /^ledger\/handoff-.*\.md$/.test(path),
16856
+ rule: "authored-prose",
16857
+ reason: "a handoff is the session conclusion in prose"
16858
+ },
16859
+ {
16860
+ matches: (path) => path.startsWith("ledger/reflections/"),
16861
+ rule: "authored-prose",
16862
+ reason: "a reflection is the session conclusion in prose"
16863
+ },
16864
+ {
16865
+ matches: (path) => path === "ledger/experiments.jsonl",
16866
+ rule: "authored-prose",
16867
+ reason: "experiment rows carry free-text verdict and note fields that restate what the session concluded"
16868
+ },
16869
+ {
16870
+ matches: (path) => path === "repo/git-log.txt",
16871
+ rule: "authored-prose",
16872
+ reason: "commit subjects are prose the session authored"
16873
+ },
16874
+ {
16875
+ matches: () => true,
16876
+ rule: "not-allow-listed",
16877
+ reason: "the evidence-only view carries an explicit allow-list of counted artifacts, and this path is not on it"
16878
+ }
16879
+ ];
16880
+ var SIGNATURE_WORDS = 8;
16881
+ var MIN_WORD_TOKENS = 7;
16882
+ function scrubIdentifiers(line) {
16883
+ return line.replace(/[a-z][a-z0-9+.-]*:\/\/\S*/gi, " ").replace(/\/?(?:[\w.@+~-]+\/)+[\w.@+~-]*/g, " ").replace(/\b[0-9a-f-]{8,}\b/gi, " ");
16884
+ }
16885
+ var PACKAGE_CONSTANT_PROSE = [POLICY_EVIDENCE_NOTE];
16886
+ function stripPackageConstants(text2) {
16887
+ let out = text2;
16888
+ for (const constant of PACKAGE_CONSTANT_PROSE) out = out.split(constant).join(" ");
16889
+ return out;
16890
+ }
16891
+ function collectJsonStrings(value, into) {
16892
+ if (typeof value === "string") into.push(value);
16893
+ else if (Array.isArray(value)) for (const item of value) collectJsonStrings(item, into);
16894
+ else if (value !== null && typeof value === "object") {
16895
+ for (const item of Object.values(value)) collectJsonStrings(item, into);
16896
+ }
16897
+ }
16898
+ function proseFragments(line) {
16899
+ const trimmed = line.trim();
16900
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
16901
+ try {
16902
+ const parsed = JSON.parse(trimmed);
16903
+ const strings = [];
16904
+ collectJsonStrings(parsed, strings);
16905
+ return strings;
16906
+ } catch {
16907
+ }
16908
+ }
16909
+ return [line];
16910
+ }
16911
+ function sessionTextSignatures(text2) {
16912
+ const out = /* @__PURE__ */ new Set();
16913
+ for (const line of stripPackageConstants(text2).split("\n")) {
16914
+ for (const fragment of proseFragments(line)) {
16915
+ const words = scrubIdentifiers(fragment).toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().split(" ").filter(Boolean);
16916
+ for (let i = 0; i + SIGNATURE_WORDS <= words.length; i++) {
16917
+ const window = words.slice(i, i + SIGNATURE_WORDS);
16918
+ if (window.filter((word) => /^[a-z]+$/.test(word)).length < MIN_WORD_TOKENS) continue;
16919
+ out.add(window.join(" "));
16920
+ }
16921
+ }
16922
+ }
16923
+ return out;
16924
+ }
16925
+ async function findSessionTextLeaks(opts) {
16926
+ const index = /* @__PURE__ */ new Map();
16927
+ const carried = [];
16928
+ for (const path of opts.carriedPaths) {
16929
+ const signatures = sessionTextSignatures(await readFile15(join33(opts.viewDir, path), "utf8"));
16930
+ carried.push({ path, signatures: signatures.size });
16931
+ for (const signature of signatures) if (!index.has(signature)) index.set(signature, path);
16932
+ }
16933
+ const hits = /* @__PURE__ */ new Map();
16934
+ const comparedSources = [];
16935
+ let sourceSignatures = 0;
16936
+ for (const sourcePath of opts.excludedPaths) {
16937
+ const signatures = sessionTextSignatures(await readFile15(join33(opts.sourceDir, sourcePath), "utf8"));
16938
+ comparedSources.push(sourcePath);
16939
+ sourceSignatures += signatures.size;
16940
+ for (const signature of signatures) {
16941
+ const viewPath = index.get(signature);
16942
+ if (viewPath === void 0) continue;
16943
+ const hit = hits.get(viewPath) ?? { matched: /* @__PURE__ */ new Set(), sources: /* @__PURE__ */ new Set(), example: signature };
16944
+ hit.matched.add(signature);
16945
+ hit.sources.add(sourcePath);
16946
+ hits.set(viewPath, hit);
16947
+ }
16948
+ }
16949
+ const leaks = [...hits].map(([viewPath, hit]) => ({
16950
+ viewPath,
16951
+ sources: [...hit.sources].sort(),
16952
+ signatures: hit.matched.size,
16953
+ example: hit.example
16954
+ }));
16955
+ return { leaks, carried, comparedSources, sourceSignatures };
16956
+ }
16957
+ function classifyEvidenceOnlyPath(path) {
16958
+ const allowed = EVIDENCE_ONLY_ALLOW_LIST.find((entry) => entry.path === path);
16959
+ if (allowed) return { carried: true, owned: allowed.owned, carries: allowed.carries };
16960
+ const rule = EXCLUSION_RULES.find((entry) => entry.matches(path));
16961
+ return { carried: false, rule: rule.rule, reason: rule.reason };
16962
+ }
16963
+ var EVIDENCE_ONLY_LIMITS = [
16964
+ "the exclusion is verbatim-tight, not paraphrase-tight: a conclusion restated in different words inside a carried artifact is not detectable by content signatures",
16965
+ "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",
16966
+ "the view carries counts, names, identifiers, timestamps and hashes; a question that needs the words of a turn cannot be answered from it"
16967
+ ];
16968
+ async function projectSessionBundle(opts) {
16969
+ if (opts.view !== "evidence-only") {
16970
+ throw new Error(
16971
+ `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".`
16972
+ );
16973
+ }
16974
+ const generatedAt = opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
16975
+ const sourceDir = resolve9(opts.bundleDir);
16976
+ const source = await readSessionBundleManifest(sourceDir);
16977
+ if (source.view !== "full") {
16978
+ throw new Error(
16979
+ `${sourceDir} is already the ${source.view} view \u2014 project from the full bundle, not from a view of it`
16980
+ );
16981
+ }
16982
+ const outDir = resolve9(opts.outDir);
16983
+ await mkdir6(outDir, { recursive: true });
16984
+ if ((await readdir20(outDir)).length > 0) {
16985
+ throw new Error(
16986
+ `view output directory ${outDir} is not empty \u2014 pass a new directory so one directory holds exactly one view`
16987
+ );
16988
+ }
16989
+ const sourceHashes = new Map(source.files.map((file) => [file.path, file]));
16990
+ const carriedPaths = [];
16991
+ const carries = /* @__PURE__ */ new Map();
16992
+ const excluded = [];
16993
+ const candidates = [];
16994
+ for (const path of await listSessionBundleFiles(sourceDir)) {
16995
+ if (path === "manifest.json") continue;
16996
+ const bytes = await readFile15(join33(sourceDir, path));
16997
+ const sha2562 = createHash15("sha256").update(bytes).digest("hex");
16998
+ const recorded = sourceHashes.get(path);
16999
+ if (!recorded) {
17000
+ throw new Error(
17001
+ `${join33(sourceDir, path)} is not listed in the bundle manifest \u2014 the bundle changed after assembly and cannot be projected; re-run \`traces bundle\``
17002
+ );
17003
+ }
17004
+ if (recorded.sha256 !== sha2562) {
17005
+ throw new Error(
17006
+ `${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\``
17007
+ );
17008
+ }
17009
+ const verdict = classifyEvidenceOnlyPath(path);
17010
+ if (!verdict.carried) {
17011
+ excluded.push({ path, bytes: bytes.length, sha256: sha2562, rule: verdict.rule, reason: verdict.reason });
17012
+ continue;
17013
+ }
17014
+ candidates.push({ path, bytes, sha256: sha2562, carries: verdict.carries, owned: verdict.owned });
17015
+ }
17016
+ const report = await findSessionTextLeaks({
17017
+ viewDir: sourceDir,
17018
+ sourceDir,
17019
+ carriedPaths: candidates.map((candidate) => candidate.path),
17020
+ excludedPaths: excluded.map((entry) => entry.path)
17021
+ });
17022
+ const leakByPath = new Map(report.leaks.map((leak) => [leak.viewPath, leak]));
17023
+ const ownedLeak = report.leaks.find((leak) => candidates.some((c) => c.path === leak.viewPath && c.owned));
17024
+ if (ownedLeak) {
17025
+ throw new Error(
17026
+ `${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.`
17027
+ );
17028
+ }
17029
+ for (const candidate of candidates) {
17030
+ const leak = leakByPath.get(candidate.path);
17031
+ if (leak) {
17032
+ excluded.push({
17033
+ path: candidate.path,
17034
+ bytes: candidate.bytes.length,
17035
+ sha256: candidate.sha256,
17036
+ rule: "content-signature",
17037
+ 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`,
17038
+ contentMatch: { signatures: leak.signatures, sources: leak.sources }
17039
+ });
17040
+ continue;
17041
+ }
17042
+ const target = join33(outDir, candidate.path);
17043
+ await mkdir6(dirname9(target), { recursive: true });
17044
+ await writeFile10(target, candidate.bytes);
17045
+ carriedPaths.push(candidate.path);
17046
+ carries.set(candidate.path, candidate.carries);
17047
+ }
17048
+ const leakCheck = {
17049
+ signatureWords: SIGNATURE_WORDS,
17050
+ comparedSources: report.comparedSources,
17051
+ sourceSignatures: report.sourceSignatures,
17052
+ carried: report.carried.filter((entry) => carries.has(entry.path)).map((entry) => ({
17053
+ path: entry.path,
17054
+ carries: carries.get(entry.path),
17055
+ signatures: entry.signatures
17056
+ })),
17057
+ matches: 0
17058
+ };
17059
+ const sourceManifestBytes = await readFile15(join33(sourceDir, "manifest.json"));
17060
+ const manifest = {
17061
+ schemaVersion: 2,
17062
+ kind: "traces.session_bundle",
17063
+ view: "evidence-only",
17064
+ createdAt: generatedAt,
17065
+ provenance: source.provenance,
17066
+ files: await hashSessionBundleFiles(outDir),
17067
+ absent: source.absent,
17068
+ excluded,
17069
+ ledgerSlices: source.ledgerSlices.filter((slice) => carriedPaths.includes(slice.path)),
17070
+ knownLimits: [...source.knownLimits, ...EVIDENCE_ONLY_LIMITS],
17071
+ projection: {
17072
+ sourceDirectory: sourceDir,
17073
+ sourceManifestSha256: createHash15("sha256").update(sourceManifestBytes).digest("hex"),
17074
+ leakCheck
17075
+ }
17076
+ };
17077
+ const manifestPath = await writeSessionBundleManifest(outDir, manifest);
17078
+ return { directory: outDir, manifestPath, manifest, leakCheck };
17079
+ }
17080
+
15872
17081
  // src/upload-state.ts
15873
- import { mkdir as mkdir5, readFile as readFile14, writeFile as writeFile9 } from "fs/promises";
17082
+ import { mkdir as mkdir7, readFile as readFile16, writeFile as writeFile11 } from "fs/promises";
15874
17083
  import { homedir as homedir12 } from "os";
15875
- import { dirname as dirname9, join as join32 } from "path";
17084
+ import { dirname as dirname10, join as join34 } from "path";
15876
17085
  import { contentHash } from "@tangle-network/agent-eval";
15877
17086
  function stateFile() {
15878
- const base = process.env.XDG_STATE_HOME ?? join32(homedir12(), ".local", "state");
15879
- return join32(base, "tangle-traces", "uploaded.json");
17087
+ const base = process.env.XDG_STATE_HOME ?? join34(homedir12(), ".local", "state");
17088
+ return join34(base, "tangle-traces", "uploaded.json");
15880
17089
  }
15881
17090
  function uploadKey(harness, sessionId) {
15882
17091
  return `${harness}:${sessionId}`;
@@ -15889,15 +17098,15 @@ function outboundHash(events, identity) {
15889
17098
  }
15890
17099
  async function loadState() {
15891
17100
  try {
15892
- return JSON.parse(await readFile14(stateFile(), "utf8"));
17101
+ return JSON.parse(await readFile16(stateFile(), "utf8"));
15893
17102
  } catch {
15894
17103
  return {};
15895
17104
  }
15896
17105
  }
15897
17106
  async function saveState(state) {
15898
17107
  const f = stateFile();
15899
- await mkdir5(dirname9(f), { recursive: true });
15900
- await writeFile9(f, `${JSON.stringify(state, null, 2)}
17108
+ await mkdir7(dirname10(f), { recursive: true });
17109
+ await writeFile11(f, `${JSON.stringify(state, null, 2)}
15901
17110
  `, "utf8");
15902
17111
  }
15903
17112
  function alreadyUploaded(state, harness, sessionId, hash2) {
@@ -15905,10 +17114,10 @@ function alreadyUploaded(state, harness, sessionId, hash2) {
15905
17114
  }
15906
17115
 
15907
17116
  // src/upload.ts
15908
- import { readFile as readFile15 } from "fs/promises";
17117
+ import { readFile as readFile17 } from "fs/promises";
15909
17118
  import { createRequire as createRequire2 } from "module";
15910
17119
  import { hostname as hostname2 } from "os";
15911
- import { basename as basename15, join as join33 } from "path";
17120
+ import { basename as basename15, join as join35 } from "path";
15912
17121
  import { hostedClientFromEnv } from "@tangle-network/agent-eval/hosted";
15913
17122
  import { REDACTION_VERSION } from "@tangle-network/agent-eval/traces";
15914
17123
  var require2 = createRequire2(import.meta.url);
@@ -15932,7 +17141,7 @@ async function planUpload(opts = {}) {
15932
17141
  async function gitBranch(cwd) {
15933
17142
  if (!cwd) return void 0;
15934
17143
  try {
15935
- const head = await readFile15(join33(cwd, ".git", "HEAD"), "utf8");
17144
+ const head = await readFile17(join35(cwd, ".git", "HEAD"), "utf8");
15936
17145
  const m = head.match(/^ref:\s*refs\/heads\/(.+)$/m);
15937
17146
  return m?.[1]?.trim();
15938
17147
  } catch {
@@ -15941,16 +17150,16 @@ async function gitBranch(cwd) {
15941
17150
  }
15942
17151
  async function sessionMeta(item) {
15943
17152
  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;
17153
+ [ATTR.HARNESS]: item.ref.harness,
17154
+ [ATTR.SESSION_FILE]: basename15(item.ref.path),
17155
+ [ATTR.HOST]: hostname2(),
17156
+ [ATTR.UPLOADER]: `tangle-traces@${TRACES_VERSION}`,
17157
+ [ATTR.REDACTION_VERSION]: REDACTION_VERSION,
17158
+ [ATTR.REDACTION_COUNT]: item.redaction.redactionCount
17159
+ };
17160
+ if (item.ref.cwd) meta[ATTR.CWD] = item.ref.cwd;
15952
17161
  const branch = await gitBranch(item.ref.cwd);
15953
- if (branch) meta[ATTR2.GIT_BRANCH] = branch;
17162
+ if (branch) meta[ATTR.GIT_BRANCH] = branch;
15954
17163
  return meta;
15955
17164
  }
15956
17165
  var msToNano = (epochMs) => (BigInt(epochMs) * 1000000n).toString();
@@ -15961,8 +17170,8 @@ function toTraceSpanEvents(spans, rootMeta) {
15961
17170
  if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") attributes2[k] = v;
15962
17171
  else if (v != null) attributes2[k] = JSON.stringify(v);
15963
17172
  }
15964
- attributes2[ATTR2.SESSION_ID] = s.trace_id;
15965
- attributes2[ATTR2.INGEST_SOURCE] = INGEST_SOURCE_CLI;
17173
+ attributes2[ATTR.SESSION_ID] = s.trace_id;
17174
+ attributes2[ATTR.INGEST_SOURCE] = INGEST_SOURCE_CLI;
15966
17175
  if (s.parent_span_id === null) Object.assign(attributes2, rootMeta);
15967
17176
  const startMs = parseIsoToEpochMs(s.start_time);
15968
17177
  const endMs = Math.max(startMs, parseIsoToEpochMs(s.end_time));
@@ -16021,7 +17230,7 @@ async function prepareOutbound(plan, opts, uploadedAt) {
16021
17230
  const key = uploadKey(item.ref.harness, item.ref.sessionId);
16022
17231
  const previous = plan.state[key];
16023
17232
  const meta = await sessionMeta(item);
16024
- const eventsAt = (timestamp) => toTraceSpanEvents(item.spans, { ...meta, [ATTR2.UPLOADED_AT]: timestamp });
17233
+ const eventsAt = (timestamp) => toTraceSpanEvents(item.spans, { ...meta, [ATTR.UPLOADED_AT]: timestamp });
16025
17234
  let eventTimestamp2 = previous?.uploadedAt ?? uploadedAt;
16026
17235
  let events = eventsAt(eventTimestamp2);
16027
17236
  let hash2 = outboundHash(events, identity);
@@ -16099,6 +17308,15 @@ async function executeUpload(plan, opts = {}) {
16099
17308
 
16100
17309
  export {
16101
17310
  appendAll,
17311
+ ATTR,
17312
+ INGEST_SOURCE_CLI,
17313
+ environmentAttributes,
17314
+ stampEnvironmentAttrs,
17315
+ environmentFromSpanAttributes,
17316
+ DEFAULT_HARNESS,
17317
+ sessionIdFromAttributes,
17318
+ indexSessionIdsByTrace,
17319
+ stampSessionIdentity,
16102
17320
  parseIsoToEpochMs,
16103
17321
  parseSince,
16104
17322
  resolveOtlpInputFiles,
@@ -16115,12 +17333,6 @@ export {
16115
17333
  capText,
16116
17334
  ACTOR_ATTR,
16117
17335
  userPromptSpan,
16118
- ATTR2 as ATTR,
16119
- INGEST_SOURCE_CLI,
16120
- DEFAULT_HARNESS,
16121
- sessionIdFromAttributes,
16122
- indexSessionIdsByTrace,
16123
- stampSessionIdentity,
16124
17336
  isChatTrajectoryMessage,
16125
17337
  isChatTrajectory,
16126
17338
  chatTrajectoryToSpans,
@@ -16173,6 +17385,7 @@ export {
16173
17385
  scanSessions,
16174
17386
  SessionWorkflowError,
16175
17387
  describeSessionRelationship,
17388
+ POLICY_EVIDENCE_NOTE,
16176
17389
  buildPolicyEvidenceRecord,
16177
17390
  collectPolicyEvidence,
16178
17391
  serializePolicyEvidence,
@@ -16233,6 +17446,8 @@ export {
16233
17446
  commandRedactor,
16234
17447
  writeHodoscopeInput,
16235
17448
  hodoscopeAnalyzer,
17449
+ httpJsonTransport,
17450
+ primeAnalyzer,
16236
17451
  CODING_REDACTION_RULES,
16237
17452
  TRACES_REDACTION_RULES,
16238
17453
  redactSpans,
@@ -16263,7 +17478,7 @@ export {
16263
17478
  analyzeSteeringChain,
16264
17479
  REACTIONS,
16265
17480
  analyzeReactions,
16266
- CORRUPTION_RECEIPT_DISPLAY_LIMIT,
17481
+ sessionReportSource,
16267
17482
  buildTraceFindingPacket,
16268
17483
  loadTracesConfig,
16269
17484
  mergeTracesConfig,
@@ -16320,10 +17535,20 @@ export {
16320
17535
  collectSessionWorkflow,
16321
17536
  collectSessionSelection,
16322
17537
  buildSessionIndex,
17538
+ findContextRoot,
16323
17539
  buildSessionIndexFromRows,
16324
17540
  collectSessionIndex,
16325
17541
  serializeSessionIndex,
16326
17542
  writeSessionIndexFile,
17543
+ listSessionBundleFiles,
17544
+ assembleSessionBundle,
17545
+ hashSessionBundleFiles,
17546
+ writeSessionBundleManifest,
17547
+ readSessionBundleManifest,
17548
+ sessionTextSignatures,
17549
+ findSessionTextLeaks,
17550
+ classifyEvidenceOnlyPath,
17551
+ projectSessionBundle,
16327
17552
  uploadKey,
16328
17553
  sessionHash,
16329
17554
  outboundHash,