@tangle-network/traces 0.23.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +61 -1078
  2. package/dist/analyst-model-call-JVUOYZ5V.js +20 -0
  3. package/dist/ask-XLS3VANY.js +39 -0
  4. package/dist/check-YF6ZHPGG.js +322 -0
  5. package/dist/chunk-3I4PJ5Q7.js +25 -0
  6. package/dist/chunk-3T62R7ON.js +5695 -0
  7. package/dist/chunk-4VKPN432.js +138 -0
  8. package/dist/chunk-6CLC7DKP.js +492 -0
  9. package/dist/chunk-7CBV5RJ2.js +535 -0
  10. package/dist/chunk-B62DOFGI.js +273 -0
  11. package/dist/chunk-CL3OQOWX.js +407 -0
  12. package/dist/chunk-DLGAKCQJ.js +782 -0
  13. package/dist/chunk-DU534HAX.js +263 -0
  14. package/dist/chunk-GLUDGUGJ.js +232 -0
  15. package/dist/chunk-J2PM5SVN.js +503 -0
  16. package/dist/chunk-M3W6TZHQ.js +381 -0
  17. package/dist/chunk-NH7HCDLB.js +759 -0
  18. package/dist/chunk-NZ5Y7UEL.js +1217 -0
  19. package/dist/chunk-O52XK7OB.js +19 -0
  20. package/dist/chunk-PYH3PL3N.js +62 -0
  21. package/dist/chunk-RZL5GAWH.js +60 -0
  22. package/dist/chunk-TR6O4S4T.js +208 -0
  23. package/dist/chunk-TW43MR5D.js +259 -0
  24. package/dist/chunk-TYLFUX42.js +228 -0
  25. package/dist/chunk-UOLRCQHC.js +120 -0
  26. package/dist/chunk-UVE5ANVV.js +3921 -0
  27. package/dist/chunk-V2OCGX57.js +53 -0
  28. package/dist/cli.js +2086 -2050
  29. package/dist/diff-ZZ6KYBPP.js +16 -0
  30. package/dist/improvement-OJQ6365E.js +25 -0
  31. package/dist/index.d.ts +1235 -4357
  32. package/dist/index.js +53 -1204
  33. package/dist/live-Y64AJ6MA.js +14 -0
  34. package/dist/mcp-ZPGTJQYQ.js +85 -0
  35. package/dist/report-JAMXRTCW.js +338 -0
  36. package/dist/run-watch-PLWTIRSV.js +649 -0
  37. package/dist/supervisor-report-RMPLLBGZ.js +29 -0
  38. package/dist/supervisor-run-context-WO6VOTGV.js +30 -0
  39. package/dist/upload-R6NMKSGQ.js +20 -0
  40. package/dist/verify-safe-C5ZXZOZ2.js +8 -0
  41. package/docs/analyze.md +76 -0
  42. package/docs/ask.md +88 -0
  43. package/docs/bundle.md +40 -0
  44. package/docs/check.md +77 -0
  45. package/docs/convert.md +44 -0
  46. package/docs/develop.md +32 -0
  47. package/docs/diff.md +38 -0
  48. package/docs/facts.md +114 -0
  49. package/docs/mcp.md +36 -0
  50. package/docs/migrate-1.0.md +33 -0
  51. package/docs/parsers.md +66 -0
  52. package/docs/release-1.0.0.md +147 -0
  53. package/docs/sdk.md +77 -0
  54. package/docs/select.md +70 -0
  55. package/docs/skills.md +46 -0
  56. package/docs/upload.md +47 -0
  57. package/docs/watch-jsonl.v1.example.jsonl +5 -0
  58. package/docs/watch.md +66 -0
  59. package/examples/README.md +9 -0
  60. package/examples/custom-analyst.ts +65 -74
  61. package/install.sh +22 -2
  62. package/package.json +8 -7
  63. package/skills/inspect-agent-traces/SKILL.md +47 -110
  64. package/dist/chunk-7DVZCCZI.js +0 -20983
  65. package/docs/replay-verify.md +0 -123
  66. package/docs/stream-findings.svg +0 -39
  67. package/docs/trace-analysts.md +0 -643
  68. package/examples/custom-backend.ts +0 -21
  69. package/examples/external-engines.ts +0 -62
  70. package/examples/improvement-config.mjs +0 -63
  71. package/examples/observe-and-alert.ts +0 -19
  72. package/examples/register-harness.ts +0 -26
  73. package/skills/build-trace-analyst/SKILL.md +0 -76
  74. package/skills/build-trace-analyst/agents/openai.yaml +0 -4
package/dist/cli.js CHANGED
@@ -1,733 +1,1333 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ assembleSessionBundle,
4
+ collectSessionSelection,
5
+ verifySessionBundle
6
+ } from "./chunk-DLGAKCQJ.js";
7
+ import {
8
+ conformanceOfSpans,
9
+ renderValidation
10
+ } from "./chunk-TW43MR5D.js";
11
+ import {
12
+ findingRejectionDetail
13
+ } from "./chunk-PYH3PL3N.js";
2
14
  import {
3
15
  DEFAULT_ANALYST_MODEL,
16
+ DEFAULT_ASK_CONCURRENCY,
4
17
  DEFAULT_QUESTION_MAX_COST_USD,
5
18
  DEFAULT_QUESTION_TIMEOUT_MS,
6
- DEFAULT_SANDBOX_BASE_URL,
7
- DEFAULT_SWEEP_THRESHOLD,
8
- MAX_TRACE_QUESTION_CHARS,
19
+ MAX_TRACE_QUESTION_CHARS
20
+ } from "./chunk-O52XK7OB.js";
21
+ import {
22
+ metadataOnly
23
+ } from "./chunk-V2OCGX57.js";
24
+ import {
25
+ RefError,
26
+ resolveRefs,
27
+ selectSpans,
28
+ shortestRefs
29
+ } from "./chunk-CL3OQOWX.js";
30
+ import {
31
+ exportTraceEvidenceFile
32
+ } from "./chunk-6CLC7DKP.js";
33
+ import {
9
34
  SkillUsageStore,
10
- analysisEngineFromEnv,
11
- appendAll,
12
- assembleSessionBundle,
13
- assertOutsideSourceBundle,
14
- assessSpans,
15
- buildPolicyEvidenceRecord,
16
- buildSessionIndexFromRows,
35
+ TurnIndex,
17
36
  buildSkillUsageReport,
18
- cmdReplayVerify,
19
- cmdReplayVerifyBatch,
20
- cmdVerifyFindings,
21
- collectSessionSelection,
22
- commandAnalyzer,
23
- commandRedactor,
24
37
  createSessionFactsReportBuilder,
25
38
  defaultSkillUsageStorePath,
26
- diffRuns,
27
- executeUpload,
28
- exportTraceEvidenceFile,
29
- externalFailureMessage,
30
- fileRunContextSupervisorRunReader,
31
- findingRejectionDetail,
32
- formatVerdict,
33
- haloAnalyzer,
34
- hodoscopeAnalyzer,
35
- importCodeTraceBench,
36
- indexSessionIdsByTrace,
37
- inspectSessionIndex,
38
- isBridgeMismatchError,
39
- iterSerializedSpans,
40
- knownHarnesses,
41
- loadTraceQuestionsFile,
42
- loadTracesConfig,
43
- locateSessions,
44
- mergeTracesConfig,
45
- normalizeTraceQuestions,
46
- openAgenticTraceStore,
47
- parseIsoToEpochMs,
48
- parseSession,
49
- parseSince,
50
- parseTimeBound,
51
- planUpload,
52
- primeAnalyzer,
53
- projectSessionBundle,
39
+ matchRows,
40
+ priceReport,
54
41
  readInstalledSkills,
55
- readJsonl,
56
- readOtlpInput,
57
- readSessionIndexFile,
58
- redactSpans,
59
- renderInspectionReport,
60
- renderRunDiff,
42
+ readSessionTurnGroups,
61
43
  renderSessionFacts,
62
44
  renderSkillUsage,
63
- renderValidation,
45
+ sessionIdentityLookup,
46
+ sessionMatches
47
+ } from "./chunk-UVE5ANVV.js";
48
+ import {
49
+ knownHarnesses,
50
+ listAdapters,
51
+ locateSessions,
52
+ parseSession,
53
+ readSessionSpans,
54
+ readsStoreRoots,
64
55
  resolveAdapter,
65
- resolveRunWatchTarget,
66
- runTraceImprovement,
67
- runTraceInvestigation,
68
- runTraceQuestions,
69
- saveReport,
70
- scanSessions,
71
56
  selectAdapters,
72
- serializePolicyEvidence,
73
- serializeSessionIndex,
74
- serializeTraceStreamEvent,
75
- sessionReportSource,
76
- streamSessions,
77
- toolArgumentsFromAttributes,
78
- totalAgenticFailureMessage,
79
- traceStreamEventsFromSpans,
57
+ sessionsInCwd
58
+ } from "./chunk-3T62R7ON.js";
59
+ import {
60
+ indexSessionIdsByTrace
61
+ } from "./chunk-TR6O4S4T.js";
62
+ import {
63
+ appendAll,
64
+ iterSerializedSpans,
65
+ readOtlpInput,
66
+ tracesCommit,
80
67
  tracesVersion,
81
- validationExitCode,
82
- verdictExitCode,
83
- verifyFindingsInSandbox,
84
- verifySafe,
85
- verifySessionBundle,
86
- watchRunTarget,
87
- watchSessions,
88
- writeAnalysisTraceFile,
89
- writeInspectionReportFile,
90
- writeOtlpFile,
91
- writePolicyEvidenceFile,
92
- writeSessionIndexFile,
93
- writeTraceEvidenceExportFile,
94
- writeTraceQuestionsArtifacts
95
- } from "./chunk-7DVZCCZI.js";
68
+ writeOtlpFile
69
+ } from "./chunk-NZ5Y7UEL.js";
70
+ import "./chunk-RZL5GAWH.js";
71
+ import {
72
+ parseSince,
73
+ parseTimeBound,
74
+ relativeBoundMs
75
+ } from "./chunk-DU534HAX.js";
96
76
 
97
77
  // src/cli.ts
98
- import { readFileSync } from "fs";
99
- import { mkdir as mkdir2, mkdtemp, readFile as readFile2, stat, writeFile as writeFile2 } from "fs/promises";
100
- import { tmpdir } from "os";
101
- import { basename, join as join2, resolve } from "path";
102
- import {
103
- parseCorpusFlag,
104
- renderVerifiedFindingsSection
105
- } from "@tangle-network/agent-eval/trajectory-replay";
106
- import {
107
- analyzeSupervisorRun,
108
- findSupervisorRunDirs,
109
- isRuntimeSupervisorRunDir,
110
- renderSupervisorRollupMarkdown,
111
- renderSupervisorRunMarkdown,
112
- rollupSupervisorRuns
113
- } from "@tangle-network/agent-eval/supervisor-run";
78
+ import { stat, writeFile } from "fs/promises";
79
+ import { homedir, hostname } from "os";
80
+ import { basename as basename2, dirname as dirname2, resolve as resolve2 } from "path";
114
81
 
115
- // src/mcp.ts
116
- import { rm } from "fs/promises";
117
- import { dirname } from "path";
118
- import { buildTraceAnalysisToolDescriptors, redact, shareAllowed, UNTRUSTED_TRACE_TEXT } from "@tangle-network/agent-eval/traces";
119
- import { createStdioToolServer } from "@tangle-network/agent-runtime/mcp";
120
- var MCP_RESULT_BYTE_CAP = 512 * 1024;
121
- async function traceMcpTools(options) {
122
- const { spans } = redactSpans(options.spans);
123
- const verdict = assessSpans(spans);
124
- if (!shareAllowed(verdict)) {
125
- const findings = verdict.findings.map((f) => `${f.category}/${f.detector} (${f.count})`).join(", ");
126
- throw new Error(
127
- `mcp: refusing to serve \u2014 redacted spans are still ${verdict.status}` + (findings ? `: ${findings}` : "") + (verdict.unreadable.length > 0 ? `; unreadable: ${verdict.unreadable.join("; ")}` : "")
128
- );
129
- }
130
- const file = await writeAnalysisTraceFile(spans);
131
- const store = await openAgenticTraceStore(file);
132
- const descriptors = buildTraceAnalysisToolDescriptors({ store }).filter((d) => d.name !== "readSpanSource");
133
- const tools = descriptors.map((descriptor) => ({
134
- name: descriptor.name,
135
- description: descriptor.description,
136
- inputSchema: descriptor.parameters,
137
- annotations: {
138
- readOnlyHint: descriptor.readOnly,
139
- idempotentHint: descriptor.idempotent,
140
- destructiveHint: false,
141
- openWorldHint: false
142
- },
143
- handler: async (args) => {
144
- const { value } = redact(await descriptor.handler(args));
145
- const result = { untrusted: UNTRUSTED_TRACE_TEXT, result: value };
146
- const bytes = Buffer.byteLength(JSON.stringify(result));
147
- if (bytes > MCP_RESULT_BYTE_CAP) {
148
- throw new RangeError(
149
- `${descriptor.name}: result is ${bytes} bytes, above the ${MCP_RESULT_BYTE_CAP}-byte cap; narrow the request`
150
- );
151
- }
152
- return result;
153
- }
154
- }));
155
- return { tools, otlpPath: file.otlpPath };
156
- }
157
- async function serveTraceMcp(options) {
158
- const { tools, otlpPath } = await traceMcpTools(options);
159
- const server = createStdioToolServer({ serverName: "traces", serverVersion: tracesVersion(), tools });
160
- try {
161
- await server.serve();
162
- } finally {
163
- await rm(dirname(otlpPath), { recursive: true, force: true });
82
+ // src/analyst-log.ts
83
+ var ANALYST_FAILURE = /\[analyst\] (?:FAIL|WARN) |\bmodel call \d+ FAIL\b/u;
84
+ function analystLog(msg, fields) {
85
+ const rejection = findingRejectionDetail(msg, fields);
86
+ if (rejection !== void 0) {
87
+ process.stderr.write(rejection ? `${msg} \u2014 ${rejection}
88
+ ` : `${msg}
89
+ `);
90
+ return;
164
91
  }
92
+ if (!ANALYST_FAILURE.test(msg)) return;
93
+ const error = typeof fields?.error === "string" && fields.error ? fields.error : void 0;
94
+ const errorClass = typeof fields?.error_class === "string" && fields.error_class ? `${fields.error_class}: ` : "";
95
+ process.stderr.write(error ? `${msg} \u2014 ${errorClass}${error}
96
+ ` : `${msg}
97
+ `);
165
98
  }
166
99
 
167
- // src/cli.ts
168
- import { explainTraceContract as explainTraceContract2 } from "@tangle-network/agent-eval";
169
-
170
- // src/check.ts
171
- import { mkdir, readFile, writeFile } from "fs/promises";
172
- import { join } from "path";
173
- import {
174
- compileTraceContractSpec,
175
- evaluateTraceContract,
176
- explainTraceContract,
177
- lintTraceContractSpec
178
- } from "@tangle-network/agent-eval";
179
- var CHECK_EXIT = Object.freeze({
180
- pass: 0,
181
- fail: 1,
182
- /** The contract is malformed or contradicts itself, or a rule could not run. */
183
- badContract: 2,
184
- unreadable: 3,
185
- ambiguous: 4
186
- });
187
- var AmbiguousTraceInputError = class extends Error {
188
- name = "AmbiguousTraceInputError";
189
- };
190
- var ContractFileError = class extends Error {
191
- name = "ContractFileError";
192
- };
193
- async function loadContract(path) {
194
- let text;
195
- try {
196
- text = await readFile(path, "utf8");
197
- } catch (error) {
198
- throw new ContractFileError(`cannot read contract ${path}: ${errorText(error)}`);
199
- }
200
- let json;
201
- try {
202
- json = JSON.parse(text);
203
- } catch (error) {
204
- throw new ContractFileError(`contract ${path} is not JSON: ${errorText(error)}`);
100
+ // src/moved.ts
101
+ var LAST_0X_RELEASE = "@tangle-network/traces@0.23.1";
102
+ var FREE_TEXT_FLAGS = /* @__PURE__ */ new Set(["--question"]);
103
+ function flagIndexes(argv, flag) {
104
+ const at = [];
105
+ for (let i = 1; i < argv.length; i++) {
106
+ if (FREE_TEXT_FLAGS.has(argv[i])) {
107
+ i++;
108
+ continue;
109
+ }
110
+ if (argv[i] === flag) at.push(i);
111
+ }
112
+ return at;
113
+ }
114
+ function dropFlag(argv, flag, takesValue) {
115
+ const drop = /* @__PURE__ */ new Set();
116
+ for (const i of flagIndexes(argv, flag)) {
117
+ drop.add(i);
118
+ if (takesValue) drop.add(i + 1);
119
+ }
120
+ return argv.filter((_, i) => !drop.has(i));
121
+ }
122
+ function renameFlag(argv, from, to) {
123
+ if (flagIndexes(argv, to).length > 0) return void 0;
124
+ const at = new Set(flagIndexes(argv, from));
125
+ return argv.map((word, i) => at.has(i) ? to : word);
126
+ }
127
+ function flagToPositional(argv, flag) {
128
+ const values = flagIndexes(argv, flag).flatMap((i) => argv[i + 1] === void 0 ? [] : [argv[i + 1]]);
129
+ const rest = dropFlag(argv, flag, true);
130
+ let at = 1;
131
+ while (at < rest.length && !rest[at].startsWith("-")) at++;
132
+ return [...rest.slice(0, at), ...values, ...rest.slice(at)];
133
+ }
134
+ function commandOf(argv) {
135
+ return argv[0] === "help" ? argv[1] : argv[0];
136
+ }
137
+ function renameCommand(argv, to) {
138
+ return argv[0] === "help" ? ["help", to, ...argv.slice(2)] : [to, ...argv.slice(1)];
139
+ }
140
+ var INPUT_FORMATS = ["auto", "otlp", "sandbox-events", "openinference", "claude-code", "codex"];
141
+ var HARNESS_FORMATS = /* @__PURE__ */ new Set(["claude-code", "codex"]);
142
+ function withoutInputFormat(argv) {
143
+ let harness;
144
+ const drop = /* @__PURE__ */ new Set();
145
+ for (const i of flagIndexes(argv, "--format")) {
146
+ const kind = argv[i + 1];
147
+ if (kind === void 0 || !INPUT_FORMATS.includes(kind)) continue;
148
+ drop.add(i);
149
+ drop.add(i + 1);
150
+ if (HARNESS_FORMATS.has(kind)) harness = kind;
151
+ }
152
+ const out = argv.filter((_, i) => !drop.has(i));
153
+ if (harness !== void 0 && flagIndexes(out, "--harness").length === 0) out.push("--harness", harness);
154
+ return out;
155
+ }
156
+ var OTLP_OUT_COMMANDS = /* @__PURE__ */ new Set(["convert", "export", "upload"]);
157
+ var MOVED = [
158
+ {
159
+ was: ["import-codetracebench", "replay-verify", "replay-verify-batch", "verify-findings"],
160
+ now: `removed. The last version with them: npx --yes ${LAST_0X_RELEASE} <command>`,
161
+ pinned: true
162
+ },
163
+ {
164
+ was: ["--analyzer", "--analyzer-prompt", "--verify-findings", "--replay-corpus", "--verify-out", "--trajectory-dir", "--revision"],
165
+ now: "removed",
166
+ pinned: true
167
+ },
168
+ {
169
+ was: ["--redactor"],
170
+ now: "traces convert X --out f, your scrubber on f, then traces upload f"
171
+ },
172
+ {
173
+ was: ["--format chat-trajectory"],
174
+ now: "removed (traces@0.23.1)",
175
+ pinned: true
176
+ },
177
+ { was: ["index"], now: "traces list --format json" },
178
+ { was: ["inspect"], now: "traces analyze <ref>" },
179
+ { was: ["evidence"], now: "traces facts --format json" },
180
+ { was: ["bundle-view"], now: "traces facts <bundle-dir> --no-content" },
181
+ {
182
+ was: ["--source-bundle"],
183
+ now: "a positional ref: traces analyze <bundle-dir>",
184
+ rewrite: (argv) => flagToPositional(argv, "--source-bundle")
185
+ },
186
+ { was: ["--view"], now: "removed (bundle-view is traces facts <bundle-dir> --no-content)" },
187
+ {
188
+ was: ["--format policy-evidence", "--format intelligence-spans"],
189
+ now: "removed (traces@0.23.1)",
190
+ pinned: true
191
+ },
192
+ {
193
+ was: ["--session", "--otlp", "--supervisor-run-dir"],
194
+ now: "a positional ref: traces analyze X",
195
+ rewrite: (argv) => ["--session", "--otlp", "--supervisor-run-dir"].reduce(flagToPositional, [...argv])
196
+ },
197
+ {
198
+ was: ["--all"],
199
+ now: "the default: every harness is read, and --harness ID narrows",
200
+ rewrite: (argv) => dropFlag(argv, "--all", false)
201
+ },
202
+ {
203
+ was: ["--workflow", "--max-workflow-sessions"],
204
+ now: "the default: a named session brings its spawned children, up to 100",
205
+ rewrite: (argv) => dropFlag(dropFlag(argv, "--workflow", false), "--max-workflow-sessions", true)
206
+ },
207
+ {
208
+ was: INPUT_FORMATS.map((kind) => `--format ${kind}`),
209
+ now: "removed: the input is sniffed, and --harness ID forces a reader for a file ref",
210
+ rewrite: withoutInputFormat
211
+ },
212
+ { was: [], label: "--harness default claude-code", now: "every harness by default; --harness ID narrows" },
213
+ { was: ["investigate"], now: "traces analyze \u2026", rewrite: (argv) => renameCommand(argv, "analyze") },
214
+ { was: ["export"], now: "traces convert <file> (--out FILE, or stdout)", rewrite: (argv) => renameCommand(argv, "convert") },
215
+ {
216
+ was: ["--otlp-out"],
217
+ now: "--out F on convert and upload --dry-run; analyze and ask keep no span copy, so write one with traces convert <ref> --out F",
218
+ rewrite: (argv) => {
219
+ const command = commandOf(argv);
220
+ return command !== void 0 && OTLP_OUT_COMMANDS.has(command) ? renameFlag(argv, "--otlp-out", "--out") : void 0;
221
+ }
222
+ },
223
+ { was: ["--metadata", "--attr"], now: "removed", pinned: true },
224
+ {
225
+ was: ["--min-loop"],
226
+ now: "removed: a loop is 3 identical calls",
227
+ rewrite: (argv) => dropFlag(argv, "--min-loop", true)
228
+ },
229
+ { was: [], label: "upload with no selection", now: "traces upload --since 24h (upload needs a ref, --current or --since)" },
230
+ {
231
+ was: ["--evidence"],
232
+ now: "check \u2026 --out DIR: evidence for each trace that did not pass; without --out, check writes nothing",
233
+ rewrite: (argv) => renameFlag(argv, "--evidence", "--out")
234
+ },
235
+ { was: [], label: "facts JSON by default", now: "text by default, 25 lines a session; pass --format json to parse the sheet" },
236
+ {
237
+ was: ["--no-refresh", "--store", "--sweep"],
238
+ now: "removed: skills always reads what sessions appended since its last run, keeps its store under $XDG_STATE_HOME/traces, and counts a command that reads 3 or more skills as a catalog sweep",
239
+ rewrite: (argv) => dropFlag(dropFlag(dropFlag(argv, "--no-refresh", false), "--store", true), "--sweep", true)
240
+ },
241
+ {
242
+ was: ["improve"],
243
+ now: "traces analyze \u2026 --out DIR (report.md and findings.json; no span copy)",
244
+ rewrite: (argv) => {
245
+ const renamed = renameCommand(argv, "analyze");
246
+ return renameFlag(renamed, "--dir", "--out") ?? renamed;
247
+ }
248
+ },
249
+ {
250
+ was: ["--llm", "--model", "--budget"],
251
+ commands: ["analyze", "investigate", "improve"],
252
+ label: "analyze --llm, --model, --budget",
253
+ now: 'analyze calls no model: traces ask <ref> --question "\u2026" --budget USD [--model ID]'
254
+ },
255
+ {
256
+ was: ["--config"],
257
+ now: "removed: run your own analysts through the SDK, analyzeSpans(spans, { registry })"
258
+ },
259
+ {
260
+ was: ["stream"],
261
+ now: "traces watch --format jsonl: the same traces.stream envelope, as session, finding and tick events",
262
+ rewrite: (argv) => {
263
+ const renamed = renameCommand(argv, "watch");
264
+ return renamed[0] === "help" || flagIndexes(renamed, "--format").length > 0 ? renamed : [...renamed, "--format", "jsonl"];
265
+ }
266
+ },
267
+ {
268
+ was: ["--mode", "--no-spans", "--no-findings"],
269
+ commands: ["stream", "watch"],
270
+ now: "removed: traces watch --format jsonl prints session, finding and tick events, with no span or batch events",
271
+ rewrite: (argv) => dropFlag(dropFlag(dropFlag(argv, "--mode", true), "--no-spans", false), "--no-findings", false)
272
+ },
273
+ {
274
+ was: ["--replay"],
275
+ commands: ["stream", "watch"],
276
+ now: "--once: one pass, then exit",
277
+ rewrite: (argv) => flagIndexes(argv, "--once").length > 0 ? dropFlag(argv, "--replay", false) : renameFlag(argv, "--replay", "--once")
278
+ },
279
+ {
280
+ was: ["--window", "--interval"],
281
+ commands: ["stream", "watch"],
282
+ now: "--since Nm (default 30m; the window slides with time). Sessions are read every 5 s, a run tree every 2 s",
283
+ rewrite: (argv) => windowToSince(dropFlag(argv, "--interval", true))
284
+ },
285
+ { was: [], label: "SDK watchSessions", now: "streamSessions({ onEvent }): the session, finding and tick events traces watch prints" },
286
+ {
287
+ was: ["validate"],
288
+ now: "traces check <f> --contract conformance (exit 1 only when the file is not a trace; the report is stdout)",
289
+ rewrite: (argv) => {
290
+ if (flagIndexes(argv, "--out").length > 0) return void 0;
291
+ const renamed = renameCommand(argv, "check");
292
+ return renamed[0] === "help" ? renamed : [...renamed, "--contract", "conformance"];
293
+ }
294
+ },
295
+ {
296
+ was: ["verify-safe"],
297
+ now: "traces check <path> --share-safe (--known-secret-env is unchanged)",
298
+ rewrite: (argv) => {
299
+ const renamed = renameCommand(argv, "check");
300
+ return renamed[0] === "help" ? renamed : [...renamed, "--share-safe"];
301
+ }
302
+ },
303
+ {
304
+ was: ["--profile"],
305
+ commands: ["verify-safe", "check"],
306
+ now: "removed: a share verdict always uses the share profile",
307
+ rewrite: (argv) => dropFlag(argv, "--profile", true)
308
+ },
309
+ { was: ["--junit"], commands: ["check"], now: "check \u2026 --format junit > FILE: the JUnit XML is the verdict on stdout" },
310
+ {
311
+ was: ["--annotations"],
312
+ commands: ["check"],
313
+ now: "check \u2026 --format github: the annotations are the verdict on stdout",
314
+ rewrite: (argv) => {
315
+ if (flagIndexes(argv, "--format").length > 0) return void 0;
316
+ const at = new Set(flagIndexes(argv, "--annotations"));
317
+ return argv.flatMap((word, i) => at.has(i) ? ["--format", "github"] : [word]);
318
+ }
319
+ },
320
+ {
321
+ was: ["--explain"],
322
+ commands: ["check"],
323
+ now: "traces check --contract FILE with no ref explains FILE",
324
+ rewrite: (argv) => {
325
+ const rest = dropFlag(argv, "--explain", false);
326
+ let at = 1;
327
+ while (at < rest.length && !rest[at].startsWith("-")) at++;
328
+ return [rest[0], ...rest.slice(at)];
329
+ }
330
+ },
331
+ { was: [], label: "check exit 3 (unreadable) and 4 (ambiguous)", now: "exit 2, with code unreadable or ambiguous in the JSON error on stderr" },
332
+ {
333
+ was: ["--kind"],
334
+ commands: ["diff"],
335
+ label: "diff --kind",
336
+ now: "removed: diff compares tool calls only, by tool, status and argument; model calls and events are never steps",
337
+ rewrite: (argv) => dropFlag(argv, "--kind", true)
338
+ },
339
+ { was: [], label: "diff exit 2 (unreadable) and 3 (no outcome)", now: "exit 2, with code unreadable or no-outcome in the JSON error on stderr" },
340
+ {
341
+ was: ["--dir"],
342
+ commands: ["ask"],
343
+ label: "ask --dir",
344
+ now: "--out DIR: answers.json and report.md",
345
+ rewrite: (argv) => renameFlag(argv, "--dir", "--out")
346
+ },
347
+ {
348
+ was: ["--question-budget", "--question-timeout", "--concurrency"],
349
+ commands: ["ask"],
350
+ label: "ask --question-budget, --question-timeout, --concurrency",
351
+ now: `removed: each question spends at most min(--budget, $${DEFAULT_QUESTION_MAX_COST_USD}) within ${DEFAULT_QUESTION_TIMEOUT_MS / 1e3} s, ${DEFAULT_ASK_CONCURRENCY} at once`,
352
+ rewrite: (argv) => dropFlag(dropFlag(dropFlag(argv, "--question-budget", true), "--question-timeout", true), "--concurrency", true)
353
+ },
354
+ { was: [], label: "ask without --budget", now: "add --budget USD: the most the run may spend" },
355
+ {
356
+ was: [],
357
+ label: "ask exit 1 when a question failed",
358
+ now: "exit 2, with code partial; answers.json under --out still holds every answer. A refused key or model: code auth or model"
359
+ }
360
+ ];
361
+ function windowToSince(argv) {
362
+ const at = flagIndexes(argv, "--window")[0];
363
+ if (at === void 0) return [...argv];
364
+ const minutes = argv[at + 1];
365
+ if (minutes === void 0 || !/^\d+$/u.test(minutes)) return void 0;
366
+ const rest = dropFlag(argv, "--window", true);
367
+ return flagIndexes(rest, "--since").length > 0 ? rest : [...rest, "--since", `${minutes}m`];
368
+ }
369
+ function findMoved(argv) {
370
+ return findAllMoved(argv)[0];
371
+ }
372
+ function findAllMoved(argv) {
373
+ const found = [];
374
+ for (const row of MOVED) {
375
+ const command = commandOf(argv);
376
+ if (row.commands && (command === void 0 || !row.commands.includes(command))) continue;
377
+ for (const spelling of row.was) {
378
+ if (uses(argv, spelling)) found.push({ row, spelling });
379
+ }
205
380
  }
206
- try {
207
- const findings = lintTraceContractSpec(json);
208
- const errors = findings.filter((f) => f.level === "error");
209
- if (errors.length > 0) {
210
- throw new ContractFileError(
211
- `contract ${path} contradicts itself:
212
- ${errors.map((f) => ` ${f.path}: ${f.message} [${f.code}]`).join("\n")}`
213
- );
381
+ return found;
382
+ }
383
+ function uses(argv, spelling) {
384
+ if (!spelling.startsWith("--")) {
385
+ return argv[0] === spelling || argv[0] === "help" && argv[1] === spelling;
386
+ }
387
+ const [flag, value] = spelling.split(" ");
388
+ return flagIndexes(argv, flag).some((i) => value === void 0 || argv[i + 1] === value);
389
+ }
390
+ function pinnedCommand(argv) {
391
+ return ["npx", "--yes", LAST_0X_RELEASE, ...argv].map(shellWord).join(" ");
392
+ }
393
+ function tracesCommand(argv) {
394
+ return ["traces", ...argv].map(shellWord).join(" ");
395
+ }
396
+ function movedMessage(found, argv) {
397
+ const all = findAllMoved(argv);
398
+ const pinned = all.find(({ row }) => row.pinned);
399
+ if (pinned) return `${pinned.spelling} was removed in traces 1.0. The last version with it runs as: ${pinnedCommand(argv)}`;
400
+ let rewritten = argv;
401
+ let mechanical = false;
402
+ const unwritten = [];
403
+ for (const moved of all) {
404
+ const next = moved.row.rewrite?.(rewritten);
405
+ if (next) {
406
+ rewritten = next;
407
+ mechanical = true;
408
+ } else {
409
+ unwritten.push(moved);
214
410
  }
215
- return {
216
- contract: compileTraceContractSpec(json),
217
- warnings: findings.map((f) => `${f.path}: ${f.message} [${f.code}]`)
218
- };
219
- } catch (error) {
220
- if (error instanceof ContractFileError) throw error;
221
- throw new ContractFileError(`contract ${path}: ${errorText(error)}`);
222
411
  }
412
+ const instead = mechanical ? tracesCommand(rewritten) : found.row.now;
413
+ const also = unwritten.filter(({ spelling }) => mechanical || spelling !== found.spelling).map(({ row, spelling }) => `${spelling}: ${row.now}`);
414
+ return `${found.spelling} was removed in traces 1.0. Instead: ${instead}${also.length > 0 ? ` (${also.join("; ")})` : ""}`;
223
415
  }
224
- var SNIFF_ROWS = 20;
225
- async function sniffCheckInput(path) {
226
- const rows = [];
227
- for await (const row of readJsonl(path)) {
228
- rows.push(row);
229
- if (rows.length >= SNIFF_ROWS) break;
230
- }
231
- const objects = rows.filter(isObject);
232
- if (objects.length === 0) throw new Error(`${path} holds no JSON rows`);
233
- const kinds = [];
234
- if (objects.every((r) => typeof r.trace_id === "string" && typeof r.span_id === "string")) kinds.push("otlp");
235
- if (objects.some((r) => (typeof r.sessionId === "string" || typeof r.session_id === "string") && typeof r.type === "string") && !objects.some(hasSpanId)) {
236
- kinds.push("claude-code");
237
- }
238
- if (objects.some((r) => CODEX_LINE_TYPES.has(String(r.type)) && isObject(r.payload))) kinds.push("codex");
239
- if (objects.every(isEvidenceRow)) kinds.push("evidence");
240
- if (kinds.length === 1) return kinds[0];
241
- if (kinds.length === 0) {
242
- throw new Error(`${path} is not a trace this command reads (OTLP spans, a Claude Code or Codex session, or trace evidence)`);
243
- }
244
- throw new AmbiguousTraceInputError(
245
- `${path} reads as ${kinds.join(" and ")}; pass --format ${kinds.join(" or --format ")}`
246
- );
416
+ function renderMovedTable() {
417
+ const lines = ["Moved in traces 1.0. Each old spelling exits 2 and prints its row.", ""];
418
+ for (const row of MOVED) {
419
+ lines.push(` ${row.label ?? row.was.join(", ")}`);
420
+ lines.push(` ${row.now}`);
421
+ }
422
+ return `${lines.join("\n")}
423
+ `;
247
424
  }
248
- var CODEX_LINE_TYPES = /* @__PURE__ */ new Set(["session_meta", "response_item", "event_msg", "turn_context"]);
249
- function hasSpanId(row) {
250
- return typeof row.span_id === "string";
251
- }
252
- function isEvidenceRow(row) {
253
- if (hasSpanId(row)) return false;
254
- if (row.kind === "traces.policy_evidence.session") return true;
255
- if (Array.isArray(row.messages) || typeof row.role === "string" && "content" in row) return true;
256
- return typeof row.trace_id === "string" && typeof row.name === "string" && isObject(row.attributes);
257
- }
258
- function contractSpansFromOtlp(spans) {
259
- return spans.map((span) => {
260
- const startedAt = epochMs(span.start_time);
261
- const endedAt = epochMs(span.end_time);
262
- const args = toolArgumentsFromAttributes(span.attributes);
263
- const truncated = span.attributes["traces.input.truncated"] === true;
264
- return {
265
- spanId: span.span_id,
266
- parentSpanId: span.parent_span_id,
267
- name: span.name,
268
- ...startedAt === void 0 ? {} : { startedAt },
269
- ...endedAt === void 0 ? {} : { endedAt },
270
- ...span.status.code === "ERROR" ? { status: "error" } : span.status.code === "OK" ? { status: "ok" } : {},
271
- // A truncated argument capture is not the call's arguments.
272
- ...args.argsCaptured && !truncated ? { args: args.args } : { argsCaptured: false },
273
- attributes: span.attributes
274
- };
275
- });
425
+ function shellWord(word) {
426
+ return /^[\w@%+=:,./-]+$/u.test(word) ? word : `'${word.replace(/'/gu, `'\\''`)}'`;
276
427
  }
277
- function epochMs(value) {
278
- try {
279
- return parseIsoToEpochMs(value);
280
- } catch {
281
- return void 0;
428
+
429
+ // src/record-lines.ts
430
+ import { createReadStream } from "fs";
431
+ import { readdir } from "fs/promises";
432
+ import { basename, dirname, join } from "path";
433
+ import { createInterface } from "readline";
434
+ var CALL_ID = /"(?:call_id|tool_use_id)":"([^"]+)"|"id":"(toolu_[^"]+)"|"gen_ai\.tool\.call\.id":\s*"([^"]+)"|"key":\s*"gen_ai\.tool\.call\.id",\s*"value":\s*\{\s*"stringValue":\s*"([^"]+)"/gu;
435
+ var OUTPUT_RECORD = /"type":"(?:[a-z_]+_output|tool_result)"/u;
436
+ var TIMESTAMP = /"timestamp":"([^"]+)"/u;
437
+ var USER_ROLE = /"role":"user"/u;
438
+ var flat = (text) => text.replace(/\s+/gu, " ").trim();
439
+ var citedText = (text) => flat(text.replace(/… \[\+\d+ chars\]$/u, ""));
440
+ function strings(value, into = []) {
441
+ if (typeof value === "string") into.push(value);
442
+ else if (Array.isArray(value)) for (const item of value) strings(item, into);
443
+ else if (value && typeof value === "object") for (const item of Object.values(value)) strings(item, into);
444
+ return into;
445
+ }
446
+ function resultText(record, id) {
447
+ if (!record || typeof record !== "object") return null;
448
+ const payload = record.payload;
449
+ if (payload && typeof payload.output === "string") return payload.output;
450
+ const content = record.message?.content;
451
+ if (!Array.isArray(content)) return null;
452
+ const block = content.find((item) => item && typeof item === "object" && item.tool_use_id === id);
453
+ if (block === void 0) return null;
454
+ return typeof block.content === "string" ? block.content : strings(block.content).join("\n");
455
+ }
456
+ async function sessionRecordFiles(path) {
457
+ const files = [path];
458
+ if (!path.endsWith(".jsonl")) return files;
459
+ const subagents = join(dirname(path), basename(path, ".jsonl"), "subagents");
460
+ const names = await readdir(subagents).catch(() => []);
461
+ for (const name of names.filter((entry) => entry.endsWith(".jsonl")).sort()) files.push(join(subagents, name));
462
+ return files;
463
+ }
464
+ async function readRecordLines(files, query) {
465
+ const calls = /* @__PURE__ */ new Map();
466
+ const outputs = /* @__PURE__ */ new Map();
467
+ const texts = /* @__PURE__ */ new Map();
468
+ const wantedTurns = /* @__PURE__ */ new Map();
469
+ for (const turn of query.turns ?? []) {
470
+ const ms = Date.parse(turn.at);
471
+ if (Number.isFinite(ms)) wantedTurns.set(ms, [...wantedTurns.get(ms) ?? [], { key: flat(turn.text), text: citedText(turn.text) }]);
472
+ }
473
+ const turns = /* @__PURE__ */ new Map();
474
+ const scan = query.callIds.size > 0 || wantedTurns.size > 0 ? files : [];
475
+ for (const file of scan) {
476
+ let line = 0;
477
+ try {
478
+ const lines = createInterface({ input: createReadStream(file, { encoding: "utf8" }), crlfDelay: Infinity });
479
+ for await (const raw of lines) {
480
+ line += 1;
481
+ const here = { file, line };
482
+ let parsed;
483
+ const parse = () => {
484
+ if (parsed === void 0) {
485
+ try {
486
+ parsed = JSON.parse(raw);
487
+ } catch {
488
+ parsed = null;
489
+ }
490
+ }
491
+ return parsed;
492
+ };
493
+ const isOutput = query.callIds.size > 0 && OUTPUT_RECORD.test(raw);
494
+ for (const match of query.callIds.size > 0 ? raw.matchAll(CALL_ID) : []) {
495
+ const id = match[1] ?? match[2] ?? match[3] ?? match[4];
496
+ if (!query.callIds.has(id)) continue;
497
+ if (!isOutput) {
498
+ if (!calls.has(id)) calls.set(id, here);
499
+ } else if (!outputs.has(id)) {
500
+ outputs.set(id, here);
501
+ if (query.outputTextFor?.has(id)) {
502
+ const text = resultText(parse(), id);
503
+ if (text !== null) texts.set(id, text);
504
+ }
505
+ }
506
+ }
507
+ if (wantedTurns.size === 0 || !USER_ROLE.test(raw)) continue;
508
+ const stamp = TIMESTAMP.exec(raw)?.[1];
509
+ const wanted = stamp === void 0 ? void 0 : wantedTurns.get(Date.parse(stamp));
510
+ if (wanted === void 0) continue;
511
+ const record = parse();
512
+ if (record === null) continue;
513
+ const said = strings(record).map(flat);
514
+ const item = record.type === "response_item";
515
+ for (const { key: body, text } of wanted) {
516
+ const key = `${Date.parse(stamp)}\0${body}`;
517
+ const prior = turns.get(key);
518
+ if ((prior === void 0 || item && !prior.item) && said.some((value) => value.includes(text))) turns.set(key, { at: here, item });
519
+ }
520
+ }
521
+ } catch {
522
+ }
282
523
  }
524
+ return {
525
+ call: (id) => calls.get(id) ?? null,
526
+ output: (id) => outputs.get(id) ?? null,
527
+ outputText: (id) => texts.get(id) ?? null,
528
+ turn: (at, text) => turns.get(`${Date.parse(at)}\0${flat(text)}`)?.at ?? null
529
+ };
283
530
  }
284
- function groupByTrace(spans) {
285
- const groups = /* @__PURE__ */ new Map();
531
+ function callIdsOf(spans) {
532
+ const ids = /* @__PURE__ */ new Map();
286
533
  for (const span of spans) {
287
- const list = groups.get(span.trace_id) ?? [];
288
- list.push(span);
289
- groups.set(span.trace_id, list);
290
- }
291
- return [...groups].map(([traceId, list]) => ({ traceId, spans: list }));
292
- }
293
- async function runCheck(contract, spans, opts) {
294
- const traces = [];
295
- for (const group of groupByTrace(spans)) {
296
- const verdict = evaluateTraceContract(contract, contractSpansFromOtlp(group.spans));
297
- const check = { traceId: group.traceId, spanCount: group.spans.length, verdict };
298
- if (verdict.status !== "pass") {
299
- check.evidenceDir = await writeEvidence(opts.evidenceRoot, contract, group.traceId, group.spans, verdict);
534
+ const shared = span.attributes["gen_ai.tool.call.id"];
535
+ const native = span.attributes["traces.codex.source_span_id"];
536
+ const id = typeof shared === "string" && shared !== "" ? shared : typeof native === "string" && native.startsWith("tool:") ? native.slice("tool:".length) : void 0;
537
+ if (id !== void 0 && id !== "") ids.set(span.span_id, id);
538
+ }
539
+ return ids;
540
+ }
541
+ function lastLine(text) {
542
+ const lines = text.split("\n").map((line) => line.trimEnd()).filter((line) => line.trim() !== "");
543
+ return lines.at(-1) ?? null;
544
+ }
545
+ async function citeFacts(sessions, matches, citings) {
546
+ const citedSessions = [];
547
+ const citedMatches = /* @__PURE__ */ new Map();
548
+ for (const facts of sessions) {
549
+ const key = facts.sessionId ?? facts.traceId;
550
+ const citing = citings.get(key);
551
+ if (citing === void 0) {
552
+ citedSessions.push(facts);
553
+ continue;
300
554
  }
301
- traces.push(check);
302
- }
303
- const status = traces.some((t) => t.verdict.status === "error") ? "error" : traces.every((t) => t.verdict.status === "pass") ? "pass" : "fail";
304
- const exitCode = status === "pass" ? CHECK_EXIT.pass : status === "fail" ? CHECK_EXIT.fail : CHECK_EXIT.badContract;
305
- return { contract: contract.name, status, exitCode, traces };
306
- }
307
- async function writeEvidence(root, contract, traceId, spans, verdict) {
308
- const dir = join(root, `${safeName(contract.name)}-${safeName(traceId).slice(0, 40)}`);
309
- await mkdir(dir, { recursive: true });
310
- const cited = new Set(verdict.violations.map((v) => v.spanId).filter((id) => id !== void 0));
311
- const redacted = redactSpans(spans.filter((s) => cited.has(s.span_id)));
312
- await writeFile(join(dir, "verdict.json"), `${JSON.stringify({ traceId, verdict }, null, 2)}
313
- `);
314
- await writeFile(join(dir, "contract.json"), `${JSON.stringify(contract, null, 2)}
315
- `);
316
- await writeFile(join(dir, "explain.txt"), `${explainTraceContract(contract)}
317
- `);
318
- await writeFile(join(dir, "cited-spans.jsonl"), redacted.spans.map((s) => JSON.stringify(s)).join("\n") + (redacted.spans.length ? "\n" : ""));
319
- return dir;
320
- }
321
- function safeName(value) {
322
- return value.replace(/[^A-Za-z0-9._-]+/g, "_");
323
- }
324
- function ruleLines(verdict) {
325
- const branchPassed = /* @__PURE__ */ new Map();
326
- for (const e of verdict.ruleExecutions) {
327
- if (e.alternative === void 0) continue;
328
- branchPassed.set(e.alternative, (branchPassed.get(e.alternative) ?? true) && e.status === "pass");
329
- }
330
- const anyBranchPassed = [...branchPassed.values()].some(Boolean);
331
- return verdict.ruleExecutions.map((e) => {
332
- const rule = e.alternative ? `${e.alternative}/${e.rule}` : e.rule;
333
- const status = e.alternative && anyBranchPassed && !branchPassed.get(e.alternative) ? "skipped" : e.status;
334
- return {
335
- rule,
336
- status,
337
- ...e.error === void 0 ? {} : { error: e.error },
338
- details: verdict.violations.filter((x) => x.rule === rule).map((x) => x.detail)
555
+ const own = matches.filter((match) => match.kind === "command" && match.sessionId === facts.sessionId);
556
+ const wanted = (spanIds) => [...spanIds].flatMap((spanId) => {
557
+ const id = spanId === void 0 ? void 0 : citing.callIds.get(spanId);
558
+ return id === void 0 ? [] : [id];
559
+ });
560
+ const failure = { spanId: "spanId" in facts.firstFailure && typeof facts.firstFailure.spanId === "string" ? facts.firstFailure.spanId : void 0 };
561
+ const commands = facts.commands.value ?? [];
562
+ const files = facts.changedFiles.value ?? [];
563
+ const outputTextFor = new Set(wanted(own.map((match) => match.spanId)));
564
+ const firstCall = (file) => file.spanIds.find((spanId) => citing.callIds.has(spanId));
565
+ const lines = await readRecordLines(await sessionRecordFiles(citing.path), {
566
+ callIds: /* @__PURE__ */ new Set([
567
+ ...wanted([failure.spanId]),
568
+ ...wanted(commands.flatMap((row) => [row.lastSpanId, ...row.nonZeroExits.map((exit) => exit.spanId)])),
569
+ ...wanted(files.map(firstCall)),
570
+ ...outputTextFor
571
+ ]),
572
+ outputTextFor,
573
+ turns: facts.humanTurns.value ?? []
574
+ });
575
+ const result = (spanId) => {
576
+ const id = spanId === void 0 ? void 0 : citing.callIds.get(spanId);
577
+ return id === void 0 ? null : lines.output(id) ?? lines.call(id);
339
578
  };
340
- });
341
- }
342
- function renderCheckText(report) {
343
- const lines = [];
344
- for (const t of report.traces) {
345
- const v = t.verdict;
346
- lines.push(`${v.status.toUpperCase()} contract ${report.contract} trace ${t.traceId} (${t.spanCount} spans, ${v.notes})`);
347
- for (const r of ruleLines(v)) {
348
- lines.push(` ${r.status.padEnd(7)} ${r.rule}${r.error ? ` ${r.error}` : ""}`);
349
- if (r.status === "fail") for (const detail of r.details.slice(0, 5)) lines.push(` ${detail}`);
579
+ const call = (spanId) => {
580
+ const id = spanId === void 0 ? void 0 : citing.callIds.get(spanId);
581
+ return id === void 0 ? null : lines.call(id);
582
+ };
583
+ const short = (record) => record === null ? null : record.file === citing.path ? { line: record.line } : record;
584
+ for (const match of own) {
585
+ const id = citing.callIds.get(match.spanId);
586
+ const text = id === void 0 || match.failed == null ? null : lines.outputText(id);
587
+ citedMatches.set(match, { ...match, record: result(match.spanId), lastLine: text === null ? null : lastLine(text) });
350
588
  }
351
- if (t.evidenceDir) lines.push(` evidence \u2192 ${t.evidenceDir}`);
352
- }
353
- return lines.join("\n");
354
- }
355
- function renderJUnit(report) {
356
- const suites = report.traces.map((t) => {
357
- const rules = ruleLines(t.verdict);
358
- const count = (status) => rules.filter((r) => r.status === status).length;
359
- const cases = rules.map((r) => {
360
- const open = ` <testcase classname="${xml(report.contract)}" name="${xml(r.rule)}">`;
361
- if (r.status === "pass") return `${open}</testcase>`;
362
- if (r.status === "skipped") return `${open}
363
- <skipped message="another alternative passed"/>
364
- </testcase>`;
365
- if (r.status === "error") {
366
- return `${open}
367
- <error message="${xml(r.error ?? "rule could not be evaluated")}"/>
368
- </testcase>`;
369
- }
370
- return `${open}
371
- <failure message="${xml(r.details[0] ?? "rule failed")}">${xml(r.details.join("\n"))}</failure>
372
- </testcase>`;
589
+ citedSessions.push({
590
+ ...facts,
591
+ recordFile: citing.path,
592
+ humanTurns: facts.humanTurns.value === null ? facts.humanTurns : { ...facts.humanTurns, value: facts.humanTurns.value.map((turn) => ({ ...turn, record: short(lines.turn(turn.at, turn.text)) })) },
593
+ changedFiles: facts.changedFiles.value === null ? facts.changedFiles : { ...facts.changedFiles, value: files.map((file) => ({ ...file, record: short(call(firstCall(file))) })) },
594
+ commands: facts.commands.value === null ? facts.commands : {
595
+ ...facts.commands,
596
+ // `lastSpanId` only finds the latest run's record; the row carries the record instead.
597
+ value: commands.map(({ lastSpanId, ...row }) => ({
598
+ ...row,
599
+ record: short(result(lastSpanId)),
600
+ nonZeroExits: row.nonZeroExits.map((exit) => ({ ...exit, record: short(result(exit.spanId)) }))
601
+ }))
602
+ },
603
+ firstFailure: failure.spanId === void 0 ? facts.firstFailure : { ...facts.firstFailure, record: short(result(failure.spanId)) }
373
604
  });
374
- return [
375
- ` <testsuite name="${xml(`${report.contract} @ ${t.traceId}`)}" tests="${rules.length}" failures="${count("fail")}" errors="${count("error")}" skipped="${count("skipped")}">`,
376
- ...cases,
377
- " </testsuite>"
378
- ].join("\n");
379
- });
380
- return `<?xml version="1.0" encoding="UTF-8"?>
381
- <testsuites name="traces check">
382
- ${suites.join("\n")}
383
- </testsuites>
384
- `;
605
+ }
606
+ return { sessions: citedSessions, matches: matches.map((match) => citedMatches.get(match) ?? match) };
385
607
  }
386
- function renderGithubAnnotations(report) {
387
- const lines = [];
388
- for (const t of report.traces) {
389
- for (const r of ruleLines(t.verdict)) {
390
- if (r.status === "pass" || r.status === "skipped") continue;
391
- const detail = r.status === "error" ? `could not be evaluated: ${r.error ?? "unknown"}` : r.details.join("\n");
392
- lines.push(`::error title=${ghProperty(`${report.contract}: ${r.rule}`)}::${ghData(`trace ${t.traceId}: ${detail}`)}`);
608
+
609
+ // src/facts-jsonl.ts
610
+ async function turnFactsJsonl(sources, host, window) {
611
+ const rows = [];
612
+ let sessions = 0;
613
+ const unread = [];
614
+ for await (const { spans, source } of sources) {
615
+ for (const group of readSessionTurnGroups(spans, window)) {
616
+ if (group.records === 0) {
617
+ unread.push({ sessionId: group.sessionId, traceId: null, source, reason: "no record could be read" });
618
+ continue;
619
+ }
620
+ sessions += 1;
621
+ const lines = source === null ? null : await readRecordLines([source], { callIds: /* @__PURE__ */ new Set(), turns: group.turns });
622
+ for (const turn of group.turns) {
623
+ const record = lines?.turn(turn.at, turn.text) ?? null;
624
+ rows.push(JSON.stringify({
625
+ host,
626
+ harness: group.harness,
627
+ session: group.sessionId,
628
+ cwd: group.cwd,
629
+ at: turn.at,
630
+ author: turn.author,
631
+ text: turn.text,
632
+ answered: turn.answered,
633
+ reply: turn.reply,
634
+ interrupted: turn.interrupted,
635
+ signals: turn.signals,
636
+ spanId: turn.spanId,
637
+ recordFile: source,
638
+ record: record === null ? null : { file: record.file, line: record.line }
639
+ }));
640
+ }
393
641
  }
394
642
  }
395
- return lines.join("\n");
396
- }
397
- function xml(value) {
398
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, "");
399
- }
400
- function ghData(value) {
401
- return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
402
- }
403
- function ghProperty(value) {
404
- return ghData(value).replace(/:/g, "%3A").replace(/,/g, "%2C");
405
- }
406
- function isObject(value) {
407
- return typeof value === "object" && value !== null && !Array.isArray(value);
408
- }
409
- function errorText(error) {
410
- return error instanceof Error ? error.message : String(error);
643
+ return { text: rows.length === 0 ? "" : `${rows.join("\n")}
644
+ `, sessions, turns: rows.length, unread };
411
645
  }
412
646
 
413
- // src/cli.ts
414
- function parseArgs(argv) {
415
- const a = {
416
- command: argv[0] === "--help" || argv[0] === "-h" ? "help" : argv[0] ?? "help",
417
- help: argv[0] === "--help" || argv[0] === "-h",
647
+ // src/commands.ts
648
+ import { resolve } from "path";
649
+ var CliError = class extends Error {
650
+ code;
651
+ constructor(message, code) {
652
+ super(message);
653
+ this.name = "CliError";
654
+ this.code = code;
655
+ }
656
+ };
657
+ var UsageError = class extends CliError {
658
+ constructor(message) {
659
+ super(message, "usage");
660
+ this.name = "UsageError";
661
+ }
662
+ };
663
+ function emptyArgs(command) {
664
+ return {
665
+ command,
666
+ refs: [],
418
667
  harness: "claude-code",
419
668
  harnessExplicit: false,
420
- all: false,
421
669
  last: 0,
422
670
  current: false,
423
671
  latestTurn: false,
424
- workflow: false,
425
- maxWorkflowSessions: 100,
426
- llm: false,
427
- interval: 5,
428
- intervalExplicit: false,
429
- window: 30,
430
- minLoop: 3,
431
- dryRun: false,
432
- yes: false,
433
672
  noContent: false,
434
- analyzers: [],
435
- replay: false,
436
- once: false,
437
- noSpans: false,
438
- noFindings: false,
439
- attrs: [],
440
- concurrency: 4,
441
- verifyFindings: false,
442
- replayCorpora: [],
443
673
  questions: [],
444
- annotations: false,
445
- explain: false
674
+ contracts: [],
675
+ shareSafe: false,
676
+ once: false,
677
+ dryRun: false,
678
+ yes: false,
679
+ unused: false,
680
+ knownSecretEnvs: []
446
681
  };
447
- for (let i = 1; i < argv.length; i++) {
448
- const arg = argv[i];
449
- const next = () => argv[++i];
450
- switch (arg) {
451
- case "--harness":
452
- a.harness = next() ?? a.harness;
453
- a.harnessExplicit = true;
454
- break;
455
- case "--all":
456
- a.all = true;
457
- break;
458
- case "--last":
459
- a.last = Number(next());
460
- break;
461
- case "--current":
462
- a.current = true;
463
- break;
464
- case "--latest-turn":
465
- a.latestTurn = true;
466
- break;
467
- case "--workflow":
468
- a.workflow = true;
469
- break;
470
- case "--max-workflow-sessions":
471
- a.maxWorkflowSessions = Number(next());
472
- break;
473
- case "--session":
474
- a.session = next();
475
- break;
476
- case "--cwd":
477
- a.cwd = next();
478
- break;
479
- case "--supervisor-run-dir":
480
- a.supervisorRunDir = next();
481
- break;
482
- case "--trajectory-dir":
483
- a.trajectoryDir = next();
484
- break;
485
- case "--revision":
486
- a.revision = next();
487
- break;
488
- case "--concurrency":
489
- a.concurrency = Number(next());
490
- break;
491
- case "--since":
492
- a.since = next();
493
- break;
494
- case "--until":
495
- a.until = next();
496
- break;
497
- case "--out":
498
- a.out = next();
499
- break;
500
- case "--dir":
501
- a.dir = next();
502
- break;
503
- case "--otlp":
504
- a.otlp = next();
505
- break;
506
- case "--source-bundle":
507
- a.sourceBundle = next();
508
- break;
509
- case "--otlp-out":
510
- a.otlpOut = next();
511
- break;
512
- case "--llm":
513
- a.llm = true;
514
- break;
515
- case "--budget":
516
- a.budget = Number(next());
517
- break;
518
- case "--model":
519
- a.model = next();
520
- break;
521
- case "--config":
522
- a.config = next();
523
- break;
524
- case "--mode":
525
- a.mode = next();
526
- break;
527
- case "--view":
528
- a.view = next();
529
- break;
530
- case "--metadata":
531
- a.metadata = next();
532
- break;
533
- case "--attr": {
534
- const v = next();
535
- if (v) a.attrs.push(v);
536
- break;
537
- }
538
- case "--interval":
539
- a.interval = Number(next());
540
- a.intervalExplicit = true;
541
- break;
542
- case "--window":
543
- a.window = Number(next());
544
- break;
545
- case "--min-loop":
546
- a.minLoop = Number(next());
547
- break;
548
- case "--dry-run":
549
- a.dryRun = true;
550
- break;
551
- case "--no-content":
552
- a.noContent = true;
553
- break;
554
- case "--replay":
555
- a.replay = true;
556
- break;
557
- // `stream --once` has always meant "one pass"; `watch --once` means the
558
- // same thing over a run directory, so one flag drives both.
559
- case "--once":
560
- a.once = true;
561
- a.replay = true;
562
- break;
563
- case "--no-spans":
564
- a.noSpans = true;
565
- break;
566
- case "--no-findings":
567
- a.noFindings = true;
568
- break;
569
- case "--analyzer": {
570
- const v = next();
571
- if (v) a.analyzers.push(v);
572
- break;
573
- }
574
- case "--verify-findings":
575
- a.verifyFindings = true;
576
- break;
577
- case "--replay-corpus": {
578
- const v = next();
579
- if (v) a.replayCorpora.push(v);
580
- break;
581
- }
582
- case "--verify-out":
583
- a.verifyOut = next();
584
- break;
585
- case "--analyzer-prompt":
586
- a.analyzerPrompt = next();
587
- break;
588
- case "--question": {
589
- const v = next();
590
- if (v !== void 0) a.questions.push(v);
591
- break;
682
+ }
683
+ function positiveInteger(flag, raw) {
684
+ const value = Number(raw);
685
+ if (!Number.isSafeInteger(value) || value < 1) throw new UsageError(`${flag} must be a positive whole number, got "${raw}"`);
686
+ return value;
687
+ }
688
+ function positiveNumber(flag, raw, unit) {
689
+ const value = Number(raw);
690
+ if (!Number.isFinite(value) || value <= 0) throw new UsageError(`${flag} must be a positive number of ${unit}`);
691
+ return value;
692
+ }
693
+ function timeBound(flag, raw) {
694
+ try {
695
+ parseTimeBound(raw, flag);
696
+ } catch (error) {
697
+ throw new UsageError(error instanceof Error ? error.message : String(error));
698
+ }
699
+ return raw;
700
+ }
701
+ function harnessId(raw) {
702
+ if (!resolveAdapter(raw)) throw new UsageError(`unknown harness "${raw}"; known: ${knownHarnesses().join(", ")}`);
703
+ return raw;
704
+ }
705
+ var FLAGS = {
706
+ "--current": { help: "the caller's own session: CODEX_THREAD_ID, else CLAUDE_CODE_SESSION_ID", set: (a) => {
707
+ a.current = true;
708
+ } },
709
+ "--last": { value: "N", help: "the N newest sessions by file mtime, never the caller's own", set: (a, v) => {
710
+ a.last = positiveInteger("--last", v);
711
+ } },
712
+ "--since": { value: "T", help: "every session with a record since T: 30m, 2h, 7d or an ISO time", set: (a, v) => {
713
+ a.since = timeBound("--since", v);
714
+ } },
715
+ "--until": { value: "T", help: "close the --since window (exclusive); same forms", set: (a, v) => {
716
+ a.until = timeBound("--until", v);
717
+ } },
718
+ "--cwd": { value: "DIR", help: "sessions recorded in DIR or under it, or in a worktree of its repo", set: (a, v) => {
719
+ a.cwd = resolve(v);
720
+ } },
721
+ "--harness": { value: "ID", help: "read only this harness; with a file ref, force its reader", set: (a, v) => {
722
+ a.harness = harnessId(v);
723
+ a.harnessExplicit = true;
724
+ } },
725
+ "--latest-turn": { help: "only the latest task turn of a resumed Codex or Claude Code session", set: (a) => {
726
+ a.latestTurn = true;
727
+ } },
728
+ "--format": { value: "F", help: "the output format", set: (a, v) => {
729
+ a.format = v;
730
+ } },
731
+ "--out": { value: "PATH", help: "write the output to PATH; stdout gets one summary line", set: (a, v) => {
732
+ a.out = v;
733
+ } },
734
+ "--no-content": { help: "omit conversation, commands, paths and assignments", set: (a) => {
735
+ a.noContent = true;
736
+ } },
737
+ "--match": {
738
+ value: "RE",
739
+ help: "one row per human turn, agent message or shell command matching RE (a command: ok, failed or ?)",
740
+ freeText: true,
741
+ set: (a, v) => {
742
+ try {
743
+ new RegExp(v);
744
+ } catch (error) {
745
+ throw new UsageError(`--match is not a regular expression: ${error instanceof Error ? error.message : String(error)}`);
592
746
  }
593
- case "--questions":
594
- a.questionsFile = next();
595
- break;
596
- case "--question-budget":
597
- a.questionBudget = Number(next());
598
- break;
599
- case "--question-timeout":
600
- a.questionTimeout = Number(next());
601
- break;
602
- case "--redactor":
603
- a.redactorCmd = next();
604
- break;
605
- case "--format":
606
- a.format = next();
607
- break;
608
- case "--contract":
609
- a.contract = next();
610
- break;
611
- case "--junit":
612
- a.junit = next();
613
- break;
614
- case "--evidence":
615
- a.evidence = next();
616
- break;
617
- case "--annotations":
618
- a.annotations = true;
619
- break;
620
- case "--explain":
621
- a.explain = true;
622
- break;
623
- case "--help":
624
- case "-h":
625
- a.help = true;
626
- break;
627
- case "--yes":
628
- case "-y":
629
- a.yes = true;
630
- break;
631
- default:
632
- if (arg?.startsWith("--")) throw new Error(`unknown flag: ${arg}`);
633
- if (arg && !a.input) a.input = arg;
634
- else if (arg) throw new Error(`unexpected positional argument: ${arg}`);
747
+ a.match = v;
748
+ }
749
+ },
750
+ "--question": { value: "TEXT", help: `a question (repeatable, at most ${MAX_TRACE_QUESTION_CHARS} characters)`, freeText: true, set: (a, v) => {
751
+ a.questions.push(v);
752
+ } },
753
+ "--questions": { value: "FILE", help: "a JSON array of questions: strings or {id?, question, answerSchema?}", set: (a, v) => {
754
+ a.questionsFile = v;
755
+ } },
756
+ "--budget": { value: "USD", help: "required: the most the run may spend, shared by every question", set: (a, v) => {
757
+ a.budget = positiveNumber("--budget", v, "USD");
758
+ } },
759
+ "--model": { value: "ID", help: `the analyst model (default ${DEFAULT_ANALYST_MODEL})`, set: (a, v) => {
760
+ a.model = v;
761
+ } },
762
+ "--contract": { value: "FILE|conformance", help: "gate on a trace contract file, or on conformance (repeatable)", set: (a, v) => {
763
+ a.contracts.push(v);
764
+ } },
765
+ "--share-safe": {
766
+ help: "gate on whether each path is safe to share, read as bytes: SAFE passes, UNSAFE fails, UNKNOWN cannot decide",
767
+ set: (a) => {
768
+ a.shareSafe = true;
769
+ }
770
+ },
771
+ "--once": { help: "one pass, then exit", set: (a) => {
772
+ a.once = true;
773
+ } },
774
+ "--dry-run": { help: "redact and preview; send nothing", set: (a) => {
775
+ a.dryRun = true;
776
+ } },
777
+ "--yes": { help: "skip the confirmation prompt", set: (a) => {
778
+ a.yes = true;
779
+ } },
780
+ "--unused": { help: "only installed skills with no use in the window", set: (a) => {
781
+ a.unused = true;
782
+ } },
783
+ "--known-secret-env": {
784
+ value: "NAME",
785
+ help: "with --share-safe: an environment variable whose value must appear nowhere (repeatable)",
786
+ set: (a, v) => {
787
+ a.knownSecretEnvs.push(v);
635
788
  }
636
789
  }
637
- return a;
638
- }
639
- var OTLP_INPUT_COMMANDS = /* @__PURE__ */ new Set([
640
- "analyze",
641
- "check",
642
- "facts",
643
- "investigate",
644
- "improve",
645
- "ask",
646
- "stream",
647
- "validate",
648
- "mcp"
649
- ]);
650
- function validateOtlpSelection(args) {
651
- if (args.otlp === void 0) return args;
652
- if (!OTLP_INPUT_COMMANDS.has(args.command)) {
653
- throw new Error(
654
- `${args.command} does not read --otlp. --otlp reads OTLP-JSONL on ${[...OTLP_INPUT_COMMANDS].join(", ")}; use --otlp-out <path> to write the artifact.`
790
+ };
791
+ var ALIASES = { "-y": "--yes" };
792
+ var SELECT = ["--current", "--last", "--since", "--cwd", "--harness", "--latest-turn"];
793
+ var MANY = Number.POSITIVE_INFINITY;
794
+ var COMMANDS = [
795
+ {
796
+ name: "list",
797
+ usage: "list [<store-root>] [--last N | --since T] [--cwd DIR] [--harness ID] [--format text|json]",
798
+ job: "Find a session: one row per session, newest first, across every harness, never the caller's own.",
799
+ flags: [
800
+ { flag: "--last", help: "the N newest sessions (default 20)" },
801
+ "--since",
802
+ "--cwd",
803
+ { flag: "--harness", help: "only this harness's sessions" },
804
+ { flag: "--format", help: "text (default), or json with id, path, bytes and parent id" }
805
+ ],
806
+ formats: ["text", "json"],
807
+ refs: [0, 1],
808
+ jq: [
809
+ [".sessions[].ref", "the short ref any command takes"],
810
+ [".sessions[] | {id, cwd, firstPrompt}", "which session is which"],
811
+ [".selection.unreadSessions", "sessions that could not be read, and why"]
812
+ ],
813
+ examples: ["traces list --cwd . --last 5", "traces list --since 1d --harness codex", "traces list --format json | jq -r '.sessions[0].id'"],
814
+ overview: [["Which sessions ran?", "traces list [--cwd .] [--last N]"]]
815
+ },
816
+ {
817
+ name: "facts",
818
+ usage: "facts <ref\u2026> | --current | --last N | --since T [flags]",
819
+ job: "What happened, exactly, for $0: who acted, turns, tools, commands, PRs, first failure, tokens, cost, time lost.",
820
+ flags: [
821
+ ...SELECT.slice(0, 3),
822
+ "--until",
823
+ ...SELECT.slice(3),
824
+ "--match",
825
+ "--no-content",
826
+ { flag: "--format", help: "text (default, 25 lines a session), json (the sheet), or jsonl: one row per typed turn" },
827
+ { flag: "--out", help: "write the sheet to PATH; stdout gets one summary line" }
828
+ ],
829
+ formats: ["text", "json", "jsonl"],
830
+ refs: [0, MANY],
831
+ select: "required",
832
+ jq: [
833
+ [".sessions[0].humanTurns.value[] | {at, text, spanId}", "each typed turn, its time and span"],
834
+ [".sessions[0].commands.value[]", "programs run: runs, failed, inScripts"],
835
+ [".sessions[0].measurements.tools[] | {name, calls, failed: .outcomes.error}", "tool calls by name, and failures"],
836
+ [".sessions[0] | {firstFailure, tokens, cost, agent: .agent.value}", "first failure, tokens, cost, who acted"]
837
+ ],
838
+ examples: ["traces facts --current", "traces facts 019a3c7e --format json | jq '.sessions[0].measurements.waste'", "traces facts --since 30d --match '^gh pr merge'"],
839
+ overview: [
840
+ ["What happened (turns, PRs, failure, cost)? $0", "traces facts <ref> | --current | --since 1d"],
841
+ ["Did we already try X / what was said? $0", "traces facts <ref> | --since 30d --match '<re>'"]
842
+ ]
843
+ },
844
+ {
845
+ name: "analyze",
846
+ usage: "analyze <ref\u2026> | --current | --last N | --since T [flags]",
847
+ job: "What went wrong, ranked, for $0: failing commands, stalls, interrupts, findings, and what the trace cannot answer.",
848
+ notes: ["At most 40 lines; several sessions get a table of the 15 with the most waste. A Runtime run dir reports its tree."],
849
+ flags: [
850
+ ...SELECT.slice(0, 3),
851
+ "--until",
852
+ ...SELECT.slice(3),
853
+ { flag: "--format", help: "text (default, at most 40 lines) or json: facts, packet, measures, conformance, cannotAnswer" },
854
+ { flag: "--out", value: "DIR", help: "write report.md and findings.json under DIR; stdout gets one summary line" }
855
+ ],
856
+ formats: ["text", "json"],
857
+ refs: [0, MANY],
858
+ select: "required",
859
+ jq: [
860
+ [".packet.findings[] | {severity, subject, claim}", "the findings, HIGH first"],
861
+ [".cannotAnswer[] | {analysis, reason}", "what the trace cannot answer, and why"],
862
+ [".facts.sessions[0].measurements.waste", "failing streaks, stalls, reverts"]
863
+ ],
864
+ examples: ["traces analyze --current", "traces analyze 019a3c7e --out analysis", "traces analyze --since 1d --cwd ."],
865
+ overview: [["Where did it waste time or get interrupted? $0", "traces analyze <ref>"]]
866
+ },
867
+ {
868
+ name: "ask",
869
+ usage: "ask <ref\u2026> | --current | --last N | --since T --question TEXT [flags]",
870
+ job: "A question facts cannot answer, with checked trace:// citations: the only command that calls a model.",
871
+ notes: [
872
+ `Each question spends at most min(--budget, $${DEFAULT_QUESTION_MAX_COST_USD}) within ${DEFAULT_QUESTION_TIMEOUT_MS / 1e3} s; ${DEFAULT_ASK_CONCURRENCY} run at once. Some unanswered: exit 2.`,
873
+ "Needs TANGLE_API_KEY (or OPENAI_API_KEY) and uv, or TRACES_PYTHON naming a Python with agent-eval-rpc[dspy]."
874
+ ],
875
+ flags: [
876
+ ...SELECT,
877
+ "--question",
878
+ "--questions",
879
+ "--budget",
880
+ "--model",
881
+ { flag: "--format", help: "text (default): the report; json: each answer, its citations, failure and cost" },
882
+ { flag: "--out", value: "DIR", help: "write answers.json and report.md under DIR; stdout gets one summary line" }
883
+ ],
884
+ formats: ["text", "json"],
885
+ refs: [0, MANY],
886
+ select: "required",
887
+ jq: [[".questions[] | {id, status, answer, failure}", "each answer, or why it has none"]],
888
+ examples: [
889
+ 'traces ask --current --question "Which command failed first?" --budget 1',
890
+ "traces ask 019a3c7e --questions questions.json --budget 5 --out answers",
891
+ `traces ask --last 3 --question "Did it verify the fix?" --budget 2 --format json | jq '.questions[0].answer'`
892
+ ],
893
+ overview: [["A question facts can't answer (model, $)", 'traces ask <ref> --question "\u2026" --budget 1']]
894
+ },
895
+ {
896
+ name: "check",
897
+ usage: "check <ref\u2026> | --current | --last N | --since T --contract FILE|conformance | --share-safe [flags]",
898
+ job: "The one gate: does this run pass a trace contract or conform, and is this file safe to share?",
899
+ notes: [
900
+ "A gate is required. --share-safe reads each path as bytes, never as a trace. --contract F with no ref explains F.",
901
+ "Exit 0 pass, 1 any gate fails, 2 a gate could not decide: a bad contract, a rule or verdict that could not run, an unreadable ref."
902
+ ],
903
+ flags: [
904
+ "--contract",
905
+ "--share-safe",
906
+ "--known-secret-env",
907
+ ...SELECT.slice(0, 4),
908
+ { flag: "--harness", help: "force the reader of a file ref (claude-code, codex, \u2026)" },
909
+ { flag: "--format", help: "text (default), json, junit or github: the verdict, on stdout" },
910
+ { flag: "--out", value: "DIR", help: "write evidence for each gate that did not pass under DIR" }
911
+ ],
912
+ formats: ["text", "json", "junit", "github"],
913
+ refs: [0, MANY],
914
+ select: "required",
915
+ jq: [
916
+ [".gates[] | {gate, status}", "each gate and what it decided"],
917
+ [".gates[].report.traces[]?.verdict.violations", "each rule a run broke, with span ids"]
918
+ ],
919
+ examples: [
920
+ "traces check run.otlp.jsonl --contract contract.json --format junit > contract.xml",
921
+ "traces check spans.otlp.jsonl --contract conformance",
922
+ "traces check handoff.md --share-safe --known-secret-env TANGLE_API_KEY"
923
+ ],
924
+ overview: [
925
+ ["Does this run pass? (CI)", "traces check <ref> --contract c.json|conformance"],
926
+ ["Is this safe to share?", "traces check <path> --share-safe"]
927
+ ]
928
+ },
929
+ {
930
+ name: "diff",
931
+ usage: "diff <a> <b> [--format text|json]",
932
+ job: "Where two runs of one task split: the first tool call that differs, with both calls.",
933
+ notes: [
934
+ "A side is a session id or 8+ char prefix or suffix, a session file, OTLP, file#<trace id> or file#branch=<id>.",
935
+ "Calls compare by tool, status and argument (the command, the file, or the input), after each run's",
936
+ "cwd, worktree, temp dirs and hex ids become <cwd>, <worktree>, <tmp> and <hex>. Model calls are not compared.",
937
+ "Exit 0 agree, 1 diverge, 2 could not compare: a side cannot be read, or neither records an outcome."
938
+ ],
939
+ flags: [{ flag: "--format", help: "text (default) or json" }],
940
+ formats: ["text", "json"],
941
+ refs: [2, 2],
942
+ jq: [
943
+ [".diff.firstDivergence", "where and how the runs first split (index is 0-based)"],
944
+ [".a.steps[.diff.firstDivergence.a].text", "A's call at that step, normalized"]
945
+ ],
946
+ examples: [
947
+ "traces diff 0199a1b2 0199c3d4",
948
+ "traces diff passing.otlp.jsonl failing.otlp.jsonl",
949
+ "traces diff run.otlp.jsonl#branch=a run.otlp.jsonl#branch=b --format json"
950
+ ],
951
+ overview: [["Where did two runs diverge?", "traces diff <a> <b>"]]
952
+ },
953
+ {
954
+ name: "watch",
955
+ usage: "watch [<id\u2026> | <store-root>] [--since T] [--once] | watch <run-dir|span file> [--once]",
956
+ job: "Live supervision: what the analyze rules find in every session active in the last 30 minutes, or one run tree.",
957
+ notes: [
958
+ "Sessions are read every 5 s; a run tree every 2 s, until its result lands (exit 1 when the run ended with an error).",
959
+ "A run dir or span file prints its tree as text, and its findings with --format jsonl."
960
+ ],
961
+ flags: [
962
+ { flag: "--since", help: "sessions active in the last T (default 30m; it slides with time), or since an ISO time" },
963
+ "--cwd",
964
+ { flag: "--harness", help: "only this harness's sessions; with a file ref, force its reader" },
965
+ { flag: "--once", help: "one pass, then exit" },
966
+ { flag: "--format", help: "text (default): a line per new finding, or the run tree; jsonl: the traces.stream events" }
967
+ ],
968
+ formats: ["text", "jsonl"],
969
+ refs: [0, MANY],
970
+ jq: [
971
+ ['select(.event == "finding") | .finding | {severity, ruleId, claim}', "each new finding"],
972
+ ['select(.event == "tick") | .unreadSessions', "sessions a pass could not read, and why"]
973
+ ],
974
+ examples: ["traces watch --once", "traces watch runs/my-run --once", "traces watch --since 2h --format jsonl"],
975
+ overview: [["What is happening right now?", "traces watch [<run-dir>] [--once]"]]
976
+ },
977
+ {
978
+ name: "bundle",
979
+ usage: "bundle <ref> | --current --out DIR (then: traces bundle verify DIR)",
980
+ job: "Keep one session's evidence beyond log rotation: session/, spans, facts, git log, sha256 manifest.",
981
+ notes: ["A bundle directory is a ref: traces facts DIR reads it."],
982
+ flags: ["--current", { flag: "--harness", help: "force the reader of a file ref" }, { flag: "--out", value: "DIR", help: "a new or empty directory for the bundle" }],
983
+ refs: [0, 1],
984
+ select: "required",
985
+ examples: ["traces bundle 019a3c7e --out evidence/019a3c7e", "traces bundle --current --out evidence/now", "traces bundle verify evidence/019a3c7e"],
986
+ overview: [["Keep a session's evidence", "traces bundle <ref> --out <dir> \xB7 bundle verify <dir>"]]
987
+ },
988
+ {
989
+ name: "bundle verify",
990
+ usage: "bundle verify <dir> [--format text|json]",
991
+ job: "Check a bundle against its manifest: every listed file with its size and SHA-256, nothing unlisted.",
992
+ notes: ["Exit 0 verified, 1 any difference."],
993
+ flags: [{ flag: "--format", help: "text (default) or json" }],
994
+ formats: ["text", "json"],
995
+ refs: [1, 1],
996
+ jq: [
997
+ [".ok", "true when every file matches"],
998
+ [".issues[] | {kind, path, detail}", "each difference"]
999
+ ],
1000
+ examples: ["traces bundle verify evidence/019a3c7e", "traces bundle verify evidence/019a3c7e --format json | jq .issues"]
1001
+ },
1002
+ {
1003
+ name: "convert",
1004
+ usage: "convert <ref\u2026> | --current | --last N | --since T [--out FILE] [--no-content]",
1005
+ job: "Hand spans to another tool: contract OTLP-JSONL, to --out or stdout.",
1006
+ notes: ["Sandbox events, Codex live streams and OpenInference files are sniffed; --harness forces a reader."],
1007
+ flags: [
1008
+ ...SELECT,
1009
+ { flag: "--no-content", help: "metadata only: drop prompts, replies, tool input and output, error text, paths and briefs" },
1010
+ { flag: "--out", help: "write the spans to PATH; stdout gets one summary line" },
1011
+ { flag: "--format", help: "the --out summary line: text (default) or json {out, spans, droppedAttributes}" }
1012
+ ],
1013
+ formats: ["text", "json"],
1014
+ refs: [0, MANY],
1015
+ select: "required",
1016
+ jq: [[".droppedAttributes", "with --out and --no-content: the attribute keys the metadata-only rule removed"]],
1017
+ examples: [
1018
+ "traces convert --current --out spans.otlp.jsonl",
1019
+ "traces convert raw.otlp.jsonl --no-content --out metadata.otlp.jsonl --format json",
1020
+ "traces convert live-stream.jsonl --harness codex-exec > spans.otlp.jsonl"
1021
+ ],
1022
+ overview: [["Hand spans to another tool", "traces convert <ref> --out <file>"]]
1023
+ },
1024
+ {
1025
+ name: "upload",
1026
+ usage: "upload <ref\u2026> | --current | --since T [--dry-run] [--no-content] [--yes]",
1027
+ job: "Send redacted sessions to Tangle Intelligence; one still UNSAFE or UNKNOWN once redacted is refused (exit 1).",
1028
+ notes: ["Env: TANGLE_INGEST_URL, TANGLE_INGEST_API_KEY (or TANGLE_API_KEY), TANGLE_TENANT_ID."],
1029
+ flags: [
1030
+ "--current",
1031
+ "--since",
1032
+ "--cwd",
1033
+ "--harness",
1034
+ "--dry-run",
1035
+ { flag: "--no-content", help: "strip prompt and response text; send metadata only" },
1036
+ "--yes",
1037
+ { flag: "--out", help: "with --dry-run: write the redacted OTLP preview to PATH" }
1038
+ ],
1039
+ refs: [0, MANY],
1040
+ examples: ["traces upload --since 24h --dry-run", "traces upload 019a3c7e --yes", "traces upload spans.otlp.jsonl --dry-run --out preview.otlp.jsonl"],
1041
+ overview: [["Ship sessions to Intelligence", "traces upload <ref>|--since T [--dry-run]"]]
1042
+ },
1043
+ {
1044
+ name: "skills",
1045
+ usage: "skills [--since T] [--unused] [--format text|json]",
1046
+ job: "Which skills are used, and which installed skills never are; no model call.",
1047
+ notes: ["Reads Claude Code, Codex and Kimi Code sessions; each run reads only bytes appended since the last."],
1048
+ flags: [
1049
+ { flag: "--since", help: "count use since T: 30m, 2h, 7d or an ISO time (default 30d)" },
1050
+ "--unused",
1051
+ { flag: "--format", help: "text (default), or json with the refresh cost and sweep exclusions" }
1052
+ ],
1053
+ formats: ["text", "json"],
1054
+ refs: [0, 0],
1055
+ jq: [
1056
+ [".rows[] | {skill, sessions}", "use per skill"],
1057
+ [".unused[].skill", "installed skills never used in the window"]
1058
+ ],
1059
+ examples: ["traces skills --since 30d", "traces skills --unused", "traces skills --format json | jq '.rows[] | {skill, sessions}'"],
1060
+ overview: [["Which skills are used?", "traces skills [--unused]"]]
1061
+ },
1062
+ {
1063
+ name: "mcp",
1064
+ usage: "mcp <ref\u2026> | --current | --last N | --since T",
1065
+ job: "Serve agent-eval's read-only span tools over stdio, for an MCP client with no shell.",
1066
+ notes: [
1067
+ "Spans and results are redacted, and it refuses to start if redacted spans are UNSAFE or UNKNOWN.",
1068
+ "Each result carries an untrusted-text notice and is capped at 512 KiB."
1069
+ ],
1070
+ flags: SELECT.slice(0, 5),
1071
+ refs: [0, MANY],
1072
+ select: "required",
1073
+ examples: ["traces mcp spans.otlp.jsonl", "traces mcp --last 5", "claude mcp add traces -- traces mcp spans.otlp.jsonl"],
1074
+ overview: [["Serve span tools over MCP", "traces mcp <ref>|--last N"]]
1075
+ }
1076
+ ];
1077
+ function usesOf(spec) {
1078
+ return spec.flags.map((use) => typeof use === "string" ? { flag: use } : use);
1079
+ }
1080
+ var SPECS = COMMANDS;
1081
+ function commandAt(argv) {
1082
+ if (argv[0] === "bundle" && argv[1] === "verify") return SPECS.find((spec) => spec.name === "bundle verify");
1083
+ return SPECS.find((spec) => spec.name === argv[0]);
1084
+ }
1085
+ function appliesTo(flag) {
1086
+ return SPECS.filter((spec) => usesOf(spec).some((use) => use.flag === flag)).map((spec) => spec.name);
1087
+ }
1088
+ function formatHelp(formats) {
1089
+ return `${formats[0]} (default) or ${formats.slice(1).join(" or ")}`;
1090
+ }
1091
+ var OVERVIEW_COLUMN = 49;
1092
+ function renderGlobalHelp() {
1093
+ const rows = SPECS.flatMap((spec) => (spec.overview ?? []).map(([question, example]) => ` ${question.padEnd(OVERVIEW_COLUMN - 1)} ${example}`));
1094
+ const formats = [...new Set(SPECS.flatMap((spec) => spec.formats ?? []))];
1095
+ return [
1096
+ "traces: what your agents did, where they wasted time, whether a run passes",
1097
+ "Start here for one session: traces facts <ref> (25 lines); one field: add --format json | jq '<path>'",
1098
+ "",
1099
+ ...rows,
1100
+ "",
1101
+ "ref: session id, or 8+ char unique prefix or suffix; session file; OTLP file/dir; bundle dir; run dir; store root",
1102
+ "Select: --current --last N --since/--until 2h|1d|ISO --cwd DIR --harness ID --latest-turn",
1103
+ `Output: --format ${formats.join("|")} --out PATH --no-content`,
1104
+ "Exit: 0 yes \xB7 1 no (fail, diverged, unsafe) \xB7 2 could not answer (stderr says why)",
1105
+ "traces <command> --help for flags, jq paths and examples \xB7 moved from 0.x: traces help moved",
1106
+ ""
1107
+ ].join("\n");
1108
+ }
1109
+ var FLAG_COLUMN = 20;
1110
+ function renderCommandHelp(spec) {
1111
+ const flagLines = usesOf(spec).map((use) => {
1112
+ const flag = FLAGS[use.flag];
1113
+ const value = use.flag === "--format" && spec.formats ? spec.formats.join("|") : use.value ?? flag.value;
1114
+ const left = value ? `${use.flag} ${value}` : use.flag;
1115
+ const help = use.help ?? (use.flag === "--format" && spec.formats ? formatHelp(spec.formats) : flag.help);
1116
+ return ` ${left.padEnd(FLAG_COLUMN - 1)} ${help}`;
1117
+ });
1118
+ const lines = [`Usage: traces ${spec.usage}`, spec.job, ...spec.notes ?? []];
1119
+ if (flagLines.length > 0) lines.push("", ...flagLines);
1120
+ if (spec.jq && spec.jq.length > 0) {
1121
+ const width = Math.max(...spec.jq.map(([path]) => path.length));
1122
+ const format = spec.formats?.includes("json") ? "json" : "jsonl";
1123
+ lines.push("", `jq paths (--format ${format}):`, ...spec.jq.map(([path, what]) => ` ${path.padEnd(width)} ${what}`));
1124
+ }
1125
+ lines.push("", "Examples:", ...spec.examples.map((example) => ` ${example}`));
1126
+ return `${lines.join("\n")}
1127
+ `;
1128
+ }
1129
+ function unknownCommand(word) {
1130
+ return new UsageError(word.startsWith("-") ? `${word} needs a command before it: traces <command> ${word} \u2026; traces --help lists the commands` : `unknown command ${word}; traces --help lists the commands`);
1131
+ }
1132
+ function parseCommandLine(argv) {
1133
+ const [first, second] = argv;
1134
+ if (first === void 0 || first === "--help" || first === "-h") return { kind: "print", text: renderGlobalHelp() };
1135
+ if (first === "--version" || first === "-v" || first === "version") {
1136
+ return { kind: "print", text: `traces ${tracesVersion()} (${tracesCommit()})
1137
+ ` };
1138
+ }
1139
+ if (first === "help") {
1140
+ if (second === void 0) return { kind: "print", text: renderGlobalHelp() };
1141
+ if (second === "moved") return { kind: "print", text: renderMovedTable() };
1142
+ const spec2 = commandAt(argv.slice(1));
1143
+ if (!spec2) throw unknownCommand(second);
1144
+ return { kind: "print", text: renderCommandHelp(spec2) };
1145
+ }
1146
+ const spec = commandAt(argv);
1147
+ if (!spec) throw unknownCommand(first);
1148
+ const rest = argv.slice(spec.name.split(" ").length);
1149
+ try {
1150
+ const args = parseFlags(spec, rest);
1151
+ if (args === "help") return { kind: "print", text: renderCommandHelp(spec) };
1152
+ checkArgs(spec, args, argv);
1153
+ return { kind: "run", args };
1154
+ } catch (error) {
1155
+ if (error instanceof UsageError && (rest.includes("--help") || rest.includes("-h"))) {
1156
+ return { kind: "print", text: renderCommandHelp(spec) };
1157
+ }
1158
+ throw error;
1159
+ }
1160
+ }
1161
+ function parseFlags(spec, rest) {
1162
+ const args = emptyArgs(spec.name);
1163
+ const allowed = new Set(usesOf(spec).map((use) => use.flag));
1164
+ let help = false;
1165
+ for (let i = 0; i < rest.length; i++) {
1166
+ const word = rest[i];
1167
+ if (word === "--help" || word === "-h") {
1168
+ help = true;
1169
+ continue;
1170
+ }
1171
+ if (!word.startsWith("-") || word === "-") {
1172
+ args.refs.push(word);
1173
+ continue;
1174
+ }
1175
+ const name = ALIASES[word] ?? word;
1176
+ const flag = FLAGS[name];
1177
+ if (!flag) throw new UsageError(`unknown flag ${word} for ${spec.name}; traces ${spec.name} --help lists its flags`);
1178
+ if (!allowed.has(name)) throw new UsageError(`${name} does not apply to ${spec.name}; it applies to: ${appliesTo(name).join(", ")}`);
1179
+ if (flag.value === void 0) {
1180
+ flag.set(args, "");
1181
+ continue;
1182
+ }
1183
+ const value = rest[i + 1];
1184
+ if (value === void 0 || !flag.freeText && value.startsWith("--")) {
1185
+ throw new UsageError(`${name} needs a value: ${name} ${flag.value}`);
1186
+ }
1187
+ i++;
1188
+ flag.set(args, value);
1189
+ }
1190
+ if (help) return "help";
1191
+ args.input = args.refs[0];
1192
+ return args;
1193
+ }
1194
+ function checkArgs(spec, args, argv) {
1195
+ const [min, max] = spec.refs;
1196
+ if (args.refs.length > max) {
1197
+ throw new UsageError(max === 0 ? `${spec.name} takes no ref, got ${args.refs[0]}: traces ${spec.usage}` : `unexpected argument ${args.refs[max]}: traces ${spec.usage}`);
1198
+ }
1199
+ if (args.refs.length < min) throw new UsageError(`${spec.name} needs ${min === 1 ? "one ref" : `${min} refs`}: traces ${spec.usage}`);
1200
+ if (args.format !== void 0 && spec.formats && !spec.formats.includes(args.format)) {
1201
+ throw new UsageError(`unknown ${spec.name} format "${args.format}" (expected ${spec.formats.join(" or ")})`);
1202
+ }
1203
+ const selected = args.refs.length > 0 || args.current || args.last > 0 || args.since !== void 0;
1204
+ if (spec.name === "upload" && !selected) {
1205
+ throw new UsageError(`upload with no selection was removed in traces 1.0. Instead: ${tracesCommand([...argv, "--since", "24h"])}`);
1206
+ }
1207
+ if (spec.name === "check") checkGates(args);
1208
+ const explains = spec.name === "check" && !selected;
1209
+ if (spec.select === "required" && !selected && !explains) {
1210
+ const flags = new Set(usesOf(spec).map((use) => use.flag));
1211
+ const ways = ["a ref", ...["--current", "--last N", "--since T"].filter((way) => flags.has(way.split(" ")[0]))];
1212
+ const choice = ways.length === 1 ? ways[0] : `${ways.slice(0, -1).join(", ")} or ${ways.at(-1)}`;
1213
+ throw new UsageError(
1214
+ `${spec.name} needs ${choice}${flags.has("--cwd") ? "; --cwd and --harness only narrow" : ""}. traces list finds a session`
655
1215
  );
656
1216
  }
657
- if (args.input) throw new Error("--otlp cannot be combined with an input file");
658
- if (args.session) throw new Error("--otlp reads a file and cannot be combined with --session");
659
- if (args.supervisorRunDir) throw new Error("--otlp cannot be combined with --supervisor-run-dir");
660
- if (args.workflow || args.latestTurn) {
661
- throw new Error("--workflow and --latest-turn expand harness sessions and cannot be combined with --otlp");
1217
+ if (spec.name === "ask" && args.budget === void 0) {
1218
+ throw new UsageError(`ask needs --budget USD, the most the run may spend: ${tracesCommand([...argv, "--budget", "1"])} (each question spends at most min(USD, $${DEFAULT_QUESTION_MAX_COST_USD}))`);
662
1219
  }
663
- return args;
664
1220
  }
665
- var CURRENT_SESSION_COMMANDS = /* @__PURE__ */ new Set([
666
- "analyze",
667
- "facts",
668
- "investigate",
669
- "improve",
670
- "ask",
671
- "convert",
672
- "index",
673
- "evidence",
674
- "bundle",
675
- "stream"
676
- ]);
677
- var WORKFLOW_COMMANDS = /* @__PURE__ */ new Set([
678
- "analyze",
679
- "facts",
680
- "investigate",
681
- "improve",
682
- "ask",
683
- "convert",
684
- "index",
685
- "evidence"
686
- ]);
687
- function applyCurrentSessionSelection(args) {
688
- if (!args.current) return args;
689
- if (!CURRENT_SESSION_COMMANDS.has(args.command)) {
690
- throw new Error(`--current is not supported by ${args.command}`);
691
- }
692
- if (args.input) throw new Error("--current cannot be combined with an input file");
693
- if (args.supervisorRunDir) throw new Error("--current cannot be combined with --supervisor-run-dir");
694
- if (args.session) throw new Error("--current cannot be combined with --session");
695
- if (args.last > 0) throw new Error("--current cannot be combined with --last");
696
- if (args.all) throw new Error("--current cannot be combined with --all");
697
- const adapter = resolveAdapter(args.harness);
698
- if (adapter?.harness !== "codex") {
699
- throw new Error("--current uses CODEX_THREAD_ID and requires --harness codex");
700
- }
701
- const sessionId = process.env.CODEX_THREAD_ID?.trim();
702
- if (!sessionId) throw new Error("--current requires CODEX_THREAD_ID from the active Codex session");
703
- return { ...args, session: sessionId };
704
- }
705
- function validateViewSelection(args) {
706
- if (args.view === void 0 || args.command === "bundle-view") return args;
707
- throw new Error(
708
- `--view is not supported by ${args.command}. A view is a projection of an assembled bundle: run \`traces bundle ... --out <dir>\` first, then \`traces bundle-view <dir> --view evidence-only --out <view-dir>\`.`
709
- );
1221
+ function checkGates(args) {
1222
+ if (args.contracts.length === 0 && !args.shareSafe) {
1223
+ throw new UsageError("check needs a gate: --contract FILE|conformance (repeatable), or --share-safe");
1224
+ }
1225
+ if (args.shareSafe && (args.current || args.last > 0 || args.since !== void 0)) {
1226
+ throw new UsageError("--share-safe reads the paths you name; --current, --last and --since select sessions for a contract. Run them as two checks");
1227
+ }
1228
+ if (args.shareSafe && args.refs.length === 0) throw new UsageError("--share-safe needs the files or directories to read: traces check <path> --share-safe");
1229
+ if (args.knownSecretEnvs.length > 0 && !args.shareSafe) throw new UsageError("--known-secret-env applies with --share-safe");
1230
+ }
1231
+ function wantsJsonErrors(argv) {
1232
+ for (let i = 0; i < argv.length; i++) {
1233
+ if (FLAGS[argv[i]]?.freeText) {
1234
+ i++;
1235
+ continue;
1236
+ }
1237
+ if (argv[i] === "--format" && (argv[i + 1] === "json" || argv[i + 1] === "jsonl")) return true;
1238
+ }
1239
+ return false;
1240
+ }
1241
+ function writeError(message, code, json, extra = {}) {
1242
+ process.stderr.write(json ? `${JSON.stringify({ error: message, code, ...extra })}
1243
+ ` : `error: ${message}
1244
+ `);
710
1245
  }
711
- function validateUntilSelection(args) {
712
- if (args.until === void 0 || args.command === "facts") return args;
713
- throw new Error(`--until is not supported by ${args.command}; it bounds the facts window (traces facts --since <t> --until <t>)`);
1246
+ function reportError(error, json) {
1247
+ const message = error instanceof Error ? error.message : String(error);
1248
+ const code = error instanceof RefError || error instanceof CliError ? error.code : "unreadable";
1249
+ const candidates = error instanceof RefError && error.candidates.length > 0 ? { candidates: error.candidates } : {};
1250
+ writeError(message, code, json, candidates);
1251
+ process.exitCode = 2;
714
1252
  }
715
- function validateWorkflowSelection(args) {
716
- if (!args.workflow && !args.latestTurn) return args;
717
- if (!WORKFLOW_COMMANDS.has(args.command)) {
718
- throw new Error(`${args.workflow ? "--workflow" : "--latest-turn"} is not supported by ${args.command}`);
1253
+ async function runTraces(argv, handlers) {
1254
+ try {
1255
+ const moved = findMoved(argv);
1256
+ if (moved) throw new UsageError(movedMessage(moved, argv));
1257
+ const parsed = parseCommandLine(argv);
1258
+ if (parsed.kind === "print") {
1259
+ process.stdout.write(parsed.text);
1260
+ return;
1261
+ }
1262
+ await handlers[parsed.args.command](parsed.args);
1263
+ } catch (error) {
1264
+ reportError(error, wantsJsonErrors(argv));
719
1265
  }
720
- if (args.input) throw new Error("--workflow and --latest-turn cannot be combined with an input file");
721
- if (args.supervisorRunDir) {
722
- throw new Error("--workflow and --latest-turn cannot be combined with --supervisor-run-dir");
1266
+ }
1267
+
1268
+ // src/cli.ts
1269
+ var MAX_CHILD_SESSIONS = 100;
1270
+ function callerSession(harness) {
1271
+ const codex = process.env.CODEX_THREAD_ID?.trim();
1272
+ const claude = process.env.CLAUDE_CODE_SESSION_ID?.trim();
1273
+ if (codex && harness !== "claude-code") return { harness: "codex", sessionId: codex };
1274
+ if (claude && harness !== "codex") return { harness: "claude-code", sessionId: claude };
1275
+ return void 0;
1276
+ }
1277
+ var CURRENT_VARIABLES = "CODEX_THREAD_ID (Codex) or CLAUDE_CODE_SESSION_ID (Claude Code)";
1278
+ function applyCurrentSessionSelection(args) {
1279
+ if (!args.current) return args;
1280
+ if (args.refs.length > 0) throw new UsageError("--current names the caller's own session; pass --current or a ref, not both");
1281
+ if (args.last > 0) throw new UsageError("--current cannot be combined with --last");
1282
+ const named = args.harnessExplicit ? resolveAdapter(args.harness)?.harness ?? args.harness : void 0;
1283
+ if (named !== void 0 && named !== "codex" && named !== "claude-code") {
1284
+ throw new UsageError(
1285
+ `--current reads ${CURRENT_VARIABLES}; ${named} sets neither. Name this session with \`traces list --cwd . --last 1\``
1286
+ );
723
1287
  }
724
- const selected = resolveAdapter(args.harness);
725
- const supportsTaskTurns = selected?.harness === "codex" || selected?.harness === "claude-code";
726
- if (args.latestTurn && (args.all || !supportsTaskTurns)) {
727
- throw new Error("--latest-turn requires --harness codex or claude-code");
1288
+ const caller = callerSession(named);
1289
+ if (!caller) {
1290
+ throw new UsageError(
1291
+ `--current needs ${CURRENT_VARIABLES}, and neither is set. On another harness, \`traces list --cwd . --last 1\` names this session`
1292
+ );
1293
+ }
1294
+ return { ...args, refs: [caller.sessionId], input: caller.sessionId, harness: caller.harness, harnessExplicit: true };
1295
+ }
1296
+ function supportsTaskTurns(adapter) {
1297
+ return adapter?.harness === "codex" || adapter?.harness === "claude-code";
1298
+ }
1299
+ function validateLatestTurn(args) {
1300
+ if (args.latestTurn && args.harnessExplicit && !supportsTaskTurns(resolveAdapter(args.harness))) {
1301
+ throw new UsageError("--latest-turn reads Codex and Claude Code sessions; pass --harness codex or claude-code, or no --harness");
728
1302
  }
729
1303
  return args;
730
1304
  }
1305
+ async function resolveSelection(args) {
1306
+ if (args.refs.length === 0) return void 0;
1307
+ const resolved = await resolveRefs(args.refs, { harness: args.harnessExplicit ? args.harness : void 0 });
1308
+ const sessions = [];
1309
+ const spans = [];
1310
+ let home;
1311
+ for (const ref of resolved) {
1312
+ if (ref.kind === "session") sessions.push({ adapter: ref.adapter, ref: { ...ref.session, cwd: ref.session.cwd ?? args.cwd ?? null } });
1313
+ else if (ref.kind === "store-root") home = ref.path;
1314
+ else spans.push(ref);
1315
+ }
1316
+ if (args.latestTurn) {
1317
+ if (spans[0]) throw new UsageError(`--latest-turn reads harness sessions; ${spans[0].ref} is a span source`);
1318
+ const other = sessions.find(({ adapter }) => !supportsTaskTurns(adapter));
1319
+ if (other) throw new UsageError(`--latest-turn reads Codex and Claude Code sessions; ${other.ref.sessionId} is ${other.adapter.harness}`);
1320
+ const narrowed = args.harnessExplicit && supportsTaskTurns(resolveAdapter(args.harness));
1321
+ if (home !== void 0 && sessions.length === 0 && !narrowed) {
1322
+ throw new UsageError("--latest-turn over a store root requires --harness codex or claude-code");
1323
+ }
1324
+ }
1325
+ return { sessions, spans, ...home !== void 0 ? { home } : {} };
1326
+ }
1327
+ async function withSelection(args) {
1328
+ if (args.selection || args.refs.length === 0) return args;
1329
+ return { ...args, selection: await resolveSelection(args) };
1330
+ }
731
1331
  async function confirm(question) {
732
1332
  if (!process.stdin.isTTY) return false;
733
1333
  const rl = (await import("readline/promises")).createInterface({
@@ -742,116 +1342,174 @@ async function confirm(question) {
742
1342
  }
743
1343
  }
744
1344
  function adaptersFor(args) {
745
- return selectAdapters({ all: args.all, harnesses: [args.harness] });
746
- }
747
- async function discover(args) {
748
- const sinceMs = args.since ? parseSince(args.since) : void 0;
749
- const out = [];
750
- for (const adapter of adaptersFor(args)) {
751
- let refs = await locateSessions(adapter, { cwd: args.cwd, sinceMs });
752
- if (args.last > 0) refs = refs.slice(0, args.last);
753
- out.push({ adapter, refs });
754
- }
755
- return out;
1345
+ let adapters = args.harnessExplicit ? selectAdapters({ harnesses: [args.harness] }) : [...listAdapters()];
1346
+ if (args.latestTurn && !args.harnessExplicit) adapters = adapters.filter(supportsTaskTurns);
1347
+ return args.selection?.home !== void 0 ? adapters.filter(readsStoreRoots) : adapters;
756
1348
  }
757
- function missingPath(error) {
758
- return typeof error === "object" && error !== null && error.code === "ENOENT";
1349
+ async function discover(args, defaultLast) {
1350
+ return narrow(args, await locateAll(args, args.since ? parseSince(args.since) : void 0), defaultLast);
759
1351
  }
760
- async function resolveSelectedSession(args) {
761
- if (!args.session) throw new Error("resolveSelectedSession requires --session");
762
- const adapter = resolveAdapter(args.harness);
763
- if (!adapter && !args.all) throw new Error(`unknown harness "${args.harness}"`);
764
- try {
765
- const st = await stat(args.session);
766
- if (!adapter) {
767
- throw new Error("--session <path> requires --harness when --all is set");
1352
+ async function locateAll(args, sinceMs) {
1353
+ const sessions = [];
1354
+ const unread = [];
1355
+ for (const adapter of adaptersFor(args)) {
1356
+ try {
1357
+ for (const ref of await locateSessions(adapter, { sinceMs, home: args.selection?.home })) sessions.push({ adapter, ref });
1358
+ } catch (error) {
1359
+ unread.push({ sessionId: null, traceId: null, source: `${adapter.harness} sessions`, reason: errorText(error) });
768
1360
  }
769
- return {
770
- adapter,
771
- ref: {
772
- harness: adapter.harness,
773
- sessionId: args.session,
774
- path: args.session,
775
- // --session is an explicit file; honor --cwd so adoption can find the
776
- // project's current or legacy skill-run log (locate() infers cwd in the scan path).
777
- cwd: args.cwd ?? null,
778
- mtimeMs: st.mtimeMs
779
- }
780
- };
781
- } catch (error) {
782
- if (!missingPath(error)) throw error;
783
1361
  }
784
- const matches = [];
785
- for (const candidate of args.all ? adaptersFor(args) : [adapter]) {
786
- const refs = await locateSessions(candidate, { cwd: args.cwd });
787
- for (const ref of refs) {
788
- if (ref.sessionId === args.session) matches.push({ adapter: candidate, ref });
1362
+ return { sessions: sessions.sort((a, b) => b.ref.mtimeMs - a.ref.mtimeMs), unread };
1363
+ }
1364
+ async function narrow(args, located, defaultLast) {
1365
+ const sinceMs = args.since ? parseSince(args.since) : void 0;
1366
+ let sessions = sinceMs === void 0 ? located.sessions : located.sessions.filter(({ ref }) => ref.mtimeMs >= sinceMs);
1367
+ if (args.cwd) {
1368
+ const kept = /* @__PURE__ */ new Set();
1369
+ for (const adapter of new Set(sessions.map((session) => session.adapter))) {
1370
+ const refs = sessions.filter((session) => session.adapter === adapter).map((session) => session.ref);
1371
+ for (const ref of await sessionsInCwd(adapter, refs, args.cwd)) kept.add(ref);
789
1372
  }
1373
+ sessions = sessions.filter(({ ref }) => kept.has(ref));
790
1374
  }
791
- if (matches.length === 1) return matches[0];
792
- if (matches.length === 0) {
793
- throw new Error(
794
- `no ${args.all ? "selected" : adapter.harness} session with ID "${args.session}"; run \`traces list\` to select an ID, or pass an explicit session path`
795
- );
1375
+ const last = args.last > 0 ? args.last : args.since ? void 0 : defaultLast;
1376
+ if (last === void 0) return { sessions, unread: located.unread };
1377
+ const caller = callerSession()?.sessionId;
1378
+ return { sessions: sessions.filter(({ ref }) => ref.sessionId !== caller).slice(0, last), unread: located.unread };
1379
+ }
1380
+ function groupsInOrder(sessions) {
1381
+ const groups = [];
1382
+ for (const { adapter, ref } of sessions) {
1383
+ const last = groups[groups.length - 1];
1384
+ if (last?.adapter === adapter) last.refs.push(ref);
1385
+ else groups.push({ adapter, refs: [ref] });
796
1386
  }
797
- throw new AmbiguousTraceInputError(
798
- `session ID "${args.session}" is ambiguous across ${matches.length} files; select one harness or pass its explicit session path`
799
- );
1387
+ return groups;
1388
+ }
1389
+ function errorText(error) {
1390
+ return error instanceof Error ? error.message : String(error);
800
1391
  }
801
- function llmAnalysisEngine(args, model) {
802
- return analysisEngineFromEnv({ model, maxCostUsd: args.budget, log: analystLog });
1392
+ function warnUnread(args, unread, command = "facts") {
1393
+ if (unread.length === 0) return;
1394
+ const select = args.current ? ["--current"] : args.refs.map(shellWord);
1395
+ if (args.harnessExplicit && !args.current) select.push("--harness", args.harness);
1396
+ if (args.since) select.push("--since", shellWord(args.since));
1397
+ if (args.until) select.push("--until", shellWord(args.until));
1398
+ if (args.last > 0) select.push("--last", String(args.last));
1399
+ if (args.cwd) select.push("--cwd", shellWord(args.cwd));
1400
+ if (args.latestTurn) select.push("--latest-turn");
1401
+ process.stderr.write(`${unread.length} unreadable: ${["traces", command, ...select].join(" ")} --format json | jq .selection
1402
+ `);
803
1403
  }
804
1404
  function analystModelFor(args) {
805
1405
  return args.model ?? process.env.TRACES_ANALYST_MODEL ?? DEFAULT_ANALYST_MODEL;
806
1406
  }
807
- function requiredBridgeVersion() {
808
- const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
809
- const version = pkg.dependencies?.["@tangle-network/agent-eval"];
810
- if (typeof version !== "string" || !version) {
811
- throw new Error("package.json is missing the @tangle-network/agent-eval dependency");
812
- }
813
- return version;
814
- }
815
- function assertRequestedAnalysesRan(args, result) {
816
- const messages = [];
817
- if (args.llm && totalAgenticFailureMessage(result.agenticPerAnalyst)) {
818
- messages.push(totalAgenticFailureMessage(result.agenticPerAnalyst, { requiredBridgeVersion: requiredBridgeVersion() }));
1407
+ var PROMPT_CHARS = 80;
1408
+ function oneLine(text, chars) {
1409
+ const flat2 = text.replace(/\s+/gu, " ").trim();
1410
+ return flat2.length <= chars ? flat2 : `${flat2.slice(0, chars - 1)}\u2026`;
1411
+ }
1412
+ function formatDuration(ms) {
1413
+ if (ms === null) return "?";
1414
+ const seconds = Math.round(ms / 1e3);
1415
+ if (seconds < 60) return `${seconds}s`;
1416
+ const minutes = Math.floor(seconds / 60);
1417
+ if (minutes < 60) return `${minutes}m ${seconds % 60}s`;
1418
+ return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
1419
+ }
1420
+ async function listRow(adapter, ref, shortRef, session) {
1421
+ const first = session.firstAt;
1422
+ const last = session.lastAt;
1423
+ let bytes = null;
1424
+ try {
1425
+ bytes = (await stat(ref.path)).size;
1426
+ } catch {
819
1427
  }
820
- const external = externalFailureMessage(result.external);
821
- if (external) messages.push(external);
822
- if (messages.length > 0) throw new Error(messages.join("\n"));
1428
+ return {
1429
+ ref: shortRef,
1430
+ id: ref.sessionId,
1431
+ harness: adapter.harness,
1432
+ path: ref.path,
1433
+ bytes,
1434
+ parentSessionId: session.parentSessionId,
1435
+ lastActivity: last ?? new Date(ref.mtimeMs).toISOString(),
1436
+ durationMs: first !== null && last !== null ? Date.parse(last) - Date.parse(first) : null,
1437
+ humanTurns: session.humanTurns.length,
1438
+ cwd: session.cwd ?? ref.cwd,
1439
+ firstPrompt: session.firstPrompt === null ? null : oneLine(session.firstPrompt, PROMPT_CHARS)
1440
+ };
823
1441
  }
824
1442
  async function cmdList(args) {
825
- const groups = await discover({ ...args, last: args.last || 20 });
826
- for (const { adapter, refs } of groups) {
827
- console.log(`
828
- ${adapter.harness}: ${refs.length} session(s)`);
829
- for (const r of refs) {
830
- console.log(` ${new Date(r.mtimeMs).toISOString()} ${r.sessionId} ${r.cwd ?? ""}`);
1443
+ const named = args.selection?.sessions[0]?.ref.sessionId ?? args.selection?.spans[0]?.ref;
1444
+ if (named !== void 0) {
1445
+ throw new UsageError(`list finds sessions; its only ref is a store root, and ${named} is not one (read it with \`traces facts ${named}\`)`);
1446
+ }
1447
+ const every = await locateAll(args);
1448
+ const shortRefs = shortestRefs(every.sessions.map(({ ref }) => ref.sessionId));
1449
+ const found = await narrow(args, every, 20);
1450
+ const rows = [];
1451
+ const unread = [...found.unread];
1452
+ const stats = await TurnIndex.with(async (index) => {
1453
+ const refreshed = await index.refresh(found.sessions);
1454
+ for (const { adapter, ref } of found.sessions) {
1455
+ const held = index.session(adapter, ref);
1456
+ if (!held || held.unread !== null) {
1457
+ unread.push({ sessionId: ref.sessionId, traceId: null, source: ref.path, reason: held?.unread ?? "not indexed" });
1458
+ continue;
1459
+ }
1460
+ rows.push(await listRow(adapter, ref, shortRefs.get(ref.sessionId) ?? ref.sessionId, held.session));
831
1461
  }
1462
+ return refreshed;
1463
+ });
1464
+ warnUnread(args, unread, "list");
1465
+ if (args.format === "json") {
1466
+ process.stdout.write(`${JSON.stringify({ sessions: rows, selection: { unreadSessions: unread, index: stats } }, null, 2)}
1467
+ `);
1468
+ return;
832
1469
  }
1470
+ const footer = `${rows.length} session${rows.length === 1 ? "" : "s"} \xB7 ${unread.length} unreadable`;
1471
+ const lines = rows.map((row) => [
1472
+ row.ref.padEnd(8),
1473
+ row.harness.padEnd(11),
1474
+ row.lastActivity.slice(0, 16).replace("T", " "),
1475
+ formatDuration(row.durationMs).padStart(7),
1476
+ `${row.humanTurns ?? "?"} turn${row.humanTurns === 1 ? "" : "s"}`.padEnd(9),
1477
+ row.cwd ?? "(no cwd)",
1478
+ row.firstPrompt ? `"${row.firstPrompt}"` : ""
1479
+ ].join(" ").trimEnd());
1480
+ process.stdout.write(`${[...lines, footer].join("\n")}
1481
+ `);
833
1482
  }
834
- async function collectOtlpSpans(path) {
835
- const input = await readOtlpInput(path);
1483
+ async function collectOtlpSpans(path, selector) {
1484
+ const { sessionReportSource } = await import("./report-JAMXRTCW.js");
1485
+ let input;
1486
+ try {
1487
+ input = await readOtlpInput(path);
1488
+ } catch (error) {
1489
+ if (!(await stat(path).catch(() => void 0))?.isDirectory()) throw error;
1490
+ throw new Error(
1491
+ `${errorText(error)}. A directory ref is read as OTLP-JSONL unless it is a Runtime run (spawn-journal.jsonl, observer.jsonl or failure.json), a harness store root, or a bundle (manifest.json)`
1492
+ );
1493
+ }
836
1494
  if (input.spans.length === 0) {
837
1495
  throw new Error(
838
- `no analyzable spans in ${path} (${input.files.length} file(s), ${input.rows.length} row(s), ${input.issues.length} unusable). Run \`traces validate ${path}\` for the per-finding reason.`
1496
+ `no analyzable spans in ${path} (${input.files.length} file(s), ${input.rows.length} row(s), ${input.issues.length} unusable). Run \`traces check ${path} --contract conformance\` for the per-finding reason.`
839
1497
  );
840
1498
  }
841
1499
  const fileByTrace = /* @__PURE__ */ new Map();
842
1500
  for (const file of input.files) {
843
1501
  for (const traceId of file.traceIds) if (!fileByTrace.has(traceId)) fileByTrace.set(traceId, file.path);
844
1502
  }
1503
+ const selected = selector ? selectSpans(input.spans, selector, path) : input.spans;
845
1504
  const byTrace = /* @__PURE__ */ new Map();
846
- for (const span of input.spans) {
1505
+ for (const span of selected) {
847
1506
  const grouped = byTrace.get(span.trace_id) ?? [];
848
1507
  grouped.push(span);
849
1508
  byTrace.set(span.trace_id, grouped);
850
1509
  }
851
1510
  return {
852
- spans: [...input.spans],
1511
+ spans: [...selected],
853
1512
  harness: "otlp",
854
- cwds: [],
855
1513
  sources: [...byTrace].map(([traceId, spans]) => sessionReportSource({
856
1514
  harness: "otlp",
857
1515
  sessionId: traceId,
@@ -869,128 +1527,189 @@ async function collectOtlpSpans(path) {
869
1527
  };
870
1528
  }
871
1529
  async function collectSpans(args) {
872
- if (args.sourceBundle) {
873
- if (!["analyze", "investigate", "improve", "ask"].includes(args.command)) throw new Error("--source-bundle is an analysis option");
874
- if (args.session || args.otlp || args.input || args.current || args.workflow || args.noContent || args.redactorCmd) {
875
- throw new Error("--source-bundle cannot be combined with another input, redaction, or metadata-only selection");
876
- }
877
- for (const output of [args.out, args.dir, args.otlpOut]) {
878
- if (output) await assertOutsideSourceBundle(args.sourceBundle, output);
879
- }
880
- return collectOtlpSpans(join2(args.sourceBundle, "derived", "trace.otlp.jsonl"));
1530
+ if (args.refs.length > 0 && !args.selection) throw new Error(`internal: ${args.command} read its refs before resolving them`);
1531
+ const sources = args.selection?.spans ?? [];
1532
+ if (sources.length === 0) {
1533
+ const collected2 = await collectSessionSpans(args);
1534
+ warnUnread(args, collected2.unread ?? []);
1535
+ return collected2;
1536
+ }
1537
+ const parts = [];
1538
+ for (const source of sources) {
1539
+ parts.push(source.kind === "run-dir" ? await collectOtlpSpans(source.path) : source.reader === "otlp" ? await collectOtlpSpans(source.path, source.selector) : await collectImportedSpans(source.path, source.reader, source.selector));
1540
+ }
1541
+ if (args.selection?.sessions.length) parts.push(await collectSessionSpans(args));
1542
+ const collected = parts.length === 1 ? parts[0] : mergeCollected(parts);
1543
+ warnUnread(args, collected.unread ?? []);
1544
+ return collected;
1545
+ }
1546
+ async function forEachSpanBatch(args, onBatch) {
1547
+ let spanCount = 0;
1548
+ const counted = async (spans, harness) => {
1549
+ spanCount += spans.length;
1550
+ await onBatch(spans, harness);
1551
+ };
1552
+ if ((args.selection?.spans.length ?? 0) === 0) {
1553
+ const collected2 = await collectSessionSpans(args, counted);
1554
+ warnUnread(args, collected2.unread ?? []);
1555
+ return { ...collected2, spanCount };
881
1556
  }
882
- if (args.otlp) return collectOtlpSpans(args.otlp);
883
- if (args.input) {
884
- if (args.workflow) throw new Error("--workflow reads discovered sessions and cannot be combined with an input file");
885
- return collectImportedSpans(args);
1557
+ const collected = await collectSpans(args);
1558
+ if (collected.spans.length > 0) await counted(collected.spans, collected.harness);
1559
+ return { ...collected, spans: [], spanCount };
1560
+ }
1561
+ function mergeCollected(parts) {
1562
+ const spans = [];
1563
+ const sources = [];
1564
+ const unread = [];
1565
+ const workflows = parts.flatMap((part) => part.workflow ? [part.workflow] : []);
1566
+ for (const part of parts) {
1567
+ appendAll(spans, part.spans);
1568
+ appendAll(sources, part.sources);
1569
+ appendAll(unread, part.unread ?? []);
886
1570
  }
887
- const selection = await collectSessionRows(args, false, args.last || 1);
1571
+ return {
1572
+ spans,
1573
+ harness: [...new Set(parts.flatMap((part) => part.harness.split("+")))].join("+"),
1574
+ sources,
1575
+ ...workflows[0] ? { workflow: workflows[0] } : {},
1576
+ ...unread.length > 0 ? { unread } : {}
1577
+ };
1578
+ }
1579
+ async function collectSessionSpans(args, onBatch) {
1580
+ const { sessionReportSource } = await import("./report-JAMXRTCW.js");
888
1581
  const spans = [];
889
1582
  const harnesses = /* @__PURE__ */ new Set();
890
- const cwds = [];
891
1583
  const sources = [];
892
- for (const row of selection.rows) {
1584
+ const sweep = onBatch !== void 0 && args.since !== void 0 && (args.selection?.sessions.length ?? 0) === 0;
1585
+ const take = (row) => {
893
1586
  harnesses.add(row.adapter.harness);
894
- appendAll(spans, row.spans);
895
1587
  sources.push(sessionReportSource(row.ref, row.spans));
896
- if (row.ref.cwd) cwds.push(row.ref.cwd);
1588
+ };
1589
+ const selection = await collectSessionRows(args, args.last || 1, sweep ? async (row) => {
1590
+ take(row);
1591
+ await onBatch(row.spans, row.adapter.harness);
1592
+ } : void 0);
1593
+ for (const row of selection.rows) {
1594
+ take(row);
1595
+ appendAll(spans, row.spans);
897
1596
  }
1597
+ const harness = [...harnesses].join("+") || (args.harnessExplicit ? args.harness : "none");
1598
+ if (onBatch && !sweep && spans.length > 0) await onBatch(spans.splice(0), harness);
898
1599
  return {
899
1600
  spans,
900
- harness: [...harnesses].join("+") || args.harness,
901
- cwds,
1601
+ harness,
902
1602
  sources,
903
- ...selection.workflow ? { workflow: selection.workflow } : {}
1603
+ ...selection.workflow ? { workflow: selection.workflow } : {},
1604
+ ...selection.unread.length > 0 ? { unread: selection.unread } : {}
904
1605
  };
905
1606
  }
906
- async function cmdValidate(args) {
907
- const path = args.otlp ?? args.input;
908
- if (!path) {
909
- throw new Error("validate needs an OTLP-JSONL file or directory: `traces validate <file|dir>`");
910
- }
911
- const input = await readOtlpInput(path);
912
- const report = renderValidation(input.validation, {
913
- subject: path,
914
- files: input.files,
915
- skipped: input.skipped,
916
- issues: input.issues,
917
- withheld: input.withheld,
918
- unreadable: input.unreadable,
919
- spans: input.spans
920
- });
921
- const validation = input.validation;
922
- if (args.out) {
923
- await saveReport(args.out, report);
924
- console.log(
925
- `conformance report \u2192 ${args.out} (${validation.findings.length} finding(s), ${validation.capabilities.filter((capability) => capability.available).length}/${validation.capabilities.length} capabilities available)`
926
- );
927
- } else {
928
- console.log(report);
929
- }
930
- process.exitCode = validationExitCode(validation);
931
- }
932
- var EVIDENCE_FORMATS = /* @__PURE__ */ new Set(["policy-evidence", "sandbox-events", "openinference", "intelligence-spans", "chat-trajectory"]);
933
- async function checkInputArgs(args) {
934
- if (!args.input) return args;
935
- if (args.otlp || args.session) throw new Error("name the trace once: a positional path, --otlp, or --session");
936
- const info = await stat(args.input);
937
- let kind;
938
- if (args.format && EVIDENCE_FORMATS.has(args.format)) kind = "evidence";
939
- else if (args.format === "otlp" || args.format === "claude-code" || args.format === "codex") kind = args.format;
940
- else if (args.format) throw new Error(`unknown --format "${args.format}" (otlp, claude-code, codex, or an evidence format)`);
941
- else kind = info.isDirectory() ? "otlp" : await sniffCheckInput(args.input);
942
- if (kind === "otlp") return { ...args, otlp: args.input, input: void 0 };
943
- if (kind === "evidence") return args;
944
- return { ...args, harness: kind, harnessExplicit: true, session: args.input, input: void 0 };
945
- }
946
1607
  async function cmdCheck(args) {
947
- if (!args.contract) {
948
- process.stderr.write("error: check needs --contract <contract.json>\n");
949
- process.exitCode = CHECK_EXIT.badContract;
950
- return;
1608
+ const check = await import("./check-YF6ZHPGG.js");
1609
+ const { explainTraceContract } = await import("@tangle-network/agent-eval");
1610
+ const contracts = /* @__PURE__ */ new Map();
1611
+ for (const path of args.contracts) {
1612
+ if (path === check.CONFORMANCE || contracts.has(path)) continue;
1613
+ try {
1614
+ contracts.set(path, await check.loadContract(path));
1615
+ } catch (error) {
1616
+ throw new UsageError(errorText(error));
1617
+ }
951
1618
  }
952
- let loaded;
953
- try {
954
- loaded = await loadContract(args.contract);
955
- } catch (error) {
956
- process.stderr.write(`error: ${error instanceof Error ? error.message : String(error)}
1619
+ for (const loaded of contracts.values()) for (const warning of loaded.warnings) process.stderr.write(`warning: ${warning}
1620
+ `);
1621
+ const traceGates = args.contracts.length > 0;
1622
+ const selected = args.refs.length > 0 || args.current || args.last > 0 || args.since !== void 0;
1623
+ if (traceGates && !selected && !args.shareSafe) {
1624
+ const explained = args.contracts.map((path) => {
1625
+ const loaded = contracts.get(path);
1626
+ return loaded ? { contract: loaded.contract.name, text: explainTraceContract(loaded.contract) } : { contract: check.CONFORMANCE, text: check.CONFORMANCE_EXPLAINED };
1627
+ });
1628
+ process.stdout.write(args.format === "json" ? `${JSON.stringify({ explain: explained }, null, 2)}
1629
+ ` : `${explained.map((entry) => entry.text).join("\n\n")}
957
1630
  `);
958
- process.exitCode = CHECK_EXIT.badContract;
959
1631
  return;
960
1632
  }
961
- for (const warning of loaded.warnings) process.stderr.write(`warning: ${warning}
1633
+ const gates = [];
1634
+ if (traceGates) {
1635
+ const resolved = await withSelection(args);
1636
+ const sources = resolved.selection?.spans ?? [];
1637
+ const otlpOnly = resolved.selection !== void 0 && resolved.selection.sessions.length === 0 && resolved.selection.home === void 0 && sources.every((source) => source.kind === "run-dir" || source.reader === "otlp");
1638
+ let run;
1639
+ const runSpans = async () => {
1640
+ if (run) return run;
1641
+ try {
1642
+ run = await collectSpans(resolved);
1643
+ } catch (error) {
1644
+ if (error instanceof RefError || error instanceof CliError) throw error;
1645
+ throw new CliError(errorText(error), error instanceof check.AmbiguousTraceInputError ? "ambiguous" : "unreadable");
1646
+ }
1647
+ if (run.spans.length === 0) throw new CliError("the trace holds no spans", "unreadable");
1648
+ return run;
1649
+ };
1650
+ for (const path of args.contracts) {
1651
+ if (path !== check.CONFORMANCE) {
1652
+ const report = await check.runCheck(contracts.get(path).contract, (await runSpans()).spans, args.out === void 0 ? {} : { evidenceRoot: args.out });
1653
+ gates.push(check.contractGate(report));
1654
+ } else if (otlpOnly) {
1655
+ for (const source of sources) gates.push(await otlpConformanceGate(check, source.ref, source.path));
1656
+ } else {
1657
+ const { spans } = await runSpans();
1658
+ const subject = args.refs.length > 0 ? args.refs.join(" ") : "the selected sessions";
1659
+ const validation = conformanceOfSpans(spans);
1660
+ gates.push(check.conformanceGate(subject, validation, renderValidation(validation, { subject, spans })));
1661
+ }
1662
+ }
1663
+ }
1664
+ if (args.shareSafe) {
1665
+ const { verifySafe } = await import("./verify-safe-C5ZXZOZ2.js");
1666
+ const knownSecrets = args.knownSecretEnvs.map((name) => {
1667
+ const value = process.env[name];
1668
+ if (!value) throw new UsageError(`--known-secret-env ${name || "(missing name)"}: that environment variable is not set`);
1669
+ return value;
1670
+ });
1671
+ for (const path of args.refs) gates.push(check.shareSafeGate(await verifySafe(path, { knownSecrets })));
1672
+ }
1673
+ const verdict = check.combineGates(gates);
1674
+ if (args.out !== void 0) await check.writeGateEvidence(args.out, verdict);
1675
+ const output = args.format === "json" ? JSON.stringify(verdict, null, 2) : args.format === "junit" ? check.renderVerdictJUnit(verdict).trimEnd() : args.format === "github" ? check.renderVerdictGithub(verdict) : check.renderVerdictText(verdict);
1676
+ if (output) process.stdout.write(`${output}
962
1677
  `);
963
- if (args.explain) {
964
- console.log(explainTraceContract2(loaded.contract));
965
- return;
1678
+ if (verdict.exitCode === check.CHECK_EXIT.couldNotDecide) {
1679
+ const undecided = verdict.gates.filter((gate) => gate.status === "unknown").map((gate) => gate.gate);
1680
+ writeError(`${undecided.join(", ")} could not decide; the verdict on stdout says why`, "unknown-verdict", args.format === "json");
966
1681
  }
967
- let spans;
1682
+ process.exitCode = verdict.exitCode;
1683
+ }
1684
+ async function otlpConformanceGate(check, subject, path) {
1685
+ let input;
968
1686
  try {
969
- spans = (await collectSpans(await checkInputArgs(args))).spans;
970
- if (spans.length === 0) throw new Error("the trace holds no spans");
1687
+ input = await readOtlpInput(path);
971
1688
  } catch (error) {
972
- process.stderr.write(`error: ${error instanceof Error ? error.message : String(error)}
973
- `);
974
- process.exitCode = error instanceof AmbiguousTraceInputError ? CHECK_EXIT.ambiguous : CHECK_EXIT.unreadable;
975
- return;
976
- }
977
- const report = await runCheck(loaded.contract, spans, { evidenceRoot: args.evidence ?? join2(".traces", "check") });
978
- console.log(renderCheckText(report));
979
- if (args.junit) await writeFile2(args.junit, renderJUnit(report), "utf8");
980
- if (args.out) await writeFile2(args.out, `${JSON.stringify(report, null, 2)}
981
- `, "utf8");
982
- if (args.annotations || process.env.GITHUB_ACTIONS === "true") {
983
- const annotations = renderGithubAnnotations(report);
984
- if (annotations) console.log(annotations);
1689
+ throw new CliError(errorText(error), "unreadable");
985
1690
  }
986
- process.exitCode = report.exitCode;
1691
+ const text = renderValidation(input.validation, {
1692
+ subject,
1693
+ files: input.files,
1694
+ skipped: input.skipped,
1695
+ issues: input.issues,
1696
+ withheld: input.withheld,
1697
+ unreadable: input.unreadable,
1698
+ spans: input.spans
1699
+ });
1700
+ return check.conformanceGate(subject, input.validation, text);
987
1701
  }
988
1702
  async function cmdConvert(args) {
989
- const { spans, workflow } = await collectSpans(args);
990
- if (spans.length === 0) throw new Error("no spans found for the given selection");
991
- warnIncompleteWorkflow(workflow);
992
- const path = await writeOtlpFile(spans, args.otlpOut);
993
- console.log(`wrote ${spans.length} spans \u2192 ${path}`);
1703
+ const collected = await collectSpans(args);
1704
+ if (collected.spans.length === 0) throw new Error("no spans found for the given selection");
1705
+ warnIncompleteWorkflow(collected.workflow);
1706
+ const { spans, dropped } = args.noContent ? metadataOnly(collected.spans) : { spans: collected.spans, dropped: [] };
1707
+ if (args.out === void 0) {
1708
+ for (const line of iterSerializedSpans(spans)) process.stdout.write(line);
1709
+ return;
1710
+ }
1711
+ const out = await writeOtlpFile(spans, args.out);
1712
+ console.log(args.format === "json" ? JSON.stringify({ out, spans: spans.length, droppedAttributes: dropped }) : `wrote ${spans.length} spans \u2192 ${out}${args.noContent ? ` (metadata only: ${dropped.length} attribute key(s) dropped)` : ""}`);
994
1713
  }
995
1714
  function workflowIssueText(issue) {
996
1715
  if (issue.kind === "cycle") return `cycle: ${issue.sessionIds.join(" -> ")}`;
@@ -1005,442 +1724,340 @@ function workflowIssueText(issue) {
1005
1724
  }
1006
1725
  return `${issue.kind} ${issue.sessionId}, referenced as ${issue.relation} by ${issue.referencedBySessionId}`;
1007
1726
  }
1727
+ var WORKFLOW_ISSUES_NAMED = 3;
1008
1728
  function warnIncompleteWorkflow(workflow) {
1009
1729
  if (!workflow || workflow.complete) return;
1730
+ const { issues } = workflow;
1731
+ const more = issues.length - WORKFLOW_ISSUES_NAMED;
1010
1732
  process.stderr.write(
1011
- `warning: session workflow is incomplete (${workflow.issues.length} issue(s)): ${workflow.issues.map(workflowIssueText).join("; ")}
1733
+ `warning: session workflow is incomplete (${issues.length} issue(s)): ${issues.slice(0, WORKFLOW_ISSUES_NAMED).map(workflowIssueText).join("; ")}${more > 0 ? `; and ${more} more` : ""}
1012
1734
  `
1013
1735
  );
1014
1736
  }
1015
- async function collectSessionRows(args, bindExplicitSource = false, defaultLast = 20) {
1016
- const groups = args.session ? [await resolveSelectedSession(args).then(({ adapter, ref }) => ({ adapter, refs: [ref] }))] : await discover({ ...args, last: args.last || defaultLast });
1017
- return collectSessionSelection(groups, {
1018
- workflow: args.workflow,
1019
- maxWorkflowSessions: args.maxWorkflowSessions,
1737
+ async function collectSessionRows(args, defaultLast, onRow) {
1738
+ const named = args.selection?.sessions ?? [];
1739
+ const source = args.selection?.spans[0];
1740
+ if (source && named.length === 0) {
1741
+ throw new Error(`${args.command} reads harness sessions; ${source.ref} is a span source`);
1742
+ }
1743
+ const found = named.length > 0 ? { sessions: named, unread: [] } : await discover(args, defaultLast);
1744
+ const unread = [...found.unread];
1745
+ const selection = await collectSessionSelection(groupsInOrder(found.sessions), {
1746
+ workflow: named.length > 0 || !args.since,
1747
+ parents: false,
1748
+ maxWorkflowSessions: MAX_CHILD_SESSIONS,
1020
1749
  taskScope: args.latestTurn ? "latest" : "all",
1021
- bindSources: bindExplicitSource && Boolean(args.session)
1022
- });
1023
- }
1024
- async function cmdEvidence(args) {
1025
- const selection = await collectSessionRows(args, true);
1026
- const rows = selection.rows.filter((row) => row.spans.length > 0);
1027
- if (rows.length === 0) throw new Error("no spans found for the given selection");
1028
- warnIncompleteWorkflow(selection.workflow);
1029
- const otlpPath = args.otlpOut ? await writeOtlpFile(rows.flatMap((row) => row.spans), args.otlpOut) : void 0;
1030
- const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
1031
- const records = await Promise.all(rows.map(
1032
- (row) => buildPolicyEvidenceRecord(row.ref, row.spans, {
1033
- generatedAt,
1034
- minLoopOccurrences: args.minLoop,
1035
- maxLoopExamples: 25,
1036
- otlpPath,
1037
- sourceSha256: row.sourceSha256
1038
- })
1039
- ));
1040
- if (args.out) {
1041
- const path = await writePolicyEvidenceFile(records, args.out);
1042
- console.log(`policy evidence \u2192 ${path} (${records.length} session rows${otlpPath ? `, OTLP: ${otlpPath}` : ""})`);
1043
- } else {
1044
- process.stdout.write(serializePolicyEvidence(records));
1045
- }
1046
- }
1047
- async function cmdIndex(args) {
1048
- const selection = await collectSessionRows(args);
1049
- const rows = selection.rows.filter((row) => row.spans.length > 0);
1050
- if (rows.length === 0) throw new Error("no spans found for the given selection");
1051
- warnIncompleteWorkflow(selection.workflow);
1052
- const index = await buildSessionIndexFromRows(rows, {
1053
- minLoopOccurrences: args.minLoop,
1054
- selection: {
1055
- command: "index",
1056
- harness: args.harnessExplicit ? args.harness : void 0,
1057
- all: args.all || void 0,
1058
- last: args.last || void 0,
1059
- session: args.session,
1060
- cwd: args.cwd,
1061
- since: args.since,
1062
- latestTurn: args.latestTurn || void 0,
1063
- ...selection.workflow ? { workflow: selection.workflow } : {}
1750
+ home: args.selection?.home,
1751
+ ...onRow ? { onRow } : {},
1752
+ onUnreadable: ({ ref, error, seed }) => {
1753
+ if (seed && named.length > 0) throw new RefError(`${ref.sessionId} could not be read (${ref.path}): ${errorText(error)}`, "unreadable");
1754
+ unread.push({ sessionId: ref.sessionId, traceId: null, source: ref.path, reason: errorText(error) });
1064
1755
  }
1065
1756
  });
1066
- if (args.out) {
1067
- const path = await writeSessionIndexFile(index, args.out);
1068
- console.log(`session index \u2192 ${path} (${index.totals.sessions} session rows)`);
1069
- } else {
1070
- process.stdout.write(serializeSessionIndex(index));
1071
- }
1757
+ return { ...selection, unread };
1072
1758
  }
1073
1759
  async function cmdBundle(args) {
1074
- if (!args.session) {
1075
- throw new Error("bundle needs --session <id|path>; run `traces list` to pick a session ID");
1076
- }
1077
- if (!args.out) throw new Error("bundle needs --out <dir> \u2014 a new or empty directory for the bundle");
1078
- const { adapter, ref } = await resolveSelectedSession(args);
1079
- const result = await assembleSessionBundle({
1080
- adapter,
1081
- ref,
1082
- outDir: args.out,
1083
- minLoopOccurrences: args.minLoop,
1084
- log: analystLog
1085
- });
1760
+ const named = args.selection;
1761
+ if (named?.sessions.length !== 1 || named.spans.length > 0) {
1762
+ throw new UsageError(
1763
+ named?.spans[0] ? `bundle keeps one harness session's evidence; ${named.spans[0].ref} is a span source` : "bundle needs one session: traces bundle <id|path> --out <dir>; run `traces list` to pick a session ID"
1764
+ );
1765
+ }
1766
+ if (!args.out) throw new UsageError("bundle needs --out <dir>: a new or empty directory for the bundle");
1767
+ const { adapter, ref } = named.sessions[0];
1768
+ const result = await assembleSessionBundle({ adapter, ref, outDir: args.out });
1086
1769
  const { manifest } = result;
1087
- console.log(
1088
- `session bundle (${manifest.view} view) \u2192 ${result.directory} (${manifest.files.length} file(s), ${manifest.ledgerSlices.length} ledger slice(s), ${manifest.absent.length} recorded absent)`
1089
- );
1770
+ console.log(`session bundle \u2192 ${result.directory} (${manifest.files.length} file(s), ${manifest.absent.length} recorded absent)`);
1090
1771
  }
1091
1772
  async function cmdMcp(args) {
1092
- if (args.help) {
1093
- process.stdout.write(`traces mcp \u2014 read-only MCP server over stdio for the selected traces
1094
-
1095
- Usage:
1096
- traces mcp --otlp <file|dir> any OTLP-JSONL
1097
- traces mcp --harness claude-code --last 5 recent harness sessions (any selection flag)
1098
-
1099
- Register with a client, for example:
1100
- claude mcp add traces -- traces mcp --otlp ./spans.otlp.jsonl
1101
-
1102
- Every tool is read-only and idempotent and says so in its MCP annotations.
1103
- Spans (including causal links) and results are redacted, the server refuses
1104
- to start if the redacted spans are still UNSAFE or UNKNOWN, each result
1105
- carries an untrusted-text notice, and a result above 512 KiB is refused
1106
- rather than truncated. readSpanSource is not served: windowed reads of
1107
- original source bytes cannot be redacted.
1108
- `);
1109
- return;
1110
- }
1111
- const selected = validateOtlpSelection(validateWorkflowSelection(applyCurrentSessionSelection(args)));
1112
- const collected = await collectSpans(selected);
1113
- if (collected.spans.length === 0) throw new Error("mcp: no spans found for the given selection");
1773
+ const { createServedSpans, serveTraceMcp } = await import("./mcp-ZPGTJQYQ.js");
1774
+ const served = createServedSpans();
1775
+ const collected = await forEachSpanBatch(args, async (spans) => served.add(spans));
1776
+ if (collected.spanCount === 0) throw new Error("mcp: no spans found for the given selection");
1114
1777
  warnIncompleteWorkflow(collected.workflow);
1115
- process.stderr.write(`traces mcp: serving ${collected.spans.length} span(s) over stdio
1778
+ process.stderr.write(`traces mcp: serving ${collected.spanCount} span(s) over stdio
1116
1779
  `);
1117
- await serveTraceMcp({ spans: collected.spans });
1780
+ await serveTraceMcp({ served: served.finish() });
1118
1781
  }
1119
- async function cmdDiff(argv) {
1120
- const inputs = [];
1121
- const kinds = [];
1122
- let format = "text";
1123
- for (let i = 0; i < argv.length; i++) {
1124
- const arg = argv[i];
1125
- if (arg === "--help" || arg === "-h") {
1126
- process.stdout.write(`traces diff \u2014 where two runs of one task first diverge
1127
-
1128
- Usage:
1129
- traces diff <a> <b> [--kind TOOL]... [--format text|json]
1130
-
1131
- Each side is an OTLP file or directory, any file convert reads, file#<trace id>,
1132
- or file#branch=<id> for one arm (agent.branch.id or agent.branch.arm, and every
1133
- span below it \u2014 refused if the label names more than one subtree). Steps pair
1134
- by span id, then position with the same name and kind, then name and kind
1135
- anywhere. Exit 0 when the runs agree on outcome, 1 when they diverge, 2 when a
1136
- side cannot be read, 3 when the steps line up with no divergence but neither
1137
- side carries a definitive OK/ERROR status or a TOOL step \u2014 there is no
1138
- recorded outcome to call agreement on.
1139
- `);
1140
- return;
1141
- }
1142
- if (arg === "--format") format = argv[++i] ?? "";
1143
- else if (arg === "--kind") kinds.push(argv[++i] ?? "");
1144
- else if (arg.startsWith("--")) throw new Error(`diff: unknown flag ${arg}`);
1145
- else inputs.push(arg);
1146
- }
1147
- if (inputs.length !== 2) {
1148
- throw new Error("diff needs two runs: traces diff <a> <b> (a file, a directory, file#<trace id> or file#branch=<id>)");
1149
- }
1150
- if (format !== "json" && format !== "text") throw new Error(`diff: --format must be json or text, got "${format}"`);
1151
- if (kinds.some((kind) => !kind)) throw new Error("diff: --kind needs a span kind, e.g. --kind TOOL");
1152
- const report = await diffRuns(inputs[0], inputs[1], { kinds });
1153
- process.stdout.write(format === "json" ? `${JSON.stringify(report, null, 2)}
1782
+ async function cmdDiff(args) {
1783
+ const { diffRuns, renderRunDiff } = await import("./diff-ZZ6KYBPP.js");
1784
+ const report = await diffRuns(args.refs[0], args.refs[1]);
1785
+ if (!report.diff.firstDivergence && report.caveat) {
1786
+ throw new CliError(`the runs' tool calls line up with no difference, but there is no outcome to call agreement on: ${report.caveat}`, "no-outcome");
1787
+ }
1788
+ process.stdout.write(args.format === "json" ? `${JSON.stringify(report, null, 2)}
1154
1789
  ` : renderRunDiff(report));
1155
1790
  if (report.diff.firstDivergence) process.exitCode = 1;
1156
- else if (report.caveat) process.exitCode = 3;
1157
1791
  }
1158
- async function cmdBundleVerify(argv) {
1159
- let dir;
1160
- let format = "text";
1161
- for (let i = 0; i < argv.length; i++) {
1162
- const arg = argv[i];
1163
- if (arg === "--format") format = argv[++i] ?? "";
1164
- else if (arg.startsWith("--")) throw new Error(`bundle verify: unknown flag ${arg}`);
1165
- else if (dir === void 0) dir = arg;
1166
- else throw new Error(`bundle verify: unexpected argument ${arg}`);
1167
- }
1168
- if (!dir) throw new Error("bundle verify needs the bundle directory: traces bundle verify <dir>");
1169
- if (format !== "json" && format !== "text") throw new Error(`bundle verify: --format must be json or text, got "${format}"`);
1170
- const result = await verifySessionBundle(dir);
1171
- if (format === "json") {
1792
+ async function cmdBundleVerify(args) {
1793
+ const result = await verifySessionBundle(args.input);
1794
+ if (args.format === "json") {
1172
1795
  process.stdout.write(`${JSON.stringify(result, null, 2)}
1173
1796
  `);
1174
1797
  } else {
1175
1798
  const lines = [
1176
- `${result.ok ? "VERIFIED" : "FAILED"} ${result.view} bundle ${result.directory}`,
1799
+ `${result.ok ? "VERIFIED" : "FAILED"} bundle ${result.directory}`,
1177
1800
  ` ${result.matched}/${result.listed} listed file(s) match their recorded size and SHA-256`,
1178
1801
  ...result.issues.map((issue) => ` ${issue.kind.padEnd(11)} ${issue.path}: ${issue.detail}`)
1179
1802
  ];
1180
- if (result.source) lines.push(` source bundle manifest: ${result.source.status} (${result.source.directory})`);
1181
1803
  process.stdout.write(`${lines.join("\n")}
1182
1804
  `);
1183
1805
  }
1184
1806
  if (!result.ok) process.exitCode = 1;
1185
1807
  }
1186
- var PROJECTABLE_VIEWS = ["evidence-only"];
1187
- async function cmdBundleView(args) {
1188
- if (!args.input) {
1189
- throw new Error("bundle-view needs the bundle directory to project; run `traces bundle --out <dir>` first");
1190
- }
1191
- if (!args.view) {
1192
- throw new Error(`bundle-view needs --view <${PROJECTABLE_VIEWS.join("|")}>`);
1193
- }
1194
- if (!PROJECTABLE_VIEWS.includes(args.view)) {
1195
- throw new Error(`unknown view "${args.view}"; projectable views: ${PROJECTABLE_VIEWS.join(", ")}`);
1196
- }
1197
- if (!args.out) throw new Error("bundle-view needs --out <dir> \u2014 a new or empty directory for the view");
1198
- const result = await projectSessionBundle({
1199
- bundleDir: args.input,
1200
- outDir: args.out,
1201
- view: args.view
1202
- });
1203
- const { manifest, leakCheck } = result;
1204
- console.log(
1205
- `${manifest.view} view \u2192 ${result.directory} (${manifest.files.length} file(s) carried, ${manifest.excluded.length} excluded, ${leakCheck.matches} session-text match(es) against ${leakCheck.sourceSignatures} signature(s) from ${leakCheck.comparedSources.length} excluded source(s))`
1206
- );
1808
+ function hostOf(args) {
1809
+ const root = args.selection?.home;
1810
+ if (root !== void 0) return basename2(root) === "home" ? basename2(dirname2(root)) : basename2(root);
1811
+ return hostname().split(".")[0].toLowerCase();
1207
1812
  }
1208
- async function cmdFacts(args) {
1209
- const format = args.format ?? "json";
1210
- if (format !== "json" && format !== "text") {
1211
- throw new Error(`unknown facts format "${format}" (expected json or text)`);
1212
- }
1213
- const window = args.since !== void 0 || args.until !== void 0 ? {
1214
- ...args.since !== void 0 ? { since: new Date(parseSince(args.since)).toISOString() } : {},
1215
- ...args.until !== void 0 ? { until: new Date(parseTimeBound(args.until, "--until")).toISOString() } : {}
1216
- } : void 0;
1217
- const builder = createSessionFactsReportBuilder({ includeContent: !args.noContent, window });
1218
- let report;
1219
- const discoversWindow = window !== void 0 && !args.session && !args.otlp && !args.input && !args.sourceBundle && !args.workflow;
1220
- if (discoversWindow) {
1221
- const harnesses = /* @__PURE__ */ new Set();
1222
- for await (const scanned of scanSessions({
1223
- adapters: adaptersFor(args),
1224
- cwd: args.cwd,
1225
- sinceMs: window.since === void 0 ? void 0 : Date.parse(window.since),
1226
- last: args.last > 0 ? args.last : void 0,
1227
- taskScope: args.latestTurn ? "latest" : "all",
1228
- onError: (error, ref) => {
1229
- if (!ref) throw error;
1230
- builder.addUnread({
1231
- sessionId: ref.sessionId,
1232
- traceId: null,
1233
- source: ref.path,
1234
- reason: error instanceof Error ? error.message : String(error)
1235
- });
1236
- }
1237
- })) {
1238
- harnesses.add(scanned.adapter.harness);
1239
- builder.add(scanned.spans);
1240
- }
1241
- report = { ...builder.build(), harness: [...harnesses].join("+") || args.harness };
1242
- } else {
1243
- const collected = await collectSpans(args);
1244
- if (collected.spans.length === 0) throw new Error("no spans found for the given selection");
1245
- warnIncompleteWorkflow(collected.workflow);
1246
- builder.add(collected.spans);
1247
- report = { ...builder.build(), harness: collected.harness };
1248
- }
1249
- const unreadInWindow = report.selection?.unreadSessions ?? [];
1250
- if (unreadInWindow.length > 0) {
1251
- const named = unreadInWindow.slice(0, 3).map((entry) => entry.source ?? entry.sessionId ?? entry.traceId);
1252
- process.stderr.write(
1253
- `warning: ${unreadInWindow.length} session(s) could not be read; each is named with its reason in selection.unreadSessions (${named.join(", ")}${unreadInWindow.length > named.length ? ", \u2026" : ""})
1254
- `
1255
- );
1256
- }
1257
- const unread = report.sessions.filter((facts) => facts.recordSpans === 0);
1258
- if (unread.length > 0) {
1259
- throw new Error(
1260
- `no records could be read from ${unread.length} selected session(s): ` + unread.map((facts) => `${facts.sessionId ?? facts.traceId} (${facts.unreadRecords.value ?? 0} unread record(s))`).join(", ") + ". The facts sheet would state zeros the spans cannot support."
1261
- );
1262
- }
1263
- const rendered = format === "json" ? `${JSON.stringify(report, null, 2)}
1264
- ` : renderSessionFacts(report);
1265
- if (args.out) {
1266
- await writeFile2(args.out, rendered, "utf8");
1267
- console.log(`session facts \u2192 ${args.out} (${report.sessions.length} session(s), $0, no model call)`);
1268
- return;
1269
- }
1270
- process.stdout.write(rendered);
1271
- }
1272
- async function cmdInspect(args) {
1273
- if (!args.input) throw new Error("inspect needs an index file; run `traces index --out session-index.json` first");
1274
- const index = await readSessionIndexFile(args.input);
1275
- const report = inspectSessionIndex(index);
1276
- if (args.out) {
1277
- const path = await writeInspectionReportFile(report, args.out);
1278
- console.log(`inspection report \u2192 ${path} (${report.totals.findings} finding(s), high ${report.totals.high})`);
1279
- } else {
1280
- process.stdout.write(renderInspectionReport(report));
1281
- }
1282
- }
1283
- async function cmdExport(args) {
1284
- if (!args.input) throw new Error("export needs an input file; run `traces export --help` for examples");
1285
- const format = traceEvidenceFormat(args.format);
1286
- const attributes = await loadExportAttributes(args);
1287
- const outPath = args.out ?? args.otlpOut;
1288
- if (outPath) {
1289
- const result2 = await writeTraceEvidenceExportFile(args.input, outPath, { format, attributes });
1290
- console.log(
1291
- `exported ${result2.spans.length} OpenInference span(s) from ${result2.format} \u2192 ${result2.path} (${result2.redactionCount} redaction${result2.redactionCount === 1 ? "" : "s"})`
1292
- );
1293
- return;
1294
- }
1295
- const result = await exportTraceEvidenceFile(args.input, { format, attributes });
1296
- for (const line of iterSerializedSpans(result.spans)) process.stdout.write(line);
1297
- }
1298
- async function cmdImportCodeTraceBench(args) {
1299
- if (!args.input) {
1300
- throw new Error(
1301
- "import-codetracebench needs the native CodeTraceBench rows JSON or JSONL file"
1302
- );
1303
- }
1304
- if (!args.trajectoryDir) {
1305
- throw new Error(
1306
- "import-codetracebench needs --trajectory-dir with <traj_id>/steps.json directories"
1307
- );
1308
- }
1309
- if (!args.out) {
1310
- throw new Error("import-codetracebench needs a new --out directory");
1813
+ async function cmdFacts(args) {
1814
+ const format = args.format ?? "text";
1815
+ const pattern = args.match === void 0 ? void 0 : new RegExp(args.match);
1816
+ if (args.noContent && (pattern !== void 0 || format === "jsonl")) {
1817
+ throw new UsageError(`${pattern ? "--match searches" : "--format jsonl rows carry"} turn text, which --no-content withholds; drop one of them`);
1818
+ }
1819
+ const window = factsWindow(args);
1820
+ const sourceOf = /* @__PURE__ */ new Map();
1821
+ let readingFrom = null;
1822
+ const host = hostOf(args);
1823
+ const matches = [];
1824
+ const citings = /* @__PURE__ */ new Map();
1825
+ const cite = !args.noContent && (format === "json" || pattern !== void 0);
1826
+ const onSession = (reading2) => {
1827
+ const source = readingFrom ?? sourceOf.get(reading2.facts.sessionId ?? "") ?? sourceOf.get(reading2.facts.traceId) ?? null;
1828
+ if (cite && source !== null) {
1829
+ citings.set(reading2.facts.sessionId ?? reading2.facts.traceId, { path: resolve2(source), callIds: callIdsOf(reading2.spans) });
1830
+ }
1831
+ if (pattern === void 0) return;
1832
+ for (const match of sessionMatches(reading2, pattern, window)) matches.push({ ...match, source });
1833
+ };
1834
+ const named = (args.selection?.sessions.length ?? 0) + (args.selection?.spans.length ?? 0);
1835
+ if (pattern !== void 0 && window !== void 0 && named === 0) return searchWindow(args, pattern, window, format);
1836
+ if (format === "jsonl" && pattern === void 0) return writeTurnFacts(args, host, window);
1837
+ const builder = createSessionFactsReportBuilder({
1838
+ includeContent: !args.noContent,
1839
+ window,
1840
+ nameUnread: named === 0,
1841
+ agentIdentity: sessionIdentityLookup(args.selection?.home ?? (process.env.HOME || homedir())),
1842
+ onSession
1843
+ });
1844
+ let report;
1845
+ if (window !== void 0 && named === 0) {
1846
+ const found = await discover(args);
1847
+ const unread2 = [...found.unread];
1848
+ const harnesses = /* @__PURE__ */ new Set();
1849
+ for (const { adapter, ref } of found.sessions) {
1850
+ let spans;
1851
+ try {
1852
+ spans = await parseSession(adapter, ref, { taskScope: args.latestTurn ? "latest" : "all" });
1853
+ } catch (error) {
1854
+ unread2.push({ sessionId: ref.sessionId, traceId: null, source: ref.path, reason: errorText(error) });
1855
+ continue;
1856
+ }
1857
+ harnesses.add(adapter.harness);
1858
+ readingFrom = ref.path;
1859
+ builder.add(spans);
1860
+ }
1861
+ readingFrom = null;
1862
+ for (const entry of unread2) builder.addUnread(entry);
1863
+ warnUnread(args, unread2);
1864
+ report = { ...builder.build(), harness: [...harnesses].join("+") || (args.harnessExplicit ? args.harness : null) };
1865
+ } else {
1866
+ const collected = await collectSpans(args);
1867
+ if (collected.spans.length === 0 && (collected.unread ?? []).length === 0) throw new Error("no spans found for the given selection");
1868
+ warnIncompleteWorkflow(collected.workflow);
1869
+ for (const source of collected.sources) sourceOf.set(source.sessionId, source.path);
1870
+ const files = args.selection?.spans ?? [];
1871
+ if (files.length === 1 && collected.sources.length === 0) readingFrom = files[0].path;
1872
+ builder.add(collected.spans);
1873
+ readingFrom = null;
1874
+ for (const entry of collected.unread ?? []) builder.addUnread(entry);
1875
+ report = { ...builder.build(), harness: collected.harness };
1311
1876
  }
1312
- if (!args.revision) {
1313
- throw new Error(
1314
- "import-codetracebench needs --revision with a full 40- or 64-character hexadecimal commit or digest"
1877
+ const unread = report.sessions.filter((facts) => facts.recordSpans === 0);
1878
+ if (unread.length > 0) {
1879
+ throw new RefError(
1880
+ `no records could be read from ${unread.length} selected session(s): ` + unread.map((facts) => `${facts.sessionId ?? facts.traceId} (${facts.unreadRecords.value ?? 0} unread record(s))`).join(", ") + ". The facts sheet would state zeros the spans cannot support.",
1881
+ "unreadable"
1315
1882
  );
1316
1883
  }
1317
- const controller = new AbortController();
1318
- const interrupt = () => controller.abort(
1319
- new Error("import-codetracebench interrupted")
1320
- );
1321
- process.once("SIGINT", interrupt);
1322
- process.once("SIGTERM", interrupt);
1323
- try {
1324
- const result = await importCodeTraceBench({
1325
- rowsPath: args.input,
1326
- trajectoryDir: args.trajectoryDir,
1327
- outDir: args.out,
1328
- revision: args.revision,
1329
- concurrency: args.concurrency,
1330
- signal: controller.signal
1884
+ if (pattern !== void 0) {
1885
+ return writeMatches(args, format, {
1886
+ generatedAt: report.generatedAt,
1887
+ sessions: report.sessions.length,
1888
+ selection: report.selection,
1889
+ matches: (await citeFacts(report.sessions, matches, citings)).matches
1331
1890
  });
1332
- const traceCount = result.receipt.counts.traces;
1333
- const stepCount = result.receipt.counts.steps;
1334
- const spanCount = result.receipt.counts.spans;
1335
- console.log(
1336
- `imported ${traceCount} CodeTraceBench ${traceCount === 1 ? "trajectory" : "trajectories"} (${stepCount} ${stepCount === 1 ? "step" : "steps"}, ${spanCount} ${spanCount === 1 ? "span" : "spans"}) \u2192 ${result.directory}; receipt: ${result.receiptPath}`
1337
- );
1338
- } finally {
1339
- process.removeListener("SIGINT", interrupt);
1340
- process.removeListener("SIGTERM", interrupt);
1341
1891
  }
1342
- }
1343
- function traceEvidenceFormat(raw) {
1344
- const format = raw ?? "auto";
1345
- if (format !== "auto" && format !== "policy-evidence" && format !== "sandbox-events" && format !== "openinference" && format !== "intelligence-spans" && format !== "chat-trajectory") {
1346
- throw new Error("--format must be auto, policy-evidence, sandbox-events, openinference, intelligence-spans, or chat-trajectory");
1892
+ report = await priceReport(report);
1893
+ const rendered = format === "json" ? `${JSON.stringify({ ...report, sessions: (await citeFacts(report.sessions, [], citings)).sessions })}
1894
+ ` : renderSessionFacts(report);
1895
+ const summary = `session facts \u2192 ${args.out} (${report.sessions.length} session(s), $0, no model call)`;
1896
+ await writeOutput(args, rendered, summary);
1897
+ }
1898
+ async function writeTurnFacts(args, host, window) {
1899
+ const unread = [];
1900
+ const named = (args.selection?.sessions.length ?? 0) + (args.selection?.spans.length ?? 0);
1901
+ async function* sources() {
1902
+ if (window !== void 0 && named === 0) {
1903
+ const found = await discover(args);
1904
+ unread.push(...found.unread);
1905
+ for (const { adapter, ref } of found.sessions) {
1906
+ try {
1907
+ yield { source: ref.path, spans: await readSessionSpans(adapter, ref, { taskScope: args.latestTurn ? "latest" : "all", includeSubagents: false }) };
1908
+ } catch (error) {
1909
+ unread.push({ sessionId: ref.sessionId, traceId: null, source: ref.path, reason: errorText(error) });
1910
+ }
1911
+ }
1912
+ return;
1913
+ }
1914
+ const collected = await collectSpans(args);
1915
+ warnIncompleteWorkflow(collected.workflow);
1916
+ unread.push(...collected.unread ?? []);
1917
+ const byTrace = /* @__PURE__ */ new Map();
1918
+ for (const span of collected.spans) {
1919
+ const held = byTrace.get(span.trace_id);
1920
+ if (held) held.push(span);
1921
+ else byTrace.set(span.trace_id, [span]);
1922
+ }
1923
+ const ids = indexSessionIdsByTrace(collected.spans).sessionByTrace;
1924
+ for (const [trace, spans] of byTrace) {
1925
+ const source = collected.sources.find((entry) => entry.sessionId === (ids.get(trace) ?? trace))?.path ?? (args.selection?.spans.length === 1 ? args.selection.spans[0].path : null);
1926
+ yield { spans, source };
1927
+ }
1347
1928
  }
1348
- return format;
1929
+ const result = await turnFactsJsonl(sources(), host, window);
1930
+ unread.push(...result.unread);
1931
+ if (named > 0 && result.unread.length > 0) throw new RefError(`no records could be read: ${result.unread.map((entry) => entry.sessionId).join(", ")}`, "unreadable");
1932
+ warnUnread(args, unread);
1933
+ await writeOutput(args, result.text, `turns \u2192 ${args.out} (${result.turns} typed turn(s) in ${result.sessions} session(s), $0, no model call)`);
1934
+ }
1935
+ function factsWindow(args) {
1936
+ if (args.since === void 0 && args.until === void 0) return void 0;
1937
+ return {
1938
+ ...args.since !== void 0 ? { since: new Date(parseSince(args.since)).toISOString() } : {},
1939
+ ...args.until !== void 0 ? { until: new Date(parseTimeBound(args.until, "--until")).toISOString() } : {}
1940
+ };
1349
1941
  }
1350
- function parseAttributeValue(raw) {
1351
- if (raw === "true") return true;
1352
- if (raw === "false") return false;
1353
- if (/^-?\d+(?:\.\d+)?$/.test(raw)) {
1354
- const number = Number(raw);
1355
- if (Number.isFinite(number)) return number;
1942
+ async function writeOutput(args, rendered, summary) {
1943
+ if (args.out) {
1944
+ await writeFile(args.out, rendered, "utf8");
1945
+ console.log(summary);
1946
+ return;
1356
1947
  }
1357
- return raw;
1948
+ process.stdout.write(rendered);
1358
1949
  }
1359
- async function loadExportAttributes(args) {
1360
- const attributes = {};
1361
- if (args.metadata) {
1362
- const parsed = JSON.parse(await readFile2(args.metadata, "utf8"));
1363
- if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
1364
- throw new Error("--metadata must point to a JSON object");
1950
+ async function writeMatches(args, format, found) {
1951
+ const matches = [...found.matches].sort((left, right) => Date.parse(left.at) - Date.parse(right.at));
1952
+ const document = {
1953
+ schemaVersion: 1,
1954
+ kind: "traces.session_matches",
1955
+ generatedAt: found.generatedAt,
1956
+ pattern: args.match,
1957
+ sessions: found.sessions,
1958
+ selection: found.selection,
1959
+ matches
1960
+ };
1961
+ const rendered = format === "json" ? `${JSON.stringify(document, null, 2)}
1962
+ ` : format === "jsonl" ? matches.map((match) => `${JSON.stringify(match)}
1963
+ `).join("") : renderMatches(args.match, found.sessions, matches);
1964
+ await writeOutput(args, rendered, `matches \u2192 ${args.out} (${matches.length} in ${found.sessions} session(s), $0, no model call)`);
1965
+ }
1966
+ async function searchWindow(args, pattern, window, format) {
1967
+ const found = await discover(args);
1968
+ const unread = [...found.unread];
1969
+ const outsideWindow = [];
1970
+ const matches = [];
1971
+ const since = window.since === void 0 ? Number.NEGATIVE_INFINITY : Date.parse(window.since);
1972
+ const until = window.until === void 0 ? Number.POSITIVE_INFINITY : Date.parse(window.until);
1973
+ let sessions = 0;
1974
+ const stats = await TurnIndex.with(async (index) => {
1975
+ const refreshed = await index.refresh(found.sessions);
1976
+ for (const { adapter, ref } of found.sessions) {
1977
+ const held = index.session(adapter, ref);
1978
+ if (!held || held.unread !== null) {
1979
+ unread.push({ sessionId: ref.sessionId, traceId: null, source: ref.path, reason: held?.unread ?? "not indexed" });
1980
+ continue;
1981
+ }
1982
+ const { session } = held;
1983
+ if (Date.parse(session.lastAt ?? "") < since || Date.parse(session.firstAt ?? "") >= until) {
1984
+ outsideWindow.push(session.sessionId ?? ref.sessionId);
1985
+ continue;
1986
+ }
1987
+ sessions += 1;
1988
+ for (const match of matchRows(session.sessionId, await index.rows(adapter, ref), pattern, window)) {
1989
+ matches.push({ ...match, source: ref.path });
1990
+ }
1365
1991
  }
1366
- Object.assign(attributes, parsed);
1367
- }
1368
- for (const attr of args.attrs) {
1369
- const eq = attr.indexOf("=");
1370
- if (eq <= 0) throw new Error(`--attr must be key=value, got ${attr}`);
1371
- attributes[attr.slice(0, eq)] = parseAttributeValue(attr.slice(eq + 1));
1992
+ return refreshed;
1993
+ });
1994
+ warnUnread(args, unread);
1995
+ await writeMatches(args, format, {
1996
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1997
+ sessions,
1998
+ selection: {
1999
+ since: window.since ?? null,
2000
+ until: window.until ?? null,
2001
+ sessionRule: "a session is searched when its records, first to last, overlap [since, until)",
2002
+ turnRule: "only rows that started inside [since, until) match",
2003
+ outsideWindow: outsideWindow.sort(),
2004
+ unreadSessions: unread,
2005
+ index: stats
2006
+ },
2007
+ matches
2008
+ });
2009
+ }
2010
+ function renderMatches(pattern, sessions, matches) {
2011
+ const lines = [`${matches.length} match(es) for /${pattern}/ in ${sessions} session(s), deterministic, $0`];
2012
+ for (const match of matches) {
2013
+ const outcome = match.kind !== "command" ? "" : `${(match.failed === true ? "failed" : match.failed === false ? "ok" : "?").padEnd(6)} `;
2014
+ lines.push(`${match.at} ${(match.sessionId ?? "?").slice(0, 8)} ${match.kind.padEnd(7)} ${outcome}${match.text}`);
2015
+ if (match.record) lines.push(` ${match.record.file}:${match.record.line}${match.lastLine == null ? "" : ` last: ${match.lastLine.slice(0, 300)}`}`);
1372
2016
  }
1373
- return attributes;
2017
+ return `${lines.join("\n")}
2018
+ `;
1374
2019
  }
1375
2020
  async function cmdAnalyze(args) {
1376
- if (args.supervisorRunDir) return cmdAnalyzeSupervisorRun(args.supervisorRunDir, args);
1377
- const result = await investigate(args, { loadDefaultConfig: false });
1378
- let report = result.report;
1379
- let verifySummary = "";
1380
- if (args.verifyFindings) {
1381
- const run = await verifyAnalyzeFindings(args, result);
1382
- report = `${report}
1383
- ${renderVerifiedFindingsSection(run)}`;
1384
- verifySummary = `, verified: ${run.counts.reproduced} reproduced / ${run.counts["fix-flipped"]} fix-flipped / ${run.counts.divergent} divergent / ${run.counts["not-replayable"]} not-replayable \u2192 ${run.out}`;
1385
- }
2021
+ const runDir = args.selection?.spans.find((source) => source.kind === "run-dir");
2022
+ if (runDir) {
2023
+ if (args.refs.length > 1) throw new UsageError(`analyze reports one supervision tree at a time; ${runDir.ref} is a run directory`);
2024
+ const { writeSupervisorReport } = await import("./supervisor-report-RMPLLBGZ.js");
2025
+ return writeSupervisorReport(runDir.path, args.out);
2026
+ }
2027
+ const { createTraceInvestigation, writeAnalysisArtifacts } = await import("./improvement-OJQ6365E.js");
2028
+ const { renderAnalysis, renderAnalysisReport } = await import("./report-JAMXRTCW.js");
2029
+ const investigation = createTraceInvestigation({
2030
+ harness: args.harnessExplicit ? args.harness : "none",
2031
+ facts: { window: factsWindow(args), agentIdentity: sessionIdentityLookup(args.selection?.home ?? (process.env.HOME || homedir())) },
2032
+ log: analystLog
2033
+ });
2034
+ const collected = await forEachSpanBatch(args, (spans, harness) => investigation.add(spans, { harness }));
2035
+ if (collected.spanCount === 0) throw new Error("no spans found for the given selection");
2036
+ warnIncompleteWorkflow(collected.workflow);
2037
+ for (const entry of collected.unread ?? []) investigation.addUnread(entry);
2038
+ const result = await investigation.finish(collected);
1386
2039
  if (args.out) {
1387
- await saveReport(args.out, report);
2040
+ const artifacts = await writeAnalysisArtifacts(renderAnalysisReport(result), result.packet, args.out);
1388
2041
  console.log(
1389
- `report \u2192 ${args.out} (${result.findings.length} findings, ${result.pipelines.stuckLoops.findings.length} loops, OTLP: ${result.otlpPath}${verifySummary})`
1390
- );
1391
- } else {
1392
- console.log(report);
1393
- }
1394
- assertRequestedAnalysesRan(args, result);
1395
- }
1396
- async function verifyAnalyzeFindings(args, result) {
1397
- if (args.replayCorpora.length === 0) {
1398
- throw new Error(
1399
- "analyze --verify-findings needs the executable trajectory source: pass --replay-corpus name=<labels.json>::<preparedDir> (repeatable). Harness sessions carry no docker_config, so findings cannot be replayed from the session store alone."
1400
- );
1401
- }
1402
- if (result.findings.length === 0) {
1403
- throw new Error("analyze --verify-findings: the analysis produced no findings to verify");
1404
- }
1405
- const out = args.verifyOut ?? (args.out ? `${args.out}.verify` : "traces-verify-findings");
1406
- return verifyFindingsInSandbox(result.findings, {
1407
- source: { kind: "corpus", corpora: args.replayCorpora.map(parseCorpusFlag) },
1408
- out,
1409
- sandbox: {
1410
- apiKey: process.env.SANDBOX_API_KEY ?? "",
1411
- baseUrl: process.env.SANDBOX_API_URL ?? DEFAULT_SANDBOX_BASE_URL
1412
- }
1413
- });
1414
- }
1415
- async function cmdAnalyzeSupervisorRun(runDir, args) {
1416
- const single = await isRuntimeSupervisorRunDir(runDir);
1417
- const runDirs = single ? [runDir] : await findSupervisorRunDirs(runDir);
1418
- if (runDirs.length === 0) {
1419
- throw new Error(
1420
- `no supervisor run found at ${runDir} \u2014 expected a Runtime run containing spawn-journal.jsonl, observer.jsonl, or failure.json, or a parent directory containing such runs`
2042
+ `analysis \u2192 ${artifacts.directory} (report.md, findings.json: ${result.packet.findings.length} finding(s), ${result.cannotAnswer.length} question(s) it cannot answer, $0)`
1421
2043
  );
2044
+ return;
1422
2045
  }
1423
- const reports = await Promise.all(
1424
- runDirs.map((dir) => analyzeSupervisorRun(fileRunContextSupervisorRunReader(dir)))
1425
- );
1426
- const markdown = single ? renderSupervisorRunMarkdown(reports[0]) : renderSupervisorRollupMarkdown(rollupSupervisorRuns(reports), `Supervisor rollup \u2014 ${runDir}`);
1427
- if (args.out) {
1428
- await saveReport(args.out, markdown);
1429
- console.log(`supervisor report \u2192 ${args.out} (${runDirs.length === 1 ? "1 run" : `${runDirs.length} runs`})`);
1430
- } else {
1431
- console.log(markdown);
2046
+ if (args.format === "json") {
2047
+ const { schemaVersion, kind, generatedAt, harness, facts, packet, measures, conformance, cannotAnswer } = result;
2048
+ process.stdout.write(`${JSON.stringify({ schemaVersion, kind, generatedAt, harness, facts, packet, measures, conformance, cannotAnswer })}
2049
+ `);
2050
+ return;
1432
2051
  }
2052
+ process.stdout.write(renderAnalysis(result));
1433
2053
  }
1434
- async function collectImportedSpans(args) {
1435
- if (!args.input) throw new Error("input file missing");
1436
- const attributes = await loadExportAttributes(args);
1437
- const result = await exportTraceEvidenceFile(args.input, {
1438
- format: traceEvidenceFormat(args.format),
1439
- attributes
1440
- });
1441
- const { sessionByTrace } = indexSessionIdsByTrace(result.spans);
2054
+ async function collectImportedSpans(path, format, selector) {
2055
+ const { sessionReportSource } = await import("./report-JAMXRTCW.js");
2056
+ const result = await exportTraceEvidenceFile(path, { format });
2057
+ const spans = selector ? selectSpans(result.spans, selector, path) : result.spans;
2058
+ const { sessionByTrace } = indexSessionIdsByTrace(spans);
1442
2059
  const sessions = /* @__PURE__ */ new Map();
1443
- for (const span of result.spans) {
2060
+ for (const span of spans) {
1444
2061
  const sessionId = sessionByTrace.get(span.trace_id);
1445
2062
  if (!sessionId) continue;
1446
2063
  const grouped = sessions.get(sessionId) ?? [];
@@ -1448,367 +2065,252 @@ async function collectImportedSpans(args) {
1448
2065
  sessions.set(sessionId, grouped);
1449
2066
  }
1450
2067
  return {
1451
- spans: result.spans,
2068
+ spans,
1452
2069
  harness: result.format,
1453
- cwds: [],
1454
- sources: [...sessions].map(([sessionId, spans]) => sessionReportSource({
2070
+ sources: [...sessions].map(([sessionId, grouped]) => sessionReportSource({
1455
2071
  harness: result.format,
1456
2072
  sessionId,
1457
- path: args.input,
2073
+ path,
1458
2074
  cwd: null,
1459
2075
  mtimeMs: 0
1460
- }, spans, sessionId))
2076
+ }, grouped, sessionId))
1461
2077
  };
1462
2078
  }
1463
- function analystLog(msg, fields) {
1464
- const rejection = findingRejectionDetail(msg, fields);
1465
- if (rejection !== void 0) {
1466
- process.stderr.write(rejection ? `${msg} \u2014 ${rejection}
1467
- ` : `${msg}
1468
- `);
1469
- return;
1470
- }
1471
- const error = typeof fields?.error === "string" && fields.error ? fields.error : void 0;
1472
- const errorClass = typeof fields?.error_class === "string" && fields.error_class ? `${fields.error_class}: ` : "";
1473
- process.stderr.write(error ? `${msg} \u2014 ${errorClass}${error}
1474
- ` : `${msg}
1475
- `);
1476
- }
1477
- function externalAnalyzersFromArgs(args) {
1478
- return args.analyzers.map(
1479
- (spec) => spec === "halo" ? haloAnalyzer({ defaultPrompt: args.analyzerPrompt, model: args.model }) : spec === "hodoscope" ? hodoscopeAnalyzer({ summarizeModel: args.model }) : spec === "prime" ? primeAnalyzer({ defaultPrompt: args.analyzerPrompt, ...args.model ? { model: args.model } : {} }) : commandAnalyzer({ name: spec, command: spec, args: (p, prompt) => prompt ? [p, prompt] : [p] })
1480
- );
1481
- }
1482
- async function cmdInvestigate(args) {
1483
- const result = await investigate(args);
1484
- if (args.out) {
1485
- await saveReport(args.out, result.report);
1486
- console.log(`investigation report \u2192 ${args.out} (${result.findings.length} findings with actions, OTLP: ${result.otlpPath})`);
1487
- } else {
1488
- console.log(result.report);
2079
+ async function cmdAsk(args) {
2080
+ const { analysisEngineFromEnv, bridgeVersion, missingAskRequirements, UnservableModelError } = await import("./analyst-model-call-JVUOYZ5V.js");
2081
+ const { createTraceQuestionCorpus, isBridgeMismatchError, loadTraceQuestionsFile, normalizeTraceQuestions, runTraceQuestions, writeTraceQuestionsArtifacts } = await import("./ask-XLS3VANY.js");
2082
+ let questions;
2083
+ try {
2084
+ questions = [
2085
+ ...args.questionsFile ? await loadTraceQuestionsFile(args.questionsFile) : [],
2086
+ ...args.questions.map((question) => ({ question }))
2087
+ ];
2088
+ if (questions.length === 0) throw new UsageError('ask needs --question "<text>" (repeatable) or --questions <questions.json>');
2089
+ normalizeTraceQuestions(questions);
2090
+ } catch (error) {
2091
+ throw error instanceof CliError ? error : new UsageError(errorText(error));
1489
2092
  }
1490
- assertRequestedAnalysesRan(args, result);
1491
- }
1492
- function conformanceOptions(collected) {
1493
- return {
1494
- ...collected.conformance ? { conformance: collected.conformance } : {},
1495
- ...collected.conformanceSubject ? { conformanceSubject: collected.conformanceSubject } : {},
1496
- ...collected.conformanceFiles ? { conformanceFiles: collected.conformanceFiles } : {},
1497
- ...collected.conformanceSkipped ? { conformanceSkipped: collected.conformanceSkipped } : {},
1498
- ...collected.conformanceIssues ? { conformanceIssues: collected.conformanceIssues } : {},
1499
- ...collected.conformanceWithheld ? { conformanceWithheld: collected.conformanceWithheld } : {},
1500
- ...collected.conformanceUnreadable ? { conformanceUnreadable: collected.conformanceUnreadable } : {}
1501
- };
1502
- }
1503
- async function investigate(args, options = {}) {
1504
- const collected = await collectSpans(args);
1505
- const { spans, harness, cwds, sources, workflow } = collected;
1506
- if (spans.length === 0) throw new Error("no spans found for the given selection");
1507
- const config = args.config !== void 0 || options.loadDefaultConfig !== false ? await loadTracesConfig(args.config) : void 0;
1508
- const analystModel = analystModelFor(args);
1509
- const engine = args.llm ? llmAnalysisEngine(args, analystModel) : void 0;
1510
- return runTraceInvestigation(mergeTracesConfig({
1511
- sourceBundle: args.sourceBundle ? { path: args.sourceBundle } : void 0,
1512
- spans,
1513
- harness,
1514
- sources,
1515
- workflow,
1516
- cwds,
1517
- minLoopOccurrences: args.minLoop,
1518
- engine,
1519
- model: args.llm ? analystModel : args.model,
1520
- budgetUsd: args.budget,
1521
- otlpOutPath: args.otlpOut,
1522
- externalAnalyzers: externalAnalyzersFromArgs(args),
1523
- analyzerPrompt: args.analyzerPrompt,
1524
- log: analystLog,
1525
- ...conformanceOptions(collected)
1526
- }, config));
1527
- }
1528
- async function cmdImprove(args) {
1529
- const collected = await collectSpans(args);
1530
- const { spans, harness, cwds, sources, workflow } = collected;
1531
- if (spans.length === 0) throw new Error("no spans found for the given selection");
1532
- const config = await loadTracesConfig(args.config);
1533
- const analystModel = analystModelFor(args);
1534
- const engine = args.llm ? llmAnalysisEngine(args, analystModel) : void 0;
1535
- const result = await runTraceImprovement({
1536
- ...mergeTracesConfig({
1537
- sourceBundle: args.sourceBundle ? { path: args.sourceBundle } : void 0,
1538
- spans,
1539
- harness,
1540
- sources,
1541
- workflow,
1542
- cwds,
1543
- minLoopOccurrences: args.minLoop,
1544
- engine,
1545
- model: args.llm ? analystModel : args.model,
1546
- budgetUsd: args.budget,
1547
- otlpOutPath: args.otlpOut,
1548
- externalAnalyzers: externalAnalyzersFromArgs(args),
1549
- analyzerPrompt: args.analyzerPrompt,
2093
+ const missing = missingAskRequirements();
2094
+ if (missing.length > 0) throw new CliError(`ask needs ${missing.join("; and ")}. Nothing was spent`, "setup");
2095
+ const budget = args.budget;
2096
+ const controller = new AbortController();
2097
+ let refused;
2098
+ let engine;
2099
+ try {
2100
+ engine = analysisEngineFromEnv({
2101
+ model: analystModelFor(args),
2102
+ maxCostUsd: Math.min(budget, DEFAULT_QUESTION_MAX_COST_USD),
2103
+ timeoutMs: DEFAULT_QUESTION_TIMEOUT_MS,
1550
2104
  log: analystLog,
1551
- ...conformanceOptions(collected)
1552
- }, config),
1553
- outDir: args.dir ?? args.out
1554
- });
1555
- const dir = result.artifacts?.directory;
1556
- if (!dir) throw new Error("improve did not produce an artifact directory");
1557
- console.log(
1558
- `improvement artifacts \u2192 ${dir} (${result.findings.length} findings with actions and checks, OTLP: ${result.otlpPath})`
1559
- );
1560
- assertRequestedAnalysesRan(args, result);
1561
- }
1562
- async function cmdAsk(args) {
1563
- if (args.out) throw new Error("ask writes a directory of artifacts; pass --dir <dir> instead of --out");
1564
- if (args.llm) throw new Error("ask always uses the model-backed engine; drop --llm");
1565
- const questions = [
1566
- ...args.questionsFile ? await loadTraceQuestionsFile(args.questionsFile) : [],
1567
- ...args.questions.map((question) => ({ question }))
1568
- ];
1569
- if (questions.length === 0) {
1570
- throw new Error('ask needs --question "<text>" (repeatable) or --questions <questions.json>');
1571
- }
1572
- normalizeTraceQuestions(questions);
1573
- if (args.questionBudget !== void 0 && (!Number.isFinite(args.questionBudget) || args.questionBudget <= 0)) {
1574
- throw new Error("--question-budget must be a positive number of USD");
1575
- }
1576
- if (args.budget !== void 0 && (!Number.isFinite(args.budget) || args.budget <= 0)) {
1577
- throw new Error("--budget must be a positive number of USD");
1578
- }
1579
- const questionTimeoutMs = args.questionTimeout === void 0 ? void 0 : Math.round(args.questionTimeout * 1e3);
1580
- if (questionTimeoutMs !== void 0 && (!Number.isSafeInteger(questionTimeoutMs) || questionTimeoutMs < 1e3)) {
1581
- throw new Error("--question-timeout must be a number of seconds, at least 1");
1582
- }
1583
- const engine = analysisEngineFromEnv({
1584
- model: analystModelFor(args),
1585
- maxCostUsd: args.questionBudget ?? Math.min(args.budget ?? Infinity, DEFAULT_QUESTION_MAX_COST_USD),
1586
- ...questionTimeoutMs !== void 0 ? { timeoutMs: questionTimeoutMs } : {},
1587
- log: analystLog
1588
- });
1589
- const collected = await collectSpans(args);
1590
- if (collected.spans.length === 0) throw new Error("no spans found for the given selection");
2105
+ // A refused key or model fails every question the same way: stop them all at the first.
2106
+ onPermanentFailure: (failure) => {
2107
+ refused ??= failure;
2108
+ controller.abort(new Error(failure.message));
2109
+ }
2110
+ });
2111
+ } catch (error) {
2112
+ if (error instanceof UnservableModelError) throw new CliError(error.message, error.failure.code);
2113
+ throw error;
2114
+ }
2115
+ const corpus = createTraceQuestionCorpus();
2116
+ const collected = await forEachSpanBatch(args, async (spans) => corpus.add(spans));
2117
+ if (collected.spanCount === 0) throw new CliError("no spans found for the given selection", "unreadable");
1591
2118
  warnIncompleteWorkflow(collected.workflow);
1592
- const directory = resolve(args.dir ?? await mkdtemp(join2(tmpdir(), "traces-ask-")));
1593
- await mkdir2(directory, { recursive: true });
1594
- const controller = new AbortController();
1595
2119
  const interrupt = () => controller.abort(new Error("ask interrupted"));
1596
2120
  process.once("SIGINT", interrupt);
1597
2121
  let result;
1598
2122
  try {
1599
2123
  result = await runTraceQuestions({
1600
2124
  questions,
1601
- spans: collected.spans,
2125
+ corpus: corpus.finish(),
1602
2126
  engine,
1603
2127
  harness: collected.harness,
1604
- concurrency: args.concurrency,
1605
- ...args.budget !== void 0 ? { budgetUsd: args.budget } : {},
1606
- ...args.sourceBundle ? { sourceBundle: { path: args.sourceBundle } } : {},
1607
- otlpOutPath: args.otlpOut ?? join2(directory, "traces.otlp.jsonl"),
2128
+ concurrency: DEFAULT_ASK_CONCURRENCY,
2129
+ budgetUsd: budget,
1608
2130
  signal: controller.signal,
1609
2131
  log: analystLog
1610
2132
  });
2133
+ } catch (error) {
2134
+ throw error instanceof RangeError ? new UsageError(error.message) : error;
1611
2135
  } finally {
1612
2136
  process.removeListener("SIGINT", interrupt);
1613
2137
  }
1614
- const artifacts = await writeTraceQuestionsArtifacts(result, directory);
1615
- process.stdout.write(result.report);
2138
+ const artifacts = args.out === void 0 ? void 0 : await writeTraceQuestionsArtifacts(result, args.out);
1616
2139
  for (const warning of result.warnings) process.stderr.write(`warning: ${warning}
1617
2140
  `);
1618
- process.stderr.write(
1619
- `ask artifacts \u2192 ${artifacts.directory} (${result.totals.answered}/${result.totals.questions} answered, wall ${(result.totals.wallTimeMs / 1e3).toFixed(1)} s, answers: ${artifacts.result})
1620
- `
2141
+ if (refused) {
2142
+ const record = artifacts ? `; ${artifacts.result} records each question` : "";
2143
+ throw new CliError(`${refused.message}. ask stopped every question at the first refused call${record}`, refused.code);
2144
+ }
2145
+ const { totals } = result;
2146
+ const cost = totals.cost.usd;
2147
+ if (artifacts) {
2148
+ process.stdout.write(args.format === "json" ? `${JSON.stringify({ out: artifacts.directory, answered: totals.answered, questions: totals.questions, costUsd: cost })}
2149
+ ` : `answers \u2192 ${artifacts.directory} (answers.json, report.md: ${totals.answered}/${totals.questions} answered, ${cost === null ? "cost uncaptured" : `$${cost > 0 && cost < 0.01 ? cost.toPrecision(2) : cost.toFixed(2)}`}, ${(totals.wallTimeMs / 1e3).toFixed(1)} s)
2150
+ `);
2151
+ } else if (args.format === "json") {
2152
+ const { report: _report, ...machine } = result;
2153
+ process.stdout.write(`${JSON.stringify(machine)}
2154
+ `);
2155
+ } else {
2156
+ process.stdout.write(result.report);
2157
+ }
2158
+ if (result.ok) return;
2159
+ const failed = result.questions.filter((answer) => answer.status === "failed");
2160
+ const where = artifacts ? artifacts.result : args.format === "json" ? "the JSON on stdout" : "the report above";
2161
+ const hint = failed.some((answer) => isBridgeMismatchError(answer.failure?.message ?? "")) ? ` The DSPy bridge protocol is version-locked: TRACES_PYTHON needs agent-eval-rpc[dspy]==${bridgeVersion()}, or unset it to let uv run that version.` : "";
2162
+ throw new CliError(
2163
+ `${failed.length} of ${result.questions.length} question(s) unanswered (${failed.map((answer) => `${answer.id}: ${answer.failure?.kind}`).join(", ")}); ${where} says why.${hint}`,
2164
+ "partial"
1621
2165
  );
1622
- if (!result.ok) {
1623
- const failed = result.questions.filter((answer) => answer.status === "failed");
1624
- const lines = [
1625
- `${failed.length} of ${result.questions.length} question(s) failed; the answers file holds every result.`,
1626
- ...failed.map((answer) => ` ${answer.id}: ${answer.failure?.kind}: ${answer.failure?.message.slice(0, 300)}`)
1627
- ];
1628
- if (failed.some((answer) => isBridgeMismatchError(answer.failure?.message ?? ""))) {
1629
- lines.push(
1630
- `hint: the DSPy bridge protocol is version-locked \u2014 the TRACES_PYTHON interpreter needs agent-eval-rpc[dspy]==${requiredBridgeVersion()} (matching this package's @tangle-network/agent-eval).`
1631
- );
1632
- }
1633
- throw new Error(lines.join("\n"));
1634
- }
1635
- }
1636
- function summarizeFindingEvidence(finding) {
1637
- const first = finding.evidence[0];
1638
- if (!first) return "no evidence";
1639
- return `${first.label}: ${first.value}`;
1640
- }
1641
- function formatLiveFinding(finding) {
1642
- const ts = new Date(finding.observedAt).toISOString().slice(11, 19);
1643
- const where = `[${finding.session.harness}] ${finding.session.sessionId.slice(0, 8)}`;
1644
- const cwd = finding.session.cwd ? ` \xB7 ${finding.session.cwd}` : "";
1645
- return [
1646
- `${ts} ${finding.severity.toUpperCase()} ${where}: ${finding.title}${cwd}`,
1647
- ` evidence: ${summarizeFindingEvidence(finding)}`,
1648
- ` action: ${finding.action}`,
1649
- ` check: ${finding.check}`
1650
- ].join("\n");
1651
2166
  }
1652
- function streamMode(raw) {
1653
- if (raw === void 0) return "visualizer";
1654
- if (raw === "visualizer" || raw === "findings" || raw === "agent") return raw;
1655
- throw new Error(`unknown stream mode "${raw}" (expected visualizer, findings, or agent)`);
1656
- }
1657
- function streamPreset(args) {
1658
- const mode = streamMode(args.mode);
1659
- return {
1660
- mode,
1661
- includeSpans: !args.noSpans && mode === "visualizer",
1662
- includeFindings: !args.noFindings,
1663
- includeBatches: true,
1664
- includeReports: mode === "agent"
1665
- };
2167
+ var LIVE_SEVERITY = { info: "INFO", low: "LOW", medium: "MED", high: "HIGH" };
2168
+ function liveFindingLine(finding, ref) {
2169
+ const spans = [...new Set(finding.evidence.flatMap((item) => item.spanIds ?? []))];
2170
+ const cite = spans.length === 0 ? "" : ` (span${spans.length === 1 ? "" : "s"} ${spans.slice(0, 2).join(", ")}${spans.length > 2 ? ", \u2026" : ""})`;
2171
+ return `${finding.observedAt.slice(11, 19)} ${LIVE_SEVERITY[finding.severity]} ${ref} ${finding.session.harness} ${finding.ruleId}: ${finding.claim}${cite}`;
1666
2172
  }
1667
- async function streamExplicitSession(args, extraAnalysts) {
1668
- if (!args.session) throw new Error("streamExplicitSession needs --session");
1669
- const adapter = resolveAdapter(args.harness);
1670
- if (!adapter) throw new Error(`unknown harness "${args.harness}"`);
1671
- const preset = streamPreset(args);
1672
- const st = await stat(args.session);
1673
- const ref = {
1674
- harness: adapter.harness,
1675
- sessionId: basename(args.session),
1676
- path: resolve(args.session),
1677
- cwd: args.cwd ?? null,
1678
- mtimeMs: st.mtimeMs
1679
- };
1680
- const spans = await parseSession(adapter, ref);
1681
- for (const event of traceStreamEventsFromSpans(spans, {
1682
- ref,
1683
- includeSpans: preset.includeSpans,
1684
- includeFindings: preset.includeFindings,
1685
- extraAnalysts
1686
- })) {
1687
- process.stdout.write(serializeTraceStreamEvent(event));
1688
- }
1689
- }
1690
- async function cmdStream(args) {
1691
- const config = await loadTracesConfig(args.config);
1692
- const preset = streamPreset(args);
1693
- const source = args.otlp ?? args.input;
1694
- if (source) {
1695
- const { spans, harness } = args.otlp ? await collectOtlpSpans(args.otlp) : await collectImportedSpans(args);
1696
- const ref = {
1697
- harness,
1698
- sessionId: basename(source),
1699
- path: resolve(source),
1700
- cwd: args.cwd ?? null,
1701
- mtimeMs: Date.now()
2173
+ async function* spanRuns(args, path) {
2174
+ const { spans } = await collectSpans(args);
2175
+ const { sessionByTrace } = indexSessionIdsByTrace(spans);
2176
+ const byTrace = /* @__PURE__ */ new Map();
2177
+ for (const span of spans) {
2178
+ const group = byTrace.get(span.trace_id);
2179
+ if (group) group.push(span);
2180
+ else byTrace.set(span.trace_id, [span]);
2181
+ }
2182
+ for (const [traceId, group] of byTrace) {
2183
+ const attribute = (key) => {
2184
+ const value = group[0].attributes[key];
2185
+ return typeof value === "string" && value.length > 0 ? value : null;
1702
2186
  };
1703
- for (const event of traceStreamEventsFromSpans(spans, {
1704
- ref,
1705
- includeSpans: preset.includeSpans,
1706
- includeFindings: preset.includeFindings,
1707
- extraAnalysts: config?.liveAnalysts
1708
- })) {
1709
- process.stdout.write(serializeTraceStreamEvent(event));
1710
- }
1711
- return;
1712
- }
1713
- if (args.session) {
1714
- await streamExplicitSession(args, config?.liveAnalysts);
1715
- return;
1716
- }
1717
- const all = args.all || !args.harnessExplicit && !args.cwd;
1718
- const controller = new AbortController();
1719
- process.once("SIGINT", () => controller.abort());
1720
- if (!args.replay) {
1721
- process.stderr.write(
1722
- `traces stream: ${preset.mode} JSONL feed for ${all ? "all harnesses" : args.harness}, last ${args.window}m, every ${args.interval}s. Ctrl-C to stop.
1723
- `
1724
- );
2187
+ yield { session: { harness: attribute("service.name") ?? "otlp", sessionId: sessionByTrace.get(traceId) ?? traceId, cwd: attribute("tangle.cwd"), path }, spans: group };
1725
2188
  }
1726
- await streamSessions({
1727
- all,
1728
- harnesses: all ? void 0 : [args.harness],
1729
- cwd: args.cwd,
1730
- last: args.last > 0 ? args.last : args.replay ? 1 : void 0,
1731
- windowMs: args.window * 6e4,
1732
- intervalMs: args.interval * 1e3,
1733
- once: args.replay,
1734
- includeSpans: preset.includeSpans,
1735
- includeFindings: preset.includeFindings,
1736
- includeBatches: preset.includeBatches,
1737
- includeReports: preset.includeReports,
1738
- extraAnalysts: config?.liveAnalysts,
1739
- signal: controller.signal,
1740
- onEvent: (event) => {
1741
- process.stdout.write(serializeTraceStreamEvent(event));
1742
- },
1743
- onError: (err, ref) => {
1744
- const where = ref ? ` [${ref.harness}] ${ref.sessionId}` : "";
1745
- process.stderr.write(`stream error${where}: ${err instanceof Error ? err.message : String(err)}
1746
- `);
1747
- }
1748
- });
1749
2189
  }
1750
2190
  async function cmdWatch(args) {
1751
- const target = args.supervisorRunDir ?? args.input;
1752
- if (target !== void 0) return cmdWatchRunTarget(target, args);
1753
- const all = args.all || !args.harnessExplicit && !args.cwd;
2191
+ const named = args.selection;
2192
+ const source = named?.spans[0];
2193
+ if (source && (named.spans.length > 1 || named.sessions.length > 0)) {
2194
+ throw new UsageError("watch follows one run tree or span file at a time, or harness sessions: traces watch <run-dir|file> or traces watch <id\u2026>");
2195
+ }
2196
+ const jsonl = args.format === "jsonl";
2197
+ if (source && !jsonl) return cmdWatchRunTarget(source.path, args);
2198
+ const { streamSessions, watchSource } = await import("./live-Y64AJ6MA.js");
2199
+ const since = args.since ?? "30m";
2200
+ const windowMs = relativeBoundMs(since);
2201
+ const sessions = named?.sessions.length ? named.sessions : void 0;
2202
+ const scope = source ? source.ref : sessions ? `${sessions.length} named session${sessions.length === 1 ? "" : "s"}` : `every session active ${windowMs === void 0 ? "since" : "in the last"} ${since}`;
1754
2203
  const controller = new AbortController();
1755
2204
  process.once("SIGINT", () => controller.abort());
1756
- process.stderr.write(
1757
- `traces watch: observing ${all ? "all harnesses" : args.harness}, sessions active in the last ${args.window}m, every ${args.interval}s. Loop + semantic live findings; read-only; Ctrl-C to stop.
1758
- `
1759
- );
1760
- await watchSessions({
1761
- all,
1762
- harnesses: all ? void 0 : [args.harness],
1763
- cwd: args.cwd,
1764
- windowMs: args.window * 6e4,
1765
- intervalMs: args.interval * 1e3,
1766
- minLoopOccurrences: args.minLoop,
1767
- signal: controller.signal,
1768
- onLoop: (l) => {
1769
- const ts = (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
1770
- process.stdout.write(
1771
- `${ts} LOOP [${l.harness}] ${l.sessionId.slice(0, 8)}: \`${l.toolName}\` repeated \xD7${l.occurrences} with identical args (${(l.windowMs / 1e3).toFixed(0)}s)${l.cwd ? ` \xB7 ${l.cwd}` : ""}
1772
- `
1773
- );
1774
- },
1775
- onFinding: (finding) => {
1776
- process.stdout.write(`${formatLiveFinding(finding)}
2205
+ if (!args.once) process.stderr.write(`traces watch: ${scope}, every 5 s; read-only. Ctrl-C to stop.
2206
+ `);
2207
+ const ids = [];
2208
+ const reported = /* @__PURE__ */ new Set();
2209
+ let last;
2210
+ let findings = 0;
2211
+ const onEvent = (event) => {
2212
+ if (jsonl) process.stdout.write(`${JSON.stringify(event)}
2213
+ `);
2214
+ if (event.event === "session") ids.push(event.session.sessionId);
2215
+ else if (event.event === "finding") {
2216
+ findings += 1;
2217
+ const id = event.finding.session.sessionId;
2218
+ if (!jsonl) process.stdout.write(`${liveFindingLine(event.finding, shortestRefs(ids).get(id) ?? id)}
1777
2219
  `);
2220
+ } else {
2221
+ last = event;
2222
+ if (args.once) return;
2223
+ for (const unread2 of event.unreadSessions) {
2224
+ const key = unread2.sessionId ?? unread2.source;
2225
+ if (reported.has(key)) continue;
2226
+ reported.add(key);
2227
+ process.stderr.write(`unreadable ${key}: ${unread2.reason}
2228
+ `);
2229
+ }
1778
2230
  }
1779
- });
2231
+ };
2232
+ const loop = { once: args.once, signal: controller.signal, onEvent };
2233
+ if (source) await watchSource(() => spanRuns(args, source.path), loop);
2234
+ else {
2235
+ await streamSessions({
2236
+ adapters: adaptersFor(args),
2237
+ ...sessions ? { sessions } : {},
2238
+ ...named?.home !== void 0 ? { home: named.home } : {},
2239
+ cwd: args.cwd,
2240
+ ...windowMs !== void 0 ? { windowMs } : { sinceMs: parseSince(since) },
2241
+ ...loop
2242
+ });
2243
+ }
2244
+ if (!args.once || !last) return;
2245
+ const count = (n, noun) => `${n} ${noun}${n === 1 ? "" : "s"}`;
2246
+ if (!jsonl) process.stdout.write(`${scope}: ${count(last.sessions, "session")} read \xB7 ${count(findings, "finding")}
2247
+ `);
2248
+ const unread = last.unreadSessions.length;
2249
+ if (unread > 0) {
2250
+ process.stderr.write(`${unread} unreadable: traces watch \u2026 --once --format jsonl | jq 'select(.event == "tick") | .unreadSessions'
2251
+ `);
2252
+ }
1780
2253
  }
1781
2254
  async function cmdWatchRunTarget(dir, args) {
1782
- const target = await resolveRunWatchTarget(resolve(dir));
2255
+ const { resolveRunWatchTarget, watchRunTarget } = await import("./run-watch-PLWTIRSV.js");
2256
+ const target = await resolveRunWatchTarget(resolve2(dir));
1783
2257
  const controller = new AbortController();
1784
2258
  if (!args.once) {
1785
2259
  process.once("SIGINT", () => controller.abort());
1786
2260
  process.stderr.write(
1787
- `traces watch: tailing ${target.requested} every ${args.interval}s; read-only, ${target.runDir === null ? "span source only \u2014 Ctrl-C to stop" : "stops when result.json lands. Ctrl-C to stop"}.
2261
+ `traces watch: tailing ${target.requested} every 2s; read-only, ${target.runDir === null ? "span source only \u2014 Ctrl-C to stop" : "stops when result.json lands. Ctrl-C to stop"}.
1788
2262
  `
1789
2263
  );
1790
2264
  }
1791
- const snapshot = await watchRunTarget(target, {
1792
- once: args.once,
1793
- // A tree view wants a tighter poll than the session observer's 5s default:
1794
- // a blocking question is worth surfacing the moment it lands.
1795
- intervalMs: args.intervalExplicit ? Math.max(250, args.interval * 1e3) : 2e3,
1796
- signal: controller.signal
1797
- });
2265
+ const snapshot = await watchRunTarget(target, { once: args.once, intervalMs: 2e3, signal: controller.signal });
1798
2266
  const result = snapshot.journal?.result ?? null;
1799
2267
  if (result !== null && result.errorMessage !== null) process.exitCode = 1;
1800
2268
  }
2269
+ async function* refSessions(args) {
2270
+ const named = args.selection;
2271
+ if (!named) return;
2272
+ for (const { adapter, ref } of named.sessions) yield { ref, spans: await parseSession(adapter, ref) };
2273
+ for (const source of named.spans) {
2274
+ const collected = source.kind === "spans" && source.reader !== "otlp" ? await collectImportedSpans(source.path, source.reader, source.selector) : await collectOtlpSpans(source.path, source.kind === "spans" ? source.selector : void 0);
2275
+ const mtimeMs = (await stat(source.path)).mtimeMs;
2276
+ const byTrace = /* @__PURE__ */ new Map();
2277
+ for (const span of collected.spans) {
2278
+ const grouped = byTrace.get(span.trace_id);
2279
+ if (grouped) grouped.push(span);
2280
+ else byTrace.set(span.trace_id, [span]);
2281
+ }
2282
+ for (const [traceId, spans] of byTrace) {
2283
+ const { sessionByTrace } = indexSessionIdsByTrace(spans);
2284
+ const ref = {
2285
+ harness: collected.harness,
2286
+ sessionId: sessionByTrace.get(traceId) ?? traceId,
2287
+ path: source.path,
2288
+ cwd: args.cwd ?? null,
2289
+ mtimeMs
2290
+ };
2291
+ yield { ref, spans };
2292
+ }
2293
+ }
2294
+ }
1801
2295
  async function cmdUpload(args) {
1802
- const sinceMs = args.since ? parseSince(args.since) : Date.now() - 24 * 60 * 60 * 1e3;
1803
- const all = args.all || !args.harnessExplicit;
1804
- const plan = await planUpload({
1805
- all,
1806
- harnesses: args.harnessExplicit ? [args.harness] : void 0,
2296
+ const { executeUpload, planUpload, planUploadFrom } = await import("./upload-R6NMKSGQ.js");
2297
+ const named = args.selection;
2298
+ const fromRefs = named !== void 0 && named.sessions.length + named.spans.length > 0;
2299
+ if (!fromRefs && args.since === void 0) {
2300
+ throw new UsageError(`upload over a store root needs a window: traces upload ${named?.home ?? "<store-root>"} --since 24h`);
2301
+ }
2302
+ const sinceMs = fromRefs ? void 0 : parseSince(args.since);
2303
+ const all = !args.harnessExplicit;
2304
+ const unread = [];
2305
+ const plan = fromRefs ? await planUploadFrom(refSessions(args)) : await planUpload({
2306
+ ...named?.home !== void 0 ? { adapters: adaptersFor(args), home: named.home } : { all, harnesses: args.harnessExplicit ? [args.harness] : void 0 },
1807
2307
  cwd: args.cwd,
1808
- sinceMs
2308
+ sinceMs,
2309
+ onError: (error, ref) => {
2310
+ unread.push({ sessionId: ref?.sessionId ?? null, traceId: null, source: ref?.path ?? null, reason: errorText(error) });
2311
+ }
1809
2312
  });
1810
- const redactorParts = args.redactorCmd?.split(/\s+/).filter(Boolean) ?? [];
1811
- const redactor = redactorParts[0] ? commandRedactor({ command: redactorParts[0], args: redactorParts.slice(1) }) : void 0;
2313
+ warnUnread(args, unread);
1812
2314
  const candidates = plan.items;
1813
2315
  const byDetector = {};
1814
2316
  let totalRedactions = 0;
@@ -1817,7 +2319,9 @@ async function cmdUpload(args) {
1817
2319
  for (const [r, n] of Object.entries(i.redaction.byDetector)) byDetector[r] = (byDetector[r] ?? 0) + n;
1818
2320
  }
1819
2321
  const w = (s) => process.stderr.write(s);
1820
- w(`
2322
+ w(sinceMs === void 0 ? `
2323
+ Refs: ${args.refs.join(" ")}
2324
+ ` : `
1821
2325
  Window: since ${new Date(sinceMs).toISOString()}
1822
2326
  `);
1823
2327
  w(`Sessions found: ${plan.items.length} \xB7 final privacy-mode dedup runs before upload
@@ -1825,7 +2329,7 @@ Window: since ${new Date(sinceMs).toISOString()}
1825
2329
  w(`PII/secrets redacted: ${totalRedactions}${Object.keys(byDetector).length ? ` (${Object.entries(byDetector).map(([r, n]) => `${r}:${n}`).join(", ")})` : ""}
1826
2330
  `);
1827
2331
  for (const i of candidates.slice(0, 25)) {
1828
- w(` + [${i.ref.harness}] ${i.ref.sessionId.slice(0, 8)} ${i.spans.length} spans ${i.redaction.redactionCount} redacted ${i.ref.cwd ?? ""}
2332
+ w(` + [${i.ref.harness}] ${i.ref.sessionId.slice(0, 8)} ${i.spans.length} spans ${i.redaction.redactionCount} redacted ${args.noContent ? "" : i.ref.cwd ?? ""}
1829
2333
  `);
1830
2334
  }
1831
2335
  if (candidates.length > 25) w(` \u2026 and ${candidates.length - 25} more
@@ -1835,9 +2339,12 @@ Window: since ${new Date(sinceMs).toISOString()}
1835
2339
  return;
1836
2340
  }
1837
2341
  if (args.dryRun) {
1838
- const res2 = await executeUpload(plan, { dryRun: true, otlpOut: args.otlpOut, stripContent: args.noContent, redactor });
2342
+ const res2 = await executeUpload(plan, { dryRun: true, otlpOut: args.out, stripContent: args.noContent });
1839
2343
  reportRefused(res2.refused);
1840
- console.log(`dry run: ${candidates.length - res2.skippedSessions - res2.refused.length} session(s), ${totalRedactions} redaction(s). Redacted OTLP -> ${res2.otlpPath}`);
2344
+ if (res2.verdict) console.log(`share verdict: ${res2.verdict.status} (the redacted${args.noContent ? ", metadata-only" : ""} payload; UNSAFE and UNKNOWN are refused)`);
2345
+ console.log(
2346
+ `dry run: ${candidates.length - res2.skippedSessions - res2.refused.length} session(s), ${totalRedactions} redaction(s).` + (res2.otlpPath ? ` Redacted OTLP -> ${res2.otlpPath}` : " Pass --out FILE to keep the redacted OTLP.")
2347
+ );
1841
2348
  console.log("No upload performed. Set TANGLE_INGEST_URL / TANGLE_INGEST_API_KEY / TANGLE_TENANT_ID and drop --dry-run to send.");
1842
2349
  return;
1843
2350
  }
@@ -1849,7 +2356,7 @@ Window: since ${new Date(sinceMs).toISOString()}
1849
2356
  }
1850
2357
  }
1851
2358
  const res = await executeUpload(plan, { log: (m) => w(`${m}
1852
- `), stripContent: args.noContent, redactor });
2359
+ `), stripContent: args.noContent });
1853
2360
  reportRefused(res.refused);
1854
2361
  console.log(
1855
2362
  `Uploaded ${res.uploadedSessions} session(s), ${res.acceptedSpans} spans accepted, ${res.redactionCount} redaction(s); ${res.skippedSessions} already-uploaded skipped; ${res.refused.length} refused.`
@@ -1872,525 +2379,54 @@ function reportRefused(refused) {
1872
2379
  process.exitCode = 1;
1873
2380
  }
1874
2381
  }
1875
- async function cmdVerifySafe(argv) {
1876
- let target;
1877
- let format = "text";
1878
- let profile = "share";
1879
- const knownSecrets = [];
1880
- for (let i = 0; i < argv.length; i++) {
1881
- const arg = argv[i];
1882
- if (arg === "--format") format = argv[++i] ?? "";
1883
- else if (arg === "--profile") profile = argv[++i] ?? "";
1884
- else if (arg === "--known-secret-env") {
1885
- const name = argv[++i] ?? "";
1886
- const value = process.env[name];
1887
- if (!value) throw new Error(`verify-safe: environment variable ${name || "(missing name)"} is not set`);
1888
- knownSecrets.push(value);
1889
- } else if (arg === "--help" || arg === "-h") {
1890
- usageVerifySafe();
1891
- return;
1892
- } else if (arg.startsWith("--")) throw new Error(`verify-safe: unknown flag ${arg}`);
1893
- else if (target === void 0) target = arg;
1894
- else throw new Error(`verify-safe: unexpected argument ${arg}`);
1895
- }
1896
- if (!target) throw new Error("verify-safe needs a file or directory: traces verify-safe <file|dir>");
1897
- if (format !== "json" && format !== "text") throw new Error(`verify-safe: --format must be json or text, got "${format}"`);
1898
- if (profile !== "default" && profile !== "share" && profile !== "strict") {
1899
- throw new Error(`verify-safe: --profile must be default, share or strict, got "${profile}"`);
1900
- }
1901
- const result = await verifySafe(target, { profile, knownSecrets });
1902
- process.stdout.write(format === "json" ? `${JSON.stringify(result, null, 2)}
1903
- ` : `${formatVerdict(result)}
1904
- `);
1905
- process.exitCode = verdictExitCode(result);
1906
- }
1907
- async function cmdSkills(argv) {
1908
- let since;
1909
- let format = "text";
1910
- let unusedOnly = false;
1911
- let refresh = true;
1912
- let storePath = defaultSkillUsageStorePath();
1913
- let sweepThreshold = DEFAULT_SWEEP_THRESHOLD;
1914
- for (let i = 0; i < argv.length; i++) {
1915
- const arg = argv[i];
1916
- if (arg === "--since") since = parseSince(argv[++i] ?? "");
1917
- else if (arg === "--format") format = argv[++i] ?? "";
1918
- else if (arg === "--unused") unusedOnly = true;
1919
- else if (arg === "--no-refresh") refresh = false;
1920
- else if (arg === "--store") storePath = argv[++i] ?? "";
1921
- else if (arg === "--sweep") sweepThreshold = Number(argv[++i]);
1922
- else if (arg === "--help" || arg === "-h") {
1923
- usageSkills();
1924
- return;
1925
- } else throw new Error(`skills: unknown argument ${arg}`);
1926
- }
1927
- if (format !== "json" && format !== "text") throw new Error(`skills: --format must be json or text, got "${format}"`);
1928
- if (!storePath) throw new Error("skills: --store needs a path");
1929
- if (!Number.isInteger(sweepThreshold) || sweepThreshold < 1) throw new Error("skills: --sweep must be a positive integer");
2382
+ var SKILLS_SINCE = "30d";
2383
+ async function cmdSkills(args) {
2384
+ const since = parseSince(args.since ?? SKILLS_SINCE);
2385
+ const storePath = defaultSkillUsageStorePath();
1930
2386
  const store = await SkillUsageStore.load(storePath);
1931
- const refreshed = refresh ? await store.refresh() : null;
1932
- if (refreshed) await store.save();
1933
- const report = buildSkillUsageReport(store.list(), await readInstalledSkills(), storePath, { since, sweepThreshold, refresh: refreshed });
1934
- if (unusedOnly) {
1935
- if (format === "json") process.stdout.write(`${JSON.stringify(report.unused, null, 2)}
2387
+ const refreshed = await store.refresh();
2388
+ await store.save();
2389
+ const report = buildSkillUsageReport(store.list(), await readInstalledSkills(), storePath, { since, refresh: refreshed });
2390
+ const json = args.format === "json";
2391
+ if (args.unused) {
2392
+ if (json) process.stdout.write(`${JSON.stringify(report.unused, null, 2)}
1936
2393
  `);
1937
2394
  else for (const skill of report.unused) process.stdout.write(`${skill.skill} ${skill.installedIn.join(",")}
1938
2395
  `);
1939
2396
  return;
1940
2397
  }
1941
- process.stdout.write(format === "json" ? `${JSON.stringify(report, null, 2)}
2398
+ process.stdout.write(json ? `${JSON.stringify(report, null, 2)}
1942
2399
  ` : `${renderSkillUsage(report)}
1943
2400
  `);
1944
2401
  }
1945
- function usageSkills() {
1946
- console.log(`traces skills [--since 30d|ISO] [--format text|json] [--unused] [--no-refresh] [--store PATH] [--sweep N]
1947
-
1948
- Count skill use across Claude Code, Codex, and Kimi Code sessions on this machine, with no
1949
- model call. Each run reads only transcript bytes appended since the previous run and
1950
- keeps the events in a store (default ${defaultSkillUsageStorePath()}), so counts
1951
- survive a harness deleting old transcripts.
1952
-
1953
- model Claude Code Skill tool calls that loaded a skill
1954
- slash Claude Code /name commands that expanded to a skill or prompt
1955
- read Codex or Kimi commands that successfully read <skill>/SKILL.md
1956
-
1957
- --since count events at or after 30m / 2h / 7d or an ISO date (default: all stored)
1958
- --unused print only installed skills with no use in the window
1959
- --no-refresh answer from the store without reading transcripts
1960
- --store store path
1961
- --sweep one Codex or Kimi command reading N or more skills is a catalog sweep, not use;
1962
- its reads are excluded and counted separately (default ${DEFAULT_SWEEP_THRESHOLD})
1963
-
1964
- OpenCode, Pi, Gemini and other harnesses are not indexed.`);
1965
- }
1966
- function usageVerifySafe() {
1967
- console.log(`traces verify-safe <file|dir> [--profile default|share|strict] [--known-secret-env NAME]... [--format text|json]
1968
-
1969
- Check whether files are safe to share, without changing them. A directory is read
1970
- recursively. .json files are parsed whole, .jsonl and .ndjson line by line, and
1971
- any other file is scanned as text.
1972
-
1973
- SAFE nothing found exit 0
1974
- SAFE_WITH_WARNINGS only warnings (raw content, identifiers, media) exit 0
1975
- UNSAFE a credential, or personal data under share/strict exit 1
1976
- UNKNOWN a file, line or byte range could not be read exit 2
1977
-
1978
- --profile default | share (default) | strict; see agent-eval docs/redaction.md
1979
- --known-secret-env name of an environment variable holding a secret to look for in
1980
- any encoding (repeatable); the value is never printed
1981
- --format text (default) | json
1982
-
1983
- Findings name the file, line and JSON Pointer, never the matched value.`);
1984
- }
1985
- function usageExport() {
1986
- console.log(`traces export: convert trace evidence files to OpenInference JSONL
1987
-
1988
- Usage:
1989
- traces export <input.jsonl|input.json> --out <spans.jsonl> [--format auto]
1990
- traces export <input.json> --attr task.id=abc --attr arm=baseline --out spans.jsonl
1991
- traces export <input.json> --metadata run-metadata.json --out spans.jsonl
1992
- traces export <input.jsonl|input.json> > spans.jsonl
1993
-
1994
- Input formats:
1995
- policy-evidence Compact JSONL rows from \`traces evidence --out policy-evidence.jsonl\`
1996
- sandbox-events JSON arrays with start/raw/result/done/error events
1997
- openinference Existing OpenInference JSONL; rewrites through traces redaction
1998
- intelligence-spans JSONL rows exported from Tangle Intelligence trace spans
1999
- chat-trajectory A message array or objects with a messages array
2000
- auto Detect the format from the file contents (default)
2001
-
2002
- Examples:
2003
- traces evidence --all --since 24h --out policy-evidence.jsonl
2004
- traces export policy-evidence.jsonl --out spans.openinference.jsonl
2005
- traces export sandbox-events.json --format sandbox-events --out spans.openinference.jsonl
2006
- traces export intelligence-spans.jsonl --out spans.openinference.jsonl
2007
- traces export sandbox-events.json --attr task.id=aec-001 --attr outcome.score=1 --out spans.openinference.jsonl
2008
- halo spans.openinference.jsonl --prompt "Analyze this trace slice" --max-turns 1
2009
-
2010
- Safety:
2011
- export runs the same local redaction (agent-eval's redaction core) used by upload before writing spans.
2012
- --metadata must be a JSON object; --attr key=value is repeatable and overrides matching metadata keys.`);
2013
- }
2014
- function usageImportCodeTraceBench() {
2015
- console.log(`traces import-codetracebench: write one label-free OTLP file per trajectory
2016
-
2017
- Usage:
2018
- traces import-codetracebench <rows.jsonl|rows.json> \\
2019
- --trajectory-dir <normalized-dir> --out <new-trace-dir> \\
2020
- --revision <40-or-64-character-hex> [--concurrency 4]
2021
-
2022
- Input layout:
2023
- <normalized-dir>/<traj_id>/steps.json
2024
- <normalized-dir>/<traj_id>/task.md optional
2025
-
2026
- The command validates every native step, rejects annotation leakage, writes
2027
- <traj_id>.otlp.jsonl files, and records input and output SHA-256 hashes in
2028
- codetracebench-import.json. The output directory must not already exist.`);
2029
- }
2030
- function usageValidate() {
2031
- console.log(`traces validate: report what a trace can and cannot answer
2032
-
2033
- Usage:
2034
- traces validate <file.jsonl|dir> [--out conformance.md]
2035
-
2036
- Reads OTLP-JSONL and prints the conformance report from
2037
- @tangle-network/agent-trace-contract:
2038
-
2039
- - findings by severity, each naming the CONSEQUENCE of the defect and the
2040
- capabilities it blocks
2041
- - the capability table: token-accounting, cost-attribution, tool-usage,
2042
- loop-convergence, tree-comparison, steering-chain, latency-analysis \u2014
2043
- available or not, with the trace's own reason
2044
- - rows that could not be read at all, with the line and the reason
2045
-
2046
- A directory reads the OTLP files under it, recursively \u2014 only the otlp/
2047
- subdirectory when the producer made one. A run directory's raw event, stream and
2048
- SDK logs are also *.jsonl; they are listed with what they actually hold, not read
2049
- as several hundred broken spans. Identical re-declarations of a span across
2050
- per-shot files are counted as repeats, not defects; only a repeat with DIFFERENT
2051
- content is an error.
2052
-
2053
- Exit code:
2054
- 0 no error findings, and at least one capability available (warnings mean
2055
- thinner analysis, not wrong analysis)
2056
- 1 an error finding, or nothing analysable at all \u2014 an empty file, or one
2057
- whose every line was unreadable, answers no question and is not a pass
2058
-
2059
- Nothing is fixed up: a span with an unreadable timestamp is reported, never
2060
- repaired, because a synthesized value puts invented work into a total.
2061
-
2062
- Examples:
2063
- traces validate spans.otlp.jsonl
2064
- traces validate results/sessions --out conformance.md
2065
- traces validate spans.otlp.jsonl && traces analyze --otlp spans.otlp.jsonl`);
2402
+ function prepare(args) {
2403
+ return validateLatestTurn(applyCurrentSessionSelection(args));
2066
2404
  }
2067
- function usage() {
2068
- console.log(`traces: analyze agent traces \u2014 yours, or any system's
2069
-
2070
- Two ways in:
2071
- --otlp <file|dir> Spans a system already emitted in the
2072
- @tangle-network/agent-trace-contract shape. No adapter, no
2073
- translation. This is how you integrate a NEW system.
2074
- --harness <id> Run an adapter over a coding agent's proprietary session
2075
- store (Claude Code, Codex, OpenCode, \u2026) \u2014 the legacy edge,
2076
- for formats we do not control.
2077
-
2078
- Commands:
2079
- validate Report what a trace can and cannot answer (exit 1 on error findings)
2080
- check Gate a run on a trace contract: check <trace> --contract <file.json>
2081
- [--junit out.xml] [--out report.json] [--evidence dir] [--annotations].
2082
- check --contract <file.json> --explain prints what the contract checks.
2083
- <trace> is claude -p stream-json output, OTLP spans (file or dir),
2084
- a Claude Code or Codex session file, or trace evidence; --format
2085
- names it when the sniff cannot.
2086
- Exit 0 pass, 1 fail, 2 bad contract or a rule could not run,
2087
- 3 unreadable trace, 4 ambiguous trace reference
2088
- list List discovered sessions
2089
- analyze Run analyst suite + loop/waste pipelines over OTLP (--otlp), sessions,
2090
- or an input file; names every analysis the spans could not support
2091
- (--supervisor-run-dir <dir> reports a supervision tree instead)
2092
- investigate Run typed investigation flow, including BYO config + evidence-backed actions
2093
- improve Write findings, evidence, report, and canonical trace artifacts
2094
- ask Answer free-form questions over the selected sessions with the
2095
- model-backed engine: questions run concurrently under one budget,
2096
- every trace:// citation is checked, and answers.json + report.md
2097
- are written to --dir (exit 1 when any question fails)
2098
- facts Print the deterministic session-facts sheet for the selected
2099
- sessions: tool calls excluding synthesized spans, subagent spawns
2100
- with task names, human turns in order, the final message per task,
2101
- changed paths, first/last record times, the harness token total, and
2102
- the first failure by agent-eval's fixed precedence (or none), the
2103
- session's recorded working directory, and for each human turn the
2104
- agent message it answered.
2105
- No model call, no budget, $0. Every fact names the span ids it came
2106
- from; a fact the spans cannot support is null with its reason.
2107
- --since/--until select every session with a record in the window and
2108
- count only that window's turns; a session that cannot be read is named
2109
- in selection.unreadSessions. Without a window: exit 1 when a selected
2110
- session cannot be read. --format json (default) or text
2111
- skills Count skill use across Claude Code, Codex, and Kimi Code sessions on this
2112
- machine, and list installed skills with no use in the window.
2113
- Incremental: a run reads only transcript bytes appended since the
2114
- last run. No model call. traces skills --help
2115
- convert Emit OTLP-JSONL only, to --otlp-out (HALO: use analyze --analyzer halo)
2116
- index Emit a reusable session index JSON for later investigation
2117
- bundle Assemble one session's durable evidence directory: transcript +
2118
- subagents, derived index/report/evidence/OTLP, the repo's .evolve
2119
- ledger sliced to the session window, git log, and a sha256 manifest
2120
- (needs --session <id|path> and --out <new-dir>). This is the FULL
2121
- view: it holds the session's own words, for a reader who cites them
2122
- mcp Serve the selected traces to an MCP client over stdio: 7 read-only,
2123
- idempotent trace tools; spans and results redacted, results capped
2124
- and marked untrusted (traces mcp --help)
2125
- diff <a> <b>
2126
- Pair two runs' steps (by span id, then position with the same name
2127
- and kind, then name and kind anywhere) and mark the first divergence:
2128
- changed, replaced, only-in-a, only-in-b or reordered. Each side is an
2129
- OTLP file or directory, any file convert reads, file#<trace id>, or
2130
- file#branch=<id> for one arm (agent.branch.id or agent.branch.arm).
2131
- --kind TOOL (repeatable) keeps only steps of that span kind.
2132
- Exit 0 agree, 1 diverge, 2 unreadable, 3 no recorded outcome to
2133
- call agreement on (--format text|json)
2134
- bundle verify <bundle-dir>
2135
- Check a bundle against its manifest: every listed file present with
2136
- its recorded size and SHA-256, no unlisted or non-regular file beside
2137
- them, and for a view, the source manifest it was projected from.
2138
- Exit 1 on any difference (--format text|json)
2139
- bundle-view <bundle-dir>
2140
- Project an assembled bundle into a narrower view for a consumer that
2141
- must NOT read the session's own words. --view evidence-only carries
2142
- the counted artifacts and structured ledger rows, excludes the
2143
- transcript, the report, the spans and every prose file by name, and
2144
- refuses to write a view whose text repeats an excluded source
2145
- inspect Read a session index and print ranked improvement findings
2146
- export Convert evidence/events files to OpenInference JSONL for HALO
2147
- import-codetracebench
2148
- Convert CodeTracer-normalized CodeTraceBench trajectories in bulk
2149
- replay-verify
2150
- Replay a trajectory prefix in a sandbox, execute step k both recorded
2151
- and corrected, and emit an executed-proof verdict (--help for flags)
2152
- replay-verify-batch
2153
- Measure replayability + fix-flip rates across gold-labeled corpora:
2154
- arm A per replayable case, LLM-generated arm-B fixes (--help for flags)
2155
- verify-findings
2156
- Execute recorded analyst findings as sandbox replay proofs: each
2157
- finding \u2192 reproduced | fix-flipped | divergent | not-replayable,
2158
- with a receipt directory per finding (--help for flags)
2159
- evidence Emit compact session-evidence JSONL for downstream policy miners
2160
- stream Emit JSONL trace stream events for live visualizers or replay
2161
- watch Online observer: tail active sessions, notify on loops + semantic findings
2162
- (watch <target> tails ONE run tree instead: a run directory or a span file)
2163
- upload Redact + upload sessions in a time window to the Tangle Intelligence Platform;
2164
- a session whose final events are UNSAFE or UNKNOWN is refused (exit 1)
2165
- verify-safe <file|dir>
2166
- Say whether files are safe to share: SAFE, SAFE_WITH_WARNINGS, UNSAFE
2167
- (exit 1) or UNKNOWN (exit 2). Read-only (--help for flags)
2168
-
2169
- Options:
2170
- --harness <id> Harness or alias (default: claude-code). Known: ${knownHarnesses().join(", ")}
2171
- --all Sweep every known harness
2172
- --last <n> Most-recent N sessions
2173
- --current Analyze the active Codex session named by CODEX_THREAD_ID
2174
- --latest-turn Limit a resumed Codex or Claude Code session to its latest task turn
2175
- --session <id|path> Analyze one listed session ID or one explicit harness session file
2176
- --workflow Expand selected sessions through stable parent/child session IDs
2177
- --max-workflow-sessions <n>
2178
- Stop workflow expansion before parsing more than N files (default 100)
2179
- --cwd <dir> Filter sessions by working directory
2180
- --supervisor-run-dir <dir>
2181
- analyze: report a SUPERVISION TREE instead of harness sessions \u2014
2182
- steers, spawn waves, concurrency, idle wall, cost by role,
2183
- accepted vs rejected. Rolls up when the dir holds many runs.
2184
- analyze reads Runtime journals and recorded startup failures.
2185
- watch reads a live Runtime journal and OTLP span files.
2186
- --since <t> 30m / 2h / 7d (before now) or an ISO date. Selection commands read
2187
- sessions modified at or after it; upload: its window (default 24h).
2188
- facts: opens a time window and selects EVERY session with a record
2189
- in it, not only the newest; its turn facts keep only turns inside it
2190
- --until <t> facts: closes the window (exclusive), same forms as --since
2191
- --out <path> Write report to a file
2192
- --dir <path> improve/ask: write artifacts to this directory
2193
- --otlp <file|dir> READ OTLP-JSONL emitted by any system, skipping the adapters.
2194
- A directory reads the OTLP files under it \u2014 only the otlp/
2195
- subdirectory when the producer made one \u2014 and names the JSONL
2196
- that is not OTLP instead of reading it as broken spans.
2197
- Supported by: validate, analyze, facts, investigate, improve, ask,
2198
- stream, mcp. Writing commands take --otlp-out.
2199
- --source-bundle <dir> Analyze a retained full bundle; explicitly grant source-field reads.
2200
- Available for analyze, investigate, improve, and ask.
2201
- --otlp-out <path> WRITE the OTLP-JSONL artifact here (also evidence
2202
- provenance / dry-run upload preview)
2203
- --format <kind> analyze/export: auto | policy-evidence | sandbox-events | openinference | intelligence-spans | chat-trajectory
2204
- facts: json (default) | text
2205
- --metadata <json> analyze/export file: attach JSON object fields as span attributes
2206
- --attr <k=v> analyze/export file: attach one span attribute (repeatable)
2207
- --mode <kind> stream: visualizer | findings | agent (default visualizer)
2208
- --view <name> bundle-view: which projection to write. evidence-only is the
2209
- writer's view; the full view is the bundle itself, so there is
2210
- nothing to project into it
2211
- --replay, --once stream: scan once and exit (default for positional input / --session)
2212
- --once watch <target>: print ONE snapshot and exit, for scripts and agents
2213
- --no-spans stream: omit per-span pulse events
2214
- --no-findings stream: omit semantic live-finding events
2215
- --llm Enable agentic RLM analysts. Runs on the Tangle router by
2216
- default (TANGLE_API_KEY); OPENAI_API_KEY alone targets OpenAI,
2217
- OPENAI_BASE_URL overrides the endpoint. Needs Python with
2218
- agent-eval-rpc[dspy] (TRACES_PYTHON selects the interpreter),
2219
- version-matched to this package's @tangle-network/agent-eval.
2220
- Exits 1 when every agentic analyst fails, with each reason.
2221
- ask uses the same engine and credentials without --llm.
2222
- --model <id> Model for --llm, HALO, and Hodoscope (default for --llm: ${DEFAULT_ANALYST_MODEL})
2223
- --config <path> investigate/improve/stream: JS config with analysts, liveAnalysts, or external analyzers
2224
- --budget <usd> USD cap for agentic analysts; ask: one ceiling shared by every question
2225
- --question <text> ask: a question (repeatable, at most ${MAX_TRACE_QUESTION_CHARS} characters)
2226
- --questions <file> ask: JSON array of questions, strings or
2227
- { id?, question, instructions?, answerSchema? }
2228
- --question-budget <usd>
2229
- ask: provider ceiling for one question (default: the smaller
2230
- of --budget and $${DEFAULT_QUESTION_MAX_COST_USD})
2231
- --question-timeout <seconds>
2232
- ask: wall-clock deadline for one question's investigation
2233
- (default: ${DEFAULT_QUESTION_TIMEOUT_MS / 1e3})
2234
- --concurrency <n> ask: questions running at once (default 4);
2235
- import-codetracebench: trajectories imported at once
2236
- --analyzer <id> analyze: also run halo, hodoscope, prime, or an installed command (repeatable);
2237
- exits 1 after writing the report when any requested analyzer fails
2238
- prime posts the full span projection to an OpenAI-compatible bridge
2239
- (TRACES_PRIME_BRIDGE_URL, default http://localhost:4181;
2240
- TRACES_PRIME_MODEL, default prime/zai/glm-5.2; TRACES_PRIME_TIMEOUT_MS)
2241
- --analyzer-prompt <p> analyze: prompt passed to external analyzers (default: diagnose)
2242
- --verify-findings analyze: execute the findings as sandbox replay proofs and mark
2243
- each VERIFIED (receipt path) or UNVERIFIABLE (reason). Needs
2244
- --replay-corpus + a running sandbox (SANDBOX_API_KEY/_URL)
2245
- --replay-corpus name=<labels.json>::<preparedDir>
2246
- trajectory source for --verify-findings (repeatable)
2247
- --verify-out <dir> receipt root for --verify-findings (default: <--out>.verify)
2248
- --interval <s> watch/stream: poll interval seconds (sessions 5, run tree 2)
2249
- --window <m> watch/stream: only sessions active in the last N minutes (default 30)
2250
- --min-loop <n> Min identical repeated calls to flag a loop (default 3)
2251
- --dry-run upload: redact + dedup + preview, write OTLP, but do NOT send
2252
- --no-content facts: omit conversation, commands, paths and assignments;
2253
- upload: strip prompt/response text; send metadata only
2254
- --redactor <cmd> upload: external PII scrubber (JSON array stdin\u2192stdout) after the redaction core
2255
- --yes, -y upload: skip the confirmation prompt
2256
- --version, -v Print the installed traces version
2257
- --help, -h Show help (use \`traces validate --help\`, \`traces export --help\`,
2258
- or \`traces import-codetracebench --help\`)
2259
-
2260
- Watch a run tree (any depth; driver spend and child spend stay separate,
2261
- and a cost the harness never priced reads "unknown", never $0):
2262
- traces watch runs/my-run live tree: status, budget, driver vs child spend
2263
- traces watch runs/my-run --once one snapshot, then exit
2264
- traces watch traces.otlp.jsonl OTLP spans only \u2014 works for ANY emitter
2265
- traces analyze --supervisor-run-dir runs/my-run the full tree report
2266
-
2267
- Upload env: TANGLE_INGEST_URL (or TANGLE_ORCHESTRATOR_URL), TANGLE_INGEST_API_KEY (or TANGLE_API_KEY), TANGLE_TENANT_ID`);
2405
+ function reading(handler) {
2406
+ return async (args) => handler(await withSelection(prepare(args)));
2268
2407
  }
2269
- async function main() {
2270
- const rawArgs = process.argv.slice(2);
2271
- if (rawArgs[0] === "--version" || rawArgs[0] === "-v" || rawArgs[0] === "version") {
2272
- console.log(`traces ${tracesVersion()}`);
2273
- return;
2274
- }
2275
- if (rawArgs[0] === "replay-verify") {
2276
- await cmdReplayVerify(rawArgs.slice(1));
2277
- return;
2278
- }
2279
- if (rawArgs[0] === "replay-verify-batch") {
2280
- await cmdReplayVerifyBatch(rawArgs.slice(1));
2281
- return;
2282
- }
2283
- if (rawArgs[0] === "verify-findings") {
2284
- await cmdVerifyFindings(rawArgs.slice(1));
2285
- return;
2286
- }
2287
- if (rawArgs[0] === "mcp") {
2288
- await cmdMcp(parseArgs(rawArgs));
2289
- return;
2290
- }
2291
- if (rawArgs[0] === "diff") {
2292
- try {
2293
- await cmdDiff(rawArgs.slice(1));
2294
- } catch (err) {
2295
- process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
2296
- `);
2297
- process.exitCode = 2;
2298
- }
2299
- return;
2300
- }
2301
- if (rawArgs[0] === "bundle" && rawArgs[1] === "verify") {
2302
- await cmdBundleVerify(rawArgs.slice(2));
2303
- return;
2304
- }
2305
- if (rawArgs[0] === "verify-safe") {
2306
- await cmdVerifySafe(rawArgs.slice(1));
2307
- return;
2308
- }
2309
- if (rawArgs[0] === "skills") {
2310
- await cmdSkills(rawArgs.slice(1));
2311
- return;
2312
- }
2313
- const parsedArgs = parseArgs(rawArgs);
2314
- if (parsedArgs.help) {
2315
- if (parsedArgs.command === "import-codetracebench" || parsedArgs.command === "help" && parsedArgs.input === "import-codetracebench") usageImportCodeTraceBench();
2316
- else if (parsedArgs.command === "export" || parsedArgs.command === "help" && parsedArgs.input === "export") usageExport();
2317
- else if (parsedArgs.command === "validate" || parsedArgs.command === "help" && parsedArgs.input === "validate") usageValidate();
2318
- else usage();
2319
- return;
2320
- }
2321
- const args = validateOtlpSelection(
2322
- validateViewSelection(validateWorkflowSelection(validateUntilSelection(applyCurrentSessionSelection(parsedArgs))))
2323
- );
2324
- switch (args.command) {
2325
- case "help":
2326
- if (args.input === "import-codetracebench") usageImportCodeTraceBench();
2327
- else if (args.input === "export") usageExport();
2328
- else if (args.input === "validate") usageValidate();
2329
- else usage();
2330
- break;
2331
- case "list":
2332
- await cmdList(args);
2333
- break;
2334
- case "analyze":
2335
- await cmdAnalyze(args);
2336
- break;
2337
- case "validate":
2338
- await cmdValidate(args);
2339
- break;
2340
- case "check":
2341
- await cmdCheck(args);
2342
- break;
2343
- case "investigate":
2344
- await cmdInvestigate(args);
2345
- break;
2346
- case "improve":
2347
- await cmdImprove(args);
2348
- break;
2349
- case "ask":
2350
- await cmdAsk(args);
2351
- break;
2352
- case "facts":
2353
- await cmdFacts(args);
2354
- break;
2355
- case "convert":
2356
- await cmdConvert(args);
2357
- break;
2358
- case "index":
2359
- await cmdIndex(args);
2360
- break;
2361
- case "bundle":
2362
- await cmdBundle(args);
2363
- break;
2364
- case "bundle-view":
2365
- await cmdBundleView(args);
2366
- break;
2367
- case "inspect":
2368
- await cmdInspect(args);
2369
- break;
2370
- case "export":
2371
- await cmdExport(args);
2372
- break;
2373
- case "import-codetracebench":
2374
- await cmdImportCodeTraceBench(args);
2375
- break;
2376
- case "evidence":
2377
- await cmdEvidence(args);
2378
- break;
2379
- case "stream":
2380
- await cmdStream(args);
2381
- break;
2382
- case "watch":
2383
- await cmdWatch(args);
2384
- break;
2385
- case "upload":
2386
- await cmdUpload(args);
2387
- break;
2388
- default:
2389
- usage();
2390
- }
2391
- }
2392
- main().catch((err) => {
2393
- process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
2394
- `);
2395
- process.exitCode = process.argv[2] === "check" ? CHECK_EXIT.badContract : 1;
2408
+ var NODE_SQLITE_WARNING = "SQLite is an experimental feature and might change at any time";
2409
+ var emitWarning = process.emitWarning.bind(process);
2410
+ process.emitWarning = ((warning, ...rest) => {
2411
+ const type = typeof rest[0] === "string" ? rest[0] : rest[0]?.type;
2412
+ const text = typeof warning === "string" ? warning : warning.message;
2413
+ if (type === "ExperimentalWarning" && text === NODE_SQLITE_WARNING) return;
2414
+ emitWarning(warning, ...rest);
2415
+ });
2416
+ await runTraces(process.argv.slice(2), {
2417
+ list: reading(cmdList),
2418
+ facts: reading(cmdFacts),
2419
+ analyze: reading(cmdAnalyze),
2420
+ ask: reading(cmdAsk),
2421
+ // check resolves its refs itself, after its contracts load, and --share-safe
2422
+ // reads paths as bytes: a bad contract exits before any trace is read.
2423
+ check: async (args) => cmdCheck(prepare(args)),
2424
+ diff: cmdDiff,
2425
+ watch: reading(cmdWatch),
2426
+ bundle: reading(cmdBundle),
2427
+ "bundle verify": cmdBundleVerify,
2428
+ convert: reading(cmdConvert),
2429
+ upload: reading(cmdUpload),
2430
+ skills: cmdSkills,
2431
+ mcp: reading(cmdMcp)
2396
2432
  });