@tangle-network/traces 0.15.4 → 0.16.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.
- package/README.md +130 -13
- package/dist/{chunk-SKOW5V5U.js → chunk-G5KKIQZ6.js} +578 -283
- package/dist/cli.js +606 -42
- package/dist/index.d.ts +140 -32
- package/dist/index.js +27 -9
- package/docs/trace-analysts.md +1 -1
- package/package.json +4 -3
package/dist/cli.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
appendAll,
|
|
9
9
|
assembleSessionBundle,
|
|
10
10
|
assertOutsideSourceBundle,
|
|
11
|
+
assessSpans,
|
|
11
12
|
buildPolicyEvidenceRecord,
|
|
12
13
|
buildSessionFactsReport,
|
|
13
14
|
buildSessionIndexFromRows,
|
|
@@ -17,11 +18,13 @@ import {
|
|
|
17
18
|
collectSessionSelection,
|
|
18
19
|
commandAnalyzer,
|
|
19
20
|
commandRedactor,
|
|
21
|
+
diffRuns,
|
|
20
22
|
executeUpload,
|
|
21
23
|
exportTraceEvidenceFile,
|
|
22
24
|
externalFailureMessage,
|
|
23
25
|
fileRunContextSupervisorRunReader,
|
|
24
26
|
findingRejectionDetail,
|
|
27
|
+
formatVerdict,
|
|
25
28
|
haloAnalyzer,
|
|
26
29
|
hodoscopeAnalyzer,
|
|
27
30
|
importCodeTraceBench,
|
|
@@ -34,14 +37,19 @@ import {
|
|
|
34
37
|
locateSessions,
|
|
35
38
|
mergeTracesConfig,
|
|
36
39
|
normalizeTraceQuestions,
|
|
40
|
+
openAgenticTraceStore,
|
|
41
|
+
parseIsoToEpochMs,
|
|
37
42
|
parseSession,
|
|
38
43
|
parseSince,
|
|
39
44
|
planUpload,
|
|
40
45
|
primeAnalyzer,
|
|
41
46
|
projectSessionBundle,
|
|
47
|
+
readJsonl,
|
|
42
48
|
readOtlpInput,
|
|
43
49
|
readSessionIndexFile,
|
|
50
|
+
redactSpans,
|
|
44
51
|
renderInspectionReport,
|
|
52
|
+
renderRunDiff,
|
|
45
53
|
renderSessionFacts,
|
|
46
54
|
renderValidation,
|
|
47
55
|
resolveAdapter,
|
|
@@ -57,26 +65,31 @@ import {
|
|
|
57
65
|
serializeTraceStreamEvent,
|
|
58
66
|
sessionReportSource,
|
|
59
67
|
streamSessions,
|
|
68
|
+
toolArgumentsFromAttributes,
|
|
60
69
|
totalAgenticFailureMessage,
|
|
61
70
|
traceStreamEventsFromSpans,
|
|
71
|
+
tracesVersion,
|
|
62
72
|
validationExitCode,
|
|
73
|
+
verdictExitCode,
|
|
63
74
|
verifyFindingsInSandbox,
|
|
75
|
+
verifySafe,
|
|
64
76
|
verifySessionBundle,
|
|
65
77
|
watchRunTarget,
|
|
66
78
|
watchSessions,
|
|
79
|
+
writeAnalysisTraceFile,
|
|
67
80
|
writeInspectionReportFile,
|
|
68
81
|
writeOtlpFile,
|
|
69
82
|
writePolicyEvidenceFile,
|
|
70
83
|
writeSessionIndexFile,
|
|
71
84
|
writeTraceEvidenceExportFile,
|
|
72
85
|
writeTraceQuestionsArtifacts
|
|
73
|
-
} from "./chunk-
|
|
86
|
+
} from "./chunk-G5KKIQZ6.js";
|
|
74
87
|
|
|
75
88
|
// src/cli.ts
|
|
76
89
|
import { readFileSync } from "fs";
|
|
77
|
-
import { mkdir, mkdtemp, readFile, stat, writeFile } from "fs/promises";
|
|
90
|
+
import { mkdir as mkdir2, mkdtemp, readFile as readFile2, stat, writeFile as writeFile2 } from "fs/promises";
|
|
78
91
|
import { tmpdir } from "os";
|
|
79
|
-
import { basename, join, resolve } from "path";
|
|
92
|
+
import { basename, join as join2, resolve } from "path";
|
|
80
93
|
import {
|
|
81
94
|
parseCorpusFlag,
|
|
82
95
|
renderVerifiedFindingsSection
|
|
@@ -89,11 +102,306 @@ import {
|
|
|
89
102
|
renderSupervisorRunMarkdown,
|
|
90
103
|
rollupSupervisorRuns
|
|
91
104
|
} from "@tangle-network/agent-eval/supervisor-run";
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
105
|
+
|
|
106
|
+
// src/mcp.ts
|
|
107
|
+
import { rm } from "fs/promises";
|
|
108
|
+
import { dirname } from "path";
|
|
109
|
+
import { buildTraceAnalysisToolDescriptors, redact, shareAllowed, UNTRUSTED_TRACE_TEXT } from "@tangle-network/agent-eval/traces";
|
|
110
|
+
import { createStdioToolServer } from "@tangle-network/agent-runtime/mcp";
|
|
111
|
+
var MCP_RESULT_BYTE_CAP = 512 * 1024;
|
|
112
|
+
async function traceMcpTools(options) {
|
|
113
|
+
const { spans } = redactSpans(options.spans);
|
|
114
|
+
const verdict = assessSpans(spans);
|
|
115
|
+
if (!shareAllowed(verdict)) {
|
|
116
|
+
const findings = verdict.findings.map((f) => `${f.category}/${f.detector} (${f.count})`).join(", ");
|
|
117
|
+
throw new Error(
|
|
118
|
+
`mcp: refusing to serve \u2014 redacted spans are still ${verdict.status}` + (findings ? `: ${findings}` : "") + (verdict.unreadable.length > 0 ? `; unreadable: ${verdict.unreadable.join("; ")}` : "")
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
const file = await writeAnalysisTraceFile(spans);
|
|
122
|
+
const store = await openAgenticTraceStore(file);
|
|
123
|
+
const descriptors = buildTraceAnalysisToolDescriptors({ store }).filter((d) => d.name !== "readSpanSource");
|
|
124
|
+
const tools = descriptors.map((descriptor) => ({
|
|
125
|
+
name: descriptor.name,
|
|
126
|
+
description: descriptor.description,
|
|
127
|
+
inputSchema: descriptor.parameters,
|
|
128
|
+
annotations: {
|
|
129
|
+
readOnlyHint: descriptor.readOnly,
|
|
130
|
+
idempotentHint: descriptor.idempotent,
|
|
131
|
+
destructiveHint: false,
|
|
132
|
+
openWorldHint: false
|
|
133
|
+
},
|
|
134
|
+
handler: async (args) => {
|
|
135
|
+
const { value } = redact(await descriptor.handler(args));
|
|
136
|
+
const result = { untrusted: UNTRUSTED_TRACE_TEXT, result: value };
|
|
137
|
+
const bytes = Buffer.byteLength(JSON.stringify(result));
|
|
138
|
+
if (bytes > MCP_RESULT_BYTE_CAP) {
|
|
139
|
+
throw new RangeError(
|
|
140
|
+
`${descriptor.name}: result is ${bytes} bytes, above the ${MCP_RESULT_BYTE_CAP}-byte cap; narrow the request`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
}));
|
|
146
|
+
return { tools, otlpPath: file.otlpPath };
|
|
147
|
+
}
|
|
148
|
+
async function serveTraceMcp(options) {
|
|
149
|
+
const { tools, otlpPath } = await traceMcpTools(options);
|
|
150
|
+
const server = createStdioToolServer({ serverName: "traces", serverVersion: tracesVersion(), tools });
|
|
151
|
+
try {
|
|
152
|
+
await server.serve();
|
|
153
|
+
} finally {
|
|
154
|
+
await rm(dirname(otlpPath), { recursive: true, force: true });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/cli.ts
|
|
159
|
+
import { explainTraceContract as explainTraceContract2 } from "@tangle-network/agent-eval";
|
|
160
|
+
|
|
161
|
+
// src/check.ts
|
|
162
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
163
|
+
import { join } from "path";
|
|
164
|
+
import {
|
|
165
|
+
compileTraceContractSpec,
|
|
166
|
+
evaluateTraceContract,
|
|
167
|
+
explainTraceContract,
|
|
168
|
+
lintTraceContractSpec
|
|
169
|
+
} from "@tangle-network/agent-eval";
|
|
170
|
+
var CHECK_EXIT = Object.freeze({
|
|
171
|
+
pass: 0,
|
|
172
|
+
fail: 1,
|
|
173
|
+
/** The contract is malformed or contradicts itself, or a rule could not run. */
|
|
174
|
+
badContract: 2,
|
|
175
|
+
unreadable: 3,
|
|
176
|
+
ambiguous: 4
|
|
177
|
+
});
|
|
178
|
+
var AmbiguousTraceInputError = class extends Error {
|
|
179
|
+
name = "AmbiguousTraceInputError";
|
|
180
|
+
};
|
|
181
|
+
var ContractFileError = class extends Error {
|
|
182
|
+
name = "ContractFileError";
|
|
183
|
+
};
|
|
184
|
+
async function loadContract(path) {
|
|
185
|
+
let text;
|
|
186
|
+
try {
|
|
187
|
+
text = await readFile(path, "utf8");
|
|
188
|
+
} catch (error) {
|
|
189
|
+
throw new ContractFileError(`cannot read contract ${path}: ${errorText(error)}`);
|
|
190
|
+
}
|
|
191
|
+
let json;
|
|
192
|
+
try {
|
|
193
|
+
json = JSON.parse(text);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
throw new ContractFileError(`contract ${path} is not JSON: ${errorText(error)}`);
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const findings = lintTraceContractSpec(json);
|
|
199
|
+
const errors = findings.filter((f) => f.level === "error");
|
|
200
|
+
if (errors.length > 0) {
|
|
201
|
+
throw new ContractFileError(
|
|
202
|
+
`contract ${path} contradicts itself:
|
|
203
|
+
${errors.map((f) => ` ${f.path}: ${f.message} [${f.code}]`).join("\n")}`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
contract: compileTraceContractSpec(json),
|
|
208
|
+
warnings: findings.map((f) => `${f.path}: ${f.message} [${f.code}]`)
|
|
209
|
+
};
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (error instanceof ContractFileError) throw error;
|
|
212
|
+
throw new ContractFileError(`contract ${path}: ${errorText(error)}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
var SNIFF_ROWS = 20;
|
|
216
|
+
async function sniffCheckInput(path) {
|
|
217
|
+
const rows = [];
|
|
218
|
+
for await (const row of readJsonl(path)) {
|
|
219
|
+
rows.push(row);
|
|
220
|
+
if (rows.length >= SNIFF_ROWS) break;
|
|
221
|
+
}
|
|
222
|
+
const objects = rows.filter(isObject);
|
|
223
|
+
if (objects.length === 0) throw new Error(`${path} holds no JSON rows`);
|
|
224
|
+
const kinds = [];
|
|
225
|
+
if (objects.every((r) => typeof r.trace_id === "string" && typeof r.span_id === "string")) kinds.push("otlp");
|
|
226
|
+
if (objects.some((r) => (typeof r.sessionId === "string" || typeof r.session_id === "string") && typeof r.type === "string") && !objects.some(hasSpanId)) {
|
|
227
|
+
kinds.push("claude-code");
|
|
228
|
+
}
|
|
229
|
+
if (objects.some((r) => CODEX_LINE_TYPES.has(String(r.type)) && isObject(r.payload))) kinds.push("codex");
|
|
230
|
+
if (objects.every(isEvidenceRow)) kinds.push("evidence");
|
|
231
|
+
if (kinds.length === 1) return kinds[0];
|
|
232
|
+
if (kinds.length === 0) {
|
|
233
|
+
throw new Error(`${path} is not a trace this command reads (OTLP spans, a Claude Code or Codex session, or trace evidence)`);
|
|
234
|
+
}
|
|
235
|
+
throw new AmbiguousTraceInputError(
|
|
236
|
+
`${path} reads as ${kinds.join(" and ")}; pass --format ${kinds.join(" or --format ")}`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
var CODEX_LINE_TYPES = /* @__PURE__ */ new Set(["session_meta", "response_item", "event_msg", "turn_context"]);
|
|
240
|
+
function hasSpanId(row) {
|
|
241
|
+
return typeof row.span_id === "string";
|
|
242
|
+
}
|
|
243
|
+
function isEvidenceRow(row) {
|
|
244
|
+
if (hasSpanId(row)) return false;
|
|
245
|
+
if (row.kind === "traces.policy_evidence.session") return true;
|
|
246
|
+
if (Array.isArray(row.messages) || typeof row.role === "string" && "content" in row) return true;
|
|
247
|
+
return typeof row.trace_id === "string" && typeof row.name === "string" && isObject(row.attributes);
|
|
248
|
+
}
|
|
249
|
+
function contractSpansFromOtlp(spans) {
|
|
250
|
+
return spans.map((span) => {
|
|
251
|
+
const startedAt = epochMs(span.start_time);
|
|
252
|
+
const endedAt = epochMs(span.end_time);
|
|
253
|
+
const args = toolArgumentsFromAttributes(span.attributes);
|
|
254
|
+
const truncated = span.attributes["traces.input.truncated"] === true;
|
|
255
|
+
return {
|
|
256
|
+
spanId: span.span_id,
|
|
257
|
+
parentSpanId: span.parent_span_id,
|
|
258
|
+
name: span.name,
|
|
259
|
+
...startedAt === void 0 ? {} : { startedAt },
|
|
260
|
+
...endedAt === void 0 ? {} : { endedAt },
|
|
261
|
+
...span.status.code === "ERROR" ? { status: "error" } : span.status.code === "OK" ? { status: "ok" } : {},
|
|
262
|
+
// A truncated argument capture is not the call's arguments.
|
|
263
|
+
...args.argsCaptured && !truncated ? { args: args.args } : { argsCaptured: false },
|
|
264
|
+
attributes: span.attributes
|
|
265
|
+
};
|
|
266
|
+
});
|
|
96
267
|
}
|
|
268
|
+
function epochMs(value) {
|
|
269
|
+
try {
|
|
270
|
+
return parseIsoToEpochMs(value);
|
|
271
|
+
} catch {
|
|
272
|
+
return void 0;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function groupByTrace(spans) {
|
|
276
|
+
const groups = /* @__PURE__ */ new Map();
|
|
277
|
+
for (const span of spans) {
|
|
278
|
+
const list = groups.get(span.trace_id) ?? [];
|
|
279
|
+
list.push(span);
|
|
280
|
+
groups.set(span.trace_id, list);
|
|
281
|
+
}
|
|
282
|
+
return [...groups].map(([traceId, list]) => ({ traceId, spans: list }));
|
|
283
|
+
}
|
|
284
|
+
async function runCheck(contract, spans, opts) {
|
|
285
|
+
const traces = [];
|
|
286
|
+
for (const group of groupByTrace(spans)) {
|
|
287
|
+
const verdict = evaluateTraceContract(contract, contractSpansFromOtlp(group.spans));
|
|
288
|
+
const check = { traceId: group.traceId, spanCount: group.spans.length, verdict };
|
|
289
|
+
if (verdict.status !== "pass") {
|
|
290
|
+
check.evidenceDir = await writeEvidence(opts.evidenceRoot, contract, group.traceId, group.spans, verdict);
|
|
291
|
+
}
|
|
292
|
+
traces.push(check);
|
|
293
|
+
}
|
|
294
|
+
const status = traces.some((t) => t.verdict.status === "error") ? "error" : traces.every((t) => t.verdict.status === "pass") ? "pass" : "fail";
|
|
295
|
+
const exitCode = status === "pass" ? CHECK_EXIT.pass : status === "fail" ? CHECK_EXIT.fail : CHECK_EXIT.badContract;
|
|
296
|
+
return { contract: contract.name, status, exitCode, traces };
|
|
297
|
+
}
|
|
298
|
+
async function writeEvidence(root, contract, traceId, spans, verdict) {
|
|
299
|
+
const dir = join(root, `${safeName(contract.name)}-${safeName(traceId).slice(0, 40)}`);
|
|
300
|
+
await mkdir(dir, { recursive: true });
|
|
301
|
+
const cited = new Set(verdict.violations.map((v) => v.spanId).filter((id) => id !== void 0));
|
|
302
|
+
const redacted = redactSpans(spans.filter((s) => cited.has(s.span_id)));
|
|
303
|
+
await writeFile(join(dir, "verdict.json"), `${JSON.stringify({ traceId, verdict }, null, 2)}
|
|
304
|
+
`);
|
|
305
|
+
await writeFile(join(dir, "contract.json"), `${JSON.stringify(contract, null, 2)}
|
|
306
|
+
`);
|
|
307
|
+
await writeFile(join(dir, "explain.txt"), `${explainTraceContract(contract)}
|
|
308
|
+
`);
|
|
309
|
+
await writeFile(join(dir, "cited-spans.jsonl"), redacted.spans.map((s) => JSON.stringify(s)).join("\n") + (redacted.spans.length ? "\n" : ""));
|
|
310
|
+
return dir;
|
|
311
|
+
}
|
|
312
|
+
function safeName(value) {
|
|
313
|
+
return value.replace(/[^A-Za-z0-9._-]+/g, "_");
|
|
314
|
+
}
|
|
315
|
+
function ruleLines(verdict) {
|
|
316
|
+
const branchPassed = /* @__PURE__ */ new Map();
|
|
317
|
+
for (const e of verdict.ruleExecutions) {
|
|
318
|
+
if (e.alternative === void 0) continue;
|
|
319
|
+
branchPassed.set(e.alternative, (branchPassed.get(e.alternative) ?? true) && e.status === "pass");
|
|
320
|
+
}
|
|
321
|
+
const anyBranchPassed = [...branchPassed.values()].some(Boolean);
|
|
322
|
+
return verdict.ruleExecutions.map((e) => {
|
|
323
|
+
const rule = e.alternative ? `${e.alternative}/${e.rule}` : e.rule;
|
|
324
|
+
const status = e.alternative && anyBranchPassed && !branchPassed.get(e.alternative) ? "skipped" : e.status;
|
|
325
|
+
return {
|
|
326
|
+
rule,
|
|
327
|
+
status,
|
|
328
|
+
...e.error === void 0 ? {} : { error: e.error },
|
|
329
|
+
details: verdict.violations.filter((x) => x.rule === rule).map((x) => x.detail)
|
|
330
|
+
};
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
function renderCheckText(report) {
|
|
334
|
+
const lines = [];
|
|
335
|
+
for (const t of report.traces) {
|
|
336
|
+
const v = t.verdict;
|
|
337
|
+
lines.push(`${v.status.toUpperCase()} contract ${report.contract} trace ${t.traceId} (${t.spanCount} spans, ${v.notes})`);
|
|
338
|
+
for (const r of ruleLines(v)) {
|
|
339
|
+
lines.push(` ${r.status.padEnd(7)} ${r.rule}${r.error ? ` ${r.error}` : ""}`);
|
|
340
|
+
if (r.status === "fail") for (const detail of r.details.slice(0, 5)) lines.push(` ${detail}`);
|
|
341
|
+
}
|
|
342
|
+
if (t.evidenceDir) lines.push(` evidence \u2192 ${t.evidenceDir}`);
|
|
343
|
+
}
|
|
344
|
+
return lines.join("\n");
|
|
345
|
+
}
|
|
346
|
+
function renderJUnit(report) {
|
|
347
|
+
const suites = report.traces.map((t) => {
|
|
348
|
+
const rules = ruleLines(t.verdict);
|
|
349
|
+
const count = (status) => rules.filter((r) => r.status === status).length;
|
|
350
|
+
const cases = rules.map((r) => {
|
|
351
|
+
const open = ` <testcase classname="${xml(report.contract)}" name="${xml(r.rule)}">`;
|
|
352
|
+
if (r.status === "pass") return `${open}</testcase>`;
|
|
353
|
+
if (r.status === "skipped") return `${open}
|
|
354
|
+
<skipped message="another alternative passed"/>
|
|
355
|
+
</testcase>`;
|
|
356
|
+
if (r.status === "error") {
|
|
357
|
+
return `${open}
|
|
358
|
+
<error message="${xml(r.error ?? "rule could not be evaluated")}"/>
|
|
359
|
+
</testcase>`;
|
|
360
|
+
}
|
|
361
|
+
return `${open}
|
|
362
|
+
<failure message="${xml(r.details[0] ?? "rule failed")}">${xml(r.details.join("\n"))}</failure>
|
|
363
|
+
</testcase>`;
|
|
364
|
+
});
|
|
365
|
+
return [
|
|
366
|
+
` <testsuite name="${xml(`${report.contract} @ ${t.traceId}`)}" tests="${rules.length}" failures="${count("fail")}" errors="${count("error")}" skipped="${count("skipped")}">`,
|
|
367
|
+
...cases,
|
|
368
|
+
" </testsuite>"
|
|
369
|
+
].join("\n");
|
|
370
|
+
});
|
|
371
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
372
|
+
<testsuites name="traces check">
|
|
373
|
+
${suites.join("\n")}
|
|
374
|
+
</testsuites>
|
|
375
|
+
`;
|
|
376
|
+
}
|
|
377
|
+
function renderGithubAnnotations(report) {
|
|
378
|
+
const lines = [];
|
|
379
|
+
for (const t of report.traces) {
|
|
380
|
+
for (const r of ruleLines(t.verdict)) {
|
|
381
|
+
if (r.status === "pass" || r.status === "skipped") continue;
|
|
382
|
+
const detail = r.status === "error" ? `could not be evaluated: ${r.error ?? "unknown"}` : r.details.join("\n");
|
|
383
|
+
lines.push(`::error title=${ghProperty(`${report.contract}: ${r.rule}`)}::${ghData(`trace ${t.traceId}: ${detail}`)}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return lines.join("\n");
|
|
387
|
+
}
|
|
388
|
+
function xml(value) {
|
|
389
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, "");
|
|
390
|
+
}
|
|
391
|
+
function ghData(value) {
|
|
392
|
+
return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
393
|
+
}
|
|
394
|
+
function ghProperty(value) {
|
|
395
|
+
return ghData(value).replace(/:/g, "%3A").replace(/,/g, "%2C");
|
|
396
|
+
}
|
|
397
|
+
function isObject(value) {
|
|
398
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
399
|
+
}
|
|
400
|
+
function errorText(error) {
|
|
401
|
+
return error instanceof Error ? error.message : String(error);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/cli.ts
|
|
97
405
|
function parseArgs(argv) {
|
|
98
406
|
const a = {
|
|
99
407
|
command: argv[0] === "--help" || argv[0] === "-h" ? "help" : argv[0] ?? "help",
|
|
@@ -123,7 +431,9 @@ function parseArgs(argv) {
|
|
|
123
431
|
concurrency: 4,
|
|
124
432
|
verifyFindings: false,
|
|
125
433
|
replayCorpora: [],
|
|
126
|
-
questions: []
|
|
434
|
+
questions: [],
|
|
435
|
+
annotations: false,
|
|
436
|
+
explain: false
|
|
127
437
|
};
|
|
128
438
|
for (let i = 1; i < argv.length; i++) {
|
|
129
439
|
const arg = argv[i];
|
|
@@ -280,6 +590,21 @@ function parseArgs(argv) {
|
|
|
280
590
|
case "--format":
|
|
281
591
|
a.format = next();
|
|
282
592
|
break;
|
|
593
|
+
case "--contract":
|
|
594
|
+
a.contract = next();
|
|
595
|
+
break;
|
|
596
|
+
case "--junit":
|
|
597
|
+
a.junit = next();
|
|
598
|
+
break;
|
|
599
|
+
case "--evidence":
|
|
600
|
+
a.evidence = next();
|
|
601
|
+
break;
|
|
602
|
+
case "--annotations":
|
|
603
|
+
a.annotations = true;
|
|
604
|
+
break;
|
|
605
|
+
case "--explain":
|
|
606
|
+
a.explain = true;
|
|
607
|
+
break;
|
|
283
608
|
case "--help":
|
|
284
609
|
case "-h":
|
|
285
610
|
a.help = true;
|
|
@@ -296,22 +621,24 @@ function parseArgs(argv) {
|
|
|
296
621
|
}
|
|
297
622
|
return a;
|
|
298
623
|
}
|
|
299
|
-
var OTLP_INPUT_COMMANDS = /* @__PURE__ */ new Set([
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
624
|
+
var OTLP_INPUT_COMMANDS = /* @__PURE__ */ new Set([
|
|
625
|
+
"analyze",
|
|
626
|
+
"check",
|
|
627
|
+
"facts",
|
|
628
|
+
"investigate",
|
|
629
|
+
"improve",
|
|
630
|
+
"ask",
|
|
631
|
+
"stream",
|
|
632
|
+
"validate",
|
|
633
|
+
"mcp"
|
|
634
|
+
]);
|
|
635
|
+
function validateOtlpSelection(args) {
|
|
636
|
+
if (args.otlp === void 0) return args;
|
|
637
|
+
if (!OTLP_INPUT_COMMANDS.has(args.command)) {
|
|
303
638
|
throw new Error(
|
|
304
|
-
`${args.command}
|
|
639
|
+
`${args.command} does not read --otlp. --otlp reads OTLP-JSONL on ${[...OTLP_INPUT_COMMANDS].join(", ")}; use --otlp-out <path> to write the artifact.`
|
|
305
640
|
);
|
|
306
641
|
}
|
|
307
|
-
console.error(
|
|
308
|
-
`warning: --otlp is deprecated on \`${args.command}\` and will be removed in 0.12. --otlp now READS OTLP-JSONL; use --otlp-out <path> to write the artifact. Treating it as --otlp-out for this run.`
|
|
309
|
-
);
|
|
310
|
-
return { ...args, otlpOut: args.otlp, otlp: void 0 };
|
|
311
|
-
}
|
|
312
|
-
function validateOtlpSelection(raw) {
|
|
313
|
-
const args = migrateWritingOtlpFlag(raw);
|
|
314
|
-
if (args.otlp === void 0) return args;
|
|
315
642
|
if (args.input) throw new Error("--otlp cannot be combined with an input file");
|
|
316
643
|
if (args.session) throw new Error("--otlp reads a file and cannot be combined with --session");
|
|
317
644
|
if (args.supervisorRunDir) throw new Error("--otlp cannot be combined with --supervisor-run-dir");
|
|
@@ -448,7 +775,7 @@ async function resolveSelectedSession(args) {
|
|
|
448
775
|
`no ${args.all ? "selected" : adapter.harness} session with ID "${args.session}"; run \`traces list\` to select an ID, or pass an explicit session path`
|
|
449
776
|
);
|
|
450
777
|
}
|
|
451
|
-
throw new
|
|
778
|
+
throw new AmbiguousTraceInputError(
|
|
452
779
|
`session ID "${args.session}" is ambiguous across ${matches.length} files; select one harness or pass its explicit session path`
|
|
453
780
|
);
|
|
454
781
|
}
|
|
@@ -531,7 +858,7 @@ async function collectSpans(args) {
|
|
|
531
858
|
for (const output of [args.out, args.dir, args.otlpOut]) {
|
|
532
859
|
if (output) await assertOutsideSourceBundle(args.sourceBundle, output);
|
|
533
860
|
}
|
|
534
|
-
return collectOtlpSpans(
|
|
861
|
+
return collectOtlpSpans(join2(args.sourceBundle, "derived", "trace.otlp.jsonl"));
|
|
535
862
|
}
|
|
536
863
|
if (args.otlp) return collectOtlpSpans(args.otlp);
|
|
537
864
|
if (args.input) {
|
|
@@ -583,6 +910,62 @@ async function cmdValidate(args) {
|
|
|
583
910
|
}
|
|
584
911
|
process.exitCode = validationExitCode(validation);
|
|
585
912
|
}
|
|
913
|
+
var EVIDENCE_FORMATS = /* @__PURE__ */ new Set(["policy-evidence", "sandbox-events", "openinference", "intelligence-spans", "chat-trajectory"]);
|
|
914
|
+
async function checkInputArgs(args) {
|
|
915
|
+
if (!args.input) return args;
|
|
916
|
+
if (args.otlp || args.session) throw new Error("name the trace once: a positional path, --otlp, or --session");
|
|
917
|
+
const info = await stat(args.input);
|
|
918
|
+
let kind;
|
|
919
|
+
if (args.format && EVIDENCE_FORMATS.has(args.format)) kind = "evidence";
|
|
920
|
+
else if (args.format === "otlp" || args.format === "claude-code" || args.format === "codex") kind = args.format;
|
|
921
|
+
else if (args.format) throw new Error(`unknown --format "${args.format}" (otlp, claude-code, codex, or an evidence format)`);
|
|
922
|
+
else kind = info.isDirectory() ? "otlp" : await sniffCheckInput(args.input);
|
|
923
|
+
if (kind === "otlp") return { ...args, otlp: args.input, input: void 0 };
|
|
924
|
+
if (kind === "evidence") return args;
|
|
925
|
+
return { ...args, harness: kind, harnessExplicit: true, session: args.input, input: void 0 };
|
|
926
|
+
}
|
|
927
|
+
async function cmdCheck(args) {
|
|
928
|
+
if (!args.contract) {
|
|
929
|
+
process.stderr.write("error: check needs --contract <contract.json>\n");
|
|
930
|
+
process.exitCode = CHECK_EXIT.badContract;
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
let loaded;
|
|
934
|
+
try {
|
|
935
|
+
loaded = await loadContract(args.contract);
|
|
936
|
+
} catch (error) {
|
|
937
|
+
process.stderr.write(`error: ${error instanceof Error ? error.message : String(error)}
|
|
938
|
+
`);
|
|
939
|
+
process.exitCode = CHECK_EXIT.badContract;
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
for (const warning of loaded.warnings) process.stderr.write(`warning: ${warning}
|
|
943
|
+
`);
|
|
944
|
+
if (args.explain) {
|
|
945
|
+
console.log(explainTraceContract2(loaded.contract));
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
let spans;
|
|
949
|
+
try {
|
|
950
|
+
spans = (await collectSpans(await checkInputArgs(args))).spans;
|
|
951
|
+
if (spans.length === 0) throw new Error("the trace holds no spans");
|
|
952
|
+
} catch (error) {
|
|
953
|
+
process.stderr.write(`error: ${error instanceof Error ? error.message : String(error)}
|
|
954
|
+
`);
|
|
955
|
+
process.exitCode = error instanceof AmbiguousTraceInputError ? CHECK_EXIT.ambiguous : CHECK_EXIT.unreadable;
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
const report = await runCheck(loaded.contract, spans, { evidenceRoot: args.evidence ?? join2(".traces", "check") });
|
|
959
|
+
console.log(renderCheckText(report));
|
|
960
|
+
if (args.junit) await writeFile2(args.junit, renderJUnit(report), "utf8");
|
|
961
|
+
if (args.out) await writeFile2(args.out, `${JSON.stringify(report, null, 2)}
|
|
962
|
+
`, "utf8");
|
|
963
|
+
if (args.annotations || process.env.GITHUB_ACTIONS === "true") {
|
|
964
|
+
const annotations = renderGithubAnnotations(report);
|
|
965
|
+
if (annotations) console.log(annotations);
|
|
966
|
+
}
|
|
967
|
+
process.exitCode = report.exitCode;
|
|
968
|
+
}
|
|
586
969
|
async function cmdConvert(args) {
|
|
587
970
|
const { spans, workflow } = await collectSpans(args);
|
|
588
971
|
if (spans.length === 0) throw new Error("no spans found for the given selection");
|
|
@@ -686,6 +1069,73 @@ async function cmdBundle(args) {
|
|
|
686
1069
|
`session bundle (${manifest.view} view) \u2192 ${result.directory} (${manifest.files.length} file(s), ${manifest.ledgerSlices.length} ledger slice(s), ${manifest.absent.length} recorded absent)`
|
|
687
1070
|
);
|
|
688
1071
|
}
|
|
1072
|
+
async function cmdMcp(args) {
|
|
1073
|
+
if (args.help) {
|
|
1074
|
+
process.stdout.write(`traces mcp \u2014 read-only MCP server over stdio for the selected traces
|
|
1075
|
+
|
|
1076
|
+
Usage:
|
|
1077
|
+
traces mcp --otlp <file|dir> any OTLP-JSONL
|
|
1078
|
+
traces mcp --harness claude-code --last 5 recent harness sessions (any selection flag)
|
|
1079
|
+
|
|
1080
|
+
Register with a client, for example:
|
|
1081
|
+
claude mcp add traces -- traces mcp --otlp ./spans.otlp.jsonl
|
|
1082
|
+
|
|
1083
|
+
Every tool is read-only and idempotent and says so in its MCP annotations.
|
|
1084
|
+
Spans (including causal links) and results are redacted, the server refuses
|
|
1085
|
+
to start if the redacted spans are still UNSAFE or UNKNOWN, each result
|
|
1086
|
+
carries an untrusted-text notice, and a result above 512 KiB is refused
|
|
1087
|
+
rather than truncated. readSpanSource is not served: windowed reads of
|
|
1088
|
+
original source bytes cannot be redacted.
|
|
1089
|
+
`);
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
const selected = validateOtlpSelection(validateWorkflowSelection(applyCurrentSessionSelection(args)));
|
|
1093
|
+
const collected = await collectSpans(selected);
|
|
1094
|
+
if (collected.spans.length === 0) throw new Error("mcp: no spans found for the given selection");
|
|
1095
|
+
warnIncompleteWorkflow(collected.workflow);
|
|
1096
|
+
process.stderr.write(`traces mcp: serving ${collected.spans.length} span(s) over stdio
|
|
1097
|
+
`);
|
|
1098
|
+
await serveTraceMcp({ spans: collected.spans });
|
|
1099
|
+
}
|
|
1100
|
+
async function cmdDiff(argv) {
|
|
1101
|
+
const inputs = [];
|
|
1102
|
+
const kinds = [];
|
|
1103
|
+
let format = "text";
|
|
1104
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1105
|
+
const arg = argv[i];
|
|
1106
|
+
if (arg === "--help" || arg === "-h") {
|
|
1107
|
+
process.stdout.write(`traces diff \u2014 where two runs of one task first diverge
|
|
1108
|
+
|
|
1109
|
+
Usage:
|
|
1110
|
+
traces diff <a> <b> [--kind TOOL]... [--format text|json]
|
|
1111
|
+
|
|
1112
|
+
Each side is an OTLP file or directory, any file convert reads, file#<trace id>,
|
|
1113
|
+
or file#branch=<id> for one arm (agent.branch.id or agent.branch.arm, and every
|
|
1114
|
+
span below it \u2014 refused if the label names more than one subtree). Steps pair
|
|
1115
|
+
by span id, then position with the same name and kind, then name and kind
|
|
1116
|
+
anywhere. Exit 0 when the runs agree on outcome, 1 when they diverge, 2 when a
|
|
1117
|
+
side cannot be read, 3 when the steps line up with no divergence but neither
|
|
1118
|
+
side carries a definitive OK/ERROR status or a TOOL step \u2014 there is no
|
|
1119
|
+
recorded outcome to call agreement on.
|
|
1120
|
+
`);
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
if (arg === "--format") format = argv[++i] ?? "";
|
|
1124
|
+
else if (arg === "--kind") kinds.push(argv[++i] ?? "");
|
|
1125
|
+
else if (arg.startsWith("--")) throw new Error(`diff: unknown flag ${arg}`);
|
|
1126
|
+
else inputs.push(arg);
|
|
1127
|
+
}
|
|
1128
|
+
if (inputs.length !== 2) {
|
|
1129
|
+
throw new Error("diff needs two runs: traces diff <a> <b> (a file, a directory, file#<trace id> or file#branch=<id>)");
|
|
1130
|
+
}
|
|
1131
|
+
if (format !== "json" && format !== "text") throw new Error(`diff: --format must be json or text, got "${format}"`);
|
|
1132
|
+
if (kinds.some((kind) => !kind)) throw new Error("diff: --kind needs a span kind, e.g. --kind TOOL");
|
|
1133
|
+
const report = await diffRuns(inputs[0], inputs[1], { kinds });
|
|
1134
|
+
process.stdout.write(format === "json" ? `${JSON.stringify(report, null, 2)}
|
|
1135
|
+
` : renderRunDiff(report));
|
|
1136
|
+
if (report.diff.firstDivergence) process.exitCode = 1;
|
|
1137
|
+
else if (report.caveat) process.exitCode = 3;
|
|
1138
|
+
}
|
|
689
1139
|
async function cmdBundleVerify(argv) {
|
|
690
1140
|
let dir;
|
|
691
1141
|
let format = "text";
|
|
@@ -754,7 +1204,7 @@ async function cmdFacts(args) {
|
|
|
754
1204
|
const rendered = format === "json" ? `${JSON.stringify(report, null, 2)}
|
|
755
1205
|
` : renderSessionFacts(report);
|
|
756
1206
|
if (args.out) {
|
|
757
|
-
await
|
|
1207
|
+
await writeFile2(args.out, rendered, "utf8");
|
|
758
1208
|
console.log(`session facts \u2192 ${args.out} (${report.sessions.length} session(s), $0, no model call)`);
|
|
759
1209
|
return;
|
|
760
1210
|
}
|
|
@@ -850,7 +1300,7 @@ function parseAttributeValue(raw) {
|
|
|
850
1300
|
async function loadExportAttributes(args) {
|
|
851
1301
|
const attributes = {};
|
|
852
1302
|
if (args.metadata) {
|
|
853
|
-
const parsed = JSON.parse(await
|
|
1303
|
+
const parsed = JSON.parse(await readFile2(args.metadata, "utf8"));
|
|
854
1304
|
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
855
1305
|
throw new Error("--metadata must point to a JSON object");
|
|
856
1306
|
}
|
|
@@ -1075,8 +1525,8 @@ async function cmdAsk(args) {
|
|
|
1075
1525
|
const collected = await collectSpans(args);
|
|
1076
1526
|
if (collected.spans.length === 0) throw new Error("no spans found for the given selection");
|
|
1077
1527
|
warnIncompleteWorkflow(collected.workflow);
|
|
1078
|
-
const directory = resolve(args.dir ?? await mkdtemp(
|
|
1079
|
-
await
|
|
1528
|
+
const directory = resolve(args.dir ?? await mkdtemp(join2(tmpdir(), "traces-ask-")));
|
|
1529
|
+
await mkdir2(directory, { recursive: true });
|
|
1080
1530
|
const controller = new AbortController();
|
|
1081
1531
|
const interrupt = () => controller.abort(new Error("ask interrupted"));
|
|
1082
1532
|
process.once("SIGINT", interrupt);
|
|
@@ -1090,7 +1540,7 @@ async function cmdAsk(args) {
|
|
|
1090
1540
|
concurrency: args.concurrency,
|
|
1091
1541
|
...args.budget !== void 0 ? { budgetUsd: args.budget } : {},
|
|
1092
1542
|
...args.sourceBundle ? { sourceBundle: { path: args.sourceBundle } } : {},
|
|
1093
|
-
otlpOutPath: args.otlpOut ??
|
|
1543
|
+
otlpOutPath: args.otlpOut ?? join2(directory, "traces.otlp.jsonl"),
|
|
1094
1544
|
signal: controller.signal,
|
|
1095
1545
|
log: analystLog
|
|
1096
1546
|
});
|
|
@@ -1296,11 +1746,11 @@ async function cmdUpload(args) {
|
|
|
1296
1746
|
const redactorParts = args.redactorCmd?.split(/\s+/).filter(Boolean) ?? [];
|
|
1297
1747
|
const redactor = redactorParts[0] ? commandRedactor({ command: redactorParts[0], args: redactorParts.slice(1) }) : void 0;
|
|
1298
1748
|
const candidates = plan.items;
|
|
1299
|
-
const
|
|
1749
|
+
const byDetector = {};
|
|
1300
1750
|
let totalRedactions = 0;
|
|
1301
1751
|
for (const i of candidates) {
|
|
1302
1752
|
totalRedactions += i.redaction.redactionCount;
|
|
1303
|
-
for (const [r, n] of Object.entries(i.redaction.
|
|
1753
|
+
for (const [r, n] of Object.entries(i.redaction.byDetector)) byDetector[r] = (byDetector[r] ?? 0) + n;
|
|
1304
1754
|
}
|
|
1305
1755
|
const w = (s) => process.stderr.write(s);
|
|
1306
1756
|
w(`
|
|
@@ -1308,7 +1758,7 @@ Window: since ${new Date(sinceMs).toISOString()}
|
|
|
1308
1758
|
`);
|
|
1309
1759
|
w(`Sessions found: ${plan.items.length} \xB7 final privacy-mode dedup runs before upload
|
|
1310
1760
|
`);
|
|
1311
|
-
w(`PII/secrets redacted: ${totalRedactions}${Object.keys(
|
|
1761
|
+
w(`PII/secrets redacted: ${totalRedactions}${Object.keys(byDetector).length ? ` (${Object.entries(byDetector).map(([r, n]) => `${r}:${n}`).join(", ")})` : ""}
|
|
1312
1762
|
`);
|
|
1313
1763
|
for (const i of candidates.slice(0, 25)) {
|
|
1314
1764
|
w(` + [${i.ref.harness}] ${i.ref.sessionId.slice(0, 8)} ${i.spans.length} spans ${i.redaction.redactionCount} redacted ${i.ref.cwd ?? ""}
|
|
@@ -1322,7 +1772,8 @@ Window: since ${new Date(sinceMs).toISOString()}
|
|
|
1322
1772
|
}
|
|
1323
1773
|
if (args.dryRun) {
|
|
1324
1774
|
const res2 = await executeUpload(plan, { dryRun: true, otlpOut: args.otlpOut, stripContent: args.noContent, redactor });
|
|
1325
|
-
|
|
1775
|
+
reportRefused(res2.refused);
|
|
1776
|
+
console.log(`dry run: ${candidates.length - res2.skippedSessions - res2.refused.length} session(s), ${totalRedactions} redaction(s). Redacted OTLP -> ${res2.otlpPath}`);
|
|
1326
1777
|
console.log("No upload performed. Set TANGLE_INGEST_URL / TANGLE_INGEST_API_KEY / TANGLE_TENANT_ID and drop --dry-run to send.");
|
|
1327
1778
|
return;
|
|
1328
1779
|
}
|
|
@@ -1335,10 +1786,79 @@ Window: since ${new Date(sinceMs).toISOString()}
|
|
|
1335
1786
|
}
|
|
1336
1787
|
const res = await executeUpload(plan, { log: (m) => w(`${m}
|
|
1337
1788
|
`), stripContent: args.noContent, redactor });
|
|
1789
|
+
reportRefused(res.refused);
|
|
1338
1790
|
console.log(
|
|
1339
|
-
`Uploaded ${res.uploadedSessions} session(s), ${res.acceptedSpans} spans accepted, ${res.redactionCount} redaction(s); ${res.skippedSessions} already-uploaded skipped.`
|
|
1791
|
+
`Uploaded ${res.uploadedSessions} session(s), ${res.acceptedSpans} spans accepted, ${res.redactionCount} redaction(s); ${res.skippedSessions} already-uploaded skipped; ${res.refused.length} refused.`
|
|
1340
1792
|
);
|
|
1341
1793
|
}
|
|
1794
|
+
function reportRefused(refused) {
|
|
1795
|
+
for (const { harness, sessionId, verdict } of refused) {
|
|
1796
|
+
process.stderr.write(` REFUSED [${harness}] ${sessionId.slice(0, 8)}: ${verdict.status}
|
|
1797
|
+
`);
|
|
1798
|
+
for (const finding of verdict.findings.filter((f) => f.severity === "error")) {
|
|
1799
|
+
process.stderr.write(` ${finding.category}/${finding.detector} x${finding.count} at ${finding.paths.join(", ")}
|
|
1800
|
+
`);
|
|
1801
|
+
}
|
|
1802
|
+
for (const entry of verdict.unreadable) process.stderr.write(` unread ${entry}
|
|
1803
|
+
`);
|
|
1804
|
+
}
|
|
1805
|
+
if (refused.length > 0) {
|
|
1806
|
+
process.stderr.write(`${refused.length} session(s) refused by the share-safety check and kept on this machine.
|
|
1807
|
+
`);
|
|
1808
|
+
process.exitCode = 1;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
async function cmdVerifySafe(argv) {
|
|
1812
|
+
let target;
|
|
1813
|
+
let format = "text";
|
|
1814
|
+
let profile = "share";
|
|
1815
|
+
const knownSecrets = [];
|
|
1816
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1817
|
+
const arg = argv[i];
|
|
1818
|
+
if (arg === "--format") format = argv[++i] ?? "";
|
|
1819
|
+
else if (arg === "--profile") profile = argv[++i] ?? "";
|
|
1820
|
+
else if (arg === "--known-secret-env") {
|
|
1821
|
+
const name = argv[++i] ?? "";
|
|
1822
|
+
const value = process.env[name];
|
|
1823
|
+
if (!value) throw new Error(`verify-safe: environment variable ${name || "(missing name)"} is not set`);
|
|
1824
|
+
knownSecrets.push(value);
|
|
1825
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
1826
|
+
usageVerifySafe();
|
|
1827
|
+
return;
|
|
1828
|
+
} else if (arg.startsWith("--")) throw new Error(`verify-safe: unknown flag ${arg}`);
|
|
1829
|
+
else if (target === void 0) target = arg;
|
|
1830
|
+
else throw new Error(`verify-safe: unexpected argument ${arg}`);
|
|
1831
|
+
}
|
|
1832
|
+
if (!target) throw new Error("verify-safe needs a file or directory: traces verify-safe <file|dir>");
|
|
1833
|
+
if (format !== "json" && format !== "text") throw new Error(`verify-safe: --format must be json or text, got "${format}"`);
|
|
1834
|
+
if (profile !== "default" && profile !== "share" && profile !== "strict") {
|
|
1835
|
+
throw new Error(`verify-safe: --profile must be default, share or strict, got "${profile}"`);
|
|
1836
|
+
}
|
|
1837
|
+
const result = await verifySafe(target, { profile, knownSecrets });
|
|
1838
|
+
process.stdout.write(format === "json" ? `${JSON.stringify(result, null, 2)}
|
|
1839
|
+
` : `${formatVerdict(result)}
|
|
1840
|
+
`);
|
|
1841
|
+
process.exitCode = verdictExitCode(result);
|
|
1842
|
+
}
|
|
1843
|
+
function usageVerifySafe() {
|
|
1844
|
+
console.log(`traces verify-safe <file|dir> [--profile default|share|strict] [--known-secret-env NAME]... [--format text|json]
|
|
1845
|
+
|
|
1846
|
+
Check whether files are safe to share, without changing them. A directory is read
|
|
1847
|
+
recursively. .json files are parsed whole, .jsonl and .ndjson line by line, and
|
|
1848
|
+
any other file is scanned as text.
|
|
1849
|
+
|
|
1850
|
+
SAFE nothing found exit 0
|
|
1851
|
+
SAFE_WITH_WARNINGS only warnings (raw content, identifiers, media) exit 0
|
|
1852
|
+
UNSAFE a credential, or personal data under share/strict exit 1
|
|
1853
|
+
UNKNOWN a file, line or byte range could not be read exit 2
|
|
1854
|
+
|
|
1855
|
+
--profile default | share (default) | strict; see agent-eval docs/redaction.md
|
|
1856
|
+
--known-secret-env name of an environment variable holding a secret to look for in
|
|
1857
|
+
any encoding (repeatable); the value is never printed
|
|
1858
|
+
--format text (default) | json
|
|
1859
|
+
|
|
1860
|
+
Findings name the file, line and JSON Pointer, never the matched value.`);
|
|
1861
|
+
}
|
|
1342
1862
|
function usageExport() {
|
|
1343
1863
|
console.log(`traces export: convert trace evidence files to OpenInference JSONL
|
|
1344
1864
|
|
|
@@ -1365,7 +1885,7 @@ Examples:
|
|
|
1365
1885
|
halo spans.openinference.jsonl --prompt "Analyze this trace slice" --max-turns 1
|
|
1366
1886
|
|
|
1367
1887
|
Safety:
|
|
1368
|
-
export runs the same local
|
|
1888
|
+
export runs the same local redaction (agent-eval's redaction core) used by upload before writing spans.
|
|
1369
1889
|
--metadata must be a JSON object; --attr key=value is repeatable and overrides matching metadata keys.`);
|
|
1370
1890
|
}
|
|
1371
1891
|
function usageImportCodeTraceBench() {
|
|
@@ -1434,6 +1954,14 @@ Two ways in:
|
|
|
1434
1954
|
|
|
1435
1955
|
Commands:
|
|
1436
1956
|
validate Report what a trace can and cannot answer (exit 1 on error findings)
|
|
1957
|
+
check Gate a run on a trace contract: check <trace> --contract <file.json>
|
|
1958
|
+
[--junit out.xml] [--out report.json] [--evidence dir] [--annotations].
|
|
1959
|
+
check --contract <file.json> --explain prints what the contract checks.
|
|
1960
|
+
<trace> is claude -p stream-json output, OTLP spans (file or dir),
|
|
1961
|
+
a Claude Code or Codex session file, or trace evidence; --format
|
|
1962
|
+
names it when the sniff cannot.
|
|
1963
|
+
Exit 0 pass, 1 fail, 2 bad contract or a rule could not run,
|
|
1964
|
+
3 unreadable trace, 4 ambiguous trace reference
|
|
1437
1965
|
list List discovered sessions
|
|
1438
1966
|
analyze Run analyst suite + loop/waste pipelines over OTLP (--otlp), sessions,
|
|
1439
1967
|
or an input file; names every analysis the spans could not support
|
|
@@ -1447,7 +1975,8 @@ Commands:
|
|
|
1447
1975
|
facts Print the deterministic session-facts sheet for the selected
|
|
1448
1976
|
sessions: tool calls excluding synthesized spans, subagent spawns
|
|
1449
1977
|
with task names, human turns in order, the final message per task,
|
|
1450
|
-
changed paths, first/last record times,
|
|
1978
|
+
changed paths, first/last record times, the harness token total, and
|
|
1979
|
+
the first failure by agent-eval's fixed precedence (or none).
|
|
1451
1980
|
No model call, no budget, $0. Every fact names the span ids it came
|
|
1452
1981
|
from; a fact the spans cannot support is null with its reason.
|
|
1453
1982
|
--format json (default) or text (exit 1 when a session cannot be read)
|
|
@@ -1458,6 +1987,17 @@ Commands:
|
|
|
1458
1987
|
ledger sliced to the session window, git log, and a sha256 manifest
|
|
1459
1988
|
(needs --session <id|path> and --out <new-dir>). This is the FULL
|
|
1460
1989
|
view: it holds the session's own words, for a reader who cites them
|
|
1990
|
+
mcp Serve the selected traces to an MCP client over stdio: 7 read-only,
|
|
1991
|
+
idempotent trace tools; spans and results redacted, results capped
|
|
1992
|
+
and marked untrusted (traces mcp --help)
|
|
1993
|
+
diff <a> <b>
|
|
1994
|
+
Pair two runs' steps (by span id, then position with the same name
|
|
1995
|
+
and kind, then name and kind anywhere) and mark the first divergence:
|
|
1996
|
+
changed, replaced, only-in-a, only-in-b or reordered. Each side is an
|
|
1997
|
+
OTLP file or directory, any file convert reads, file#<trace id>, or
|
|
1998
|
+
file#branch=<id> for one arm (agent.branch.id or agent.branch.arm).
|
|
1999
|
+
--kind TOOL (repeatable) keeps only steps of that span kind.
|
|
2000
|
+
Exit 0 agree, 1 diverge, 2 unreadable (--format text|json)
|
|
1461
2001
|
bundle verify <bundle-dir>
|
|
1462
2002
|
Check a bundle against its manifest: every listed file present with
|
|
1463
2003
|
its recorded size and SHA-256, no unlisted or non-regular file beside
|
|
@@ -1487,7 +2027,11 @@ Commands:
|
|
|
1487
2027
|
stream Emit JSONL trace stream events for live visualizers or replay
|
|
1488
2028
|
watch Online observer: tail active sessions, notify on loops + semantic findings
|
|
1489
2029
|
(watch <target> tails ONE run tree instead: a run directory or a span file)
|
|
1490
|
-
upload Redact + upload sessions in a time window to the Tangle Intelligence Platform
|
|
2030
|
+
upload Redact + upload sessions in a time window to the Tangle Intelligence Platform;
|
|
2031
|
+
a session whose final events are UNSAFE or UNKNOWN is refused (exit 1)
|
|
2032
|
+
verify-safe <file|dir>
|
|
2033
|
+
Say whether files are safe to share: SAFE, SAFE_WITH_WARNINGS, UNSAFE
|
|
2034
|
+
(exit 1) or UNKNOWN (exit 2). Read-only (--help for flags)
|
|
1491
2035
|
|
|
1492
2036
|
Options:
|
|
1493
2037
|
--harness <id> Harness or alias (default: claude-code). Known: ${knownHarnesses().join(", ")}
|
|
@@ -1513,9 +2057,8 @@ Options:
|
|
|
1513
2057
|
A directory reads the OTLP files under it \u2014 only the otlp/
|
|
1514
2058
|
subdirectory when the producer made one \u2014 and names the JSONL
|
|
1515
2059
|
that is not OTLP instead of reading it as broken spans.
|
|
1516
|
-
Supported by: validate, analyze, investigate, improve, ask,
|
|
1517
|
-
|
|
1518
|
-
--otlp-out; it still works, with a warning, until 0.12.
|
|
2060
|
+
Supported by: validate, analyze, facts, investigate, improve, ask,
|
|
2061
|
+
stream, mcp. Writing commands take --otlp-out.
|
|
1519
2062
|
--source-bundle <dir> Analyze a retained full bundle; explicitly grant source-field reads.
|
|
1520
2063
|
Available for analyze, investigate, improve, and ask.
|
|
1521
2064
|
--otlp-out <path> WRITE the OTLP-JSONL artifact here (also evidence
|
|
@@ -1567,7 +2110,7 @@ Options:
|
|
|
1567
2110
|
--min-loop <n> Min identical repeated calls to flag a loop (default 3)
|
|
1568
2111
|
--dry-run upload: redact + dedup + preview, write OTLP, but do NOT send
|
|
1569
2112
|
--no-content upload: strip prompt/response text; send metadata only
|
|
1570
|
-
--redactor <cmd> upload: external PII scrubber (JSON array stdin\u2192stdout) after the
|
|
2113
|
+
--redactor <cmd> upload: external PII scrubber (JSON array stdin\u2192stdout) after the redaction core
|
|
1571
2114
|
--yes, -y upload: skip the confirmation prompt
|
|
1572
2115
|
--version, -v Print the installed traces version
|
|
1573
2116
|
--help, -h Show help (use \`traces validate --help\`, \`traces export --help\`,
|
|
@@ -1585,7 +2128,7 @@ Upload env: TANGLE_INGEST_URL (or TANGLE_ORCHESTRATOR_URL), TANGLE_INGEST_API_KE
|
|
|
1585
2128
|
async function main() {
|
|
1586
2129
|
const rawArgs = process.argv.slice(2);
|
|
1587
2130
|
if (rawArgs[0] === "--version" || rawArgs[0] === "-v" || rawArgs[0] === "version") {
|
|
1588
|
-
console.log(`traces ${
|
|
2131
|
+
console.log(`traces ${tracesVersion()}`);
|
|
1589
2132
|
return;
|
|
1590
2133
|
}
|
|
1591
2134
|
if (rawArgs[0] === "replay-verify") {
|
|
@@ -1600,10 +2143,28 @@ async function main() {
|
|
|
1600
2143
|
await cmdVerifyFindings(rawArgs.slice(1));
|
|
1601
2144
|
return;
|
|
1602
2145
|
}
|
|
2146
|
+
if (rawArgs[0] === "mcp") {
|
|
2147
|
+
await cmdMcp(parseArgs(rawArgs));
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
2150
|
+
if (rawArgs[0] === "diff") {
|
|
2151
|
+
try {
|
|
2152
|
+
await cmdDiff(rawArgs.slice(1));
|
|
2153
|
+
} catch (err) {
|
|
2154
|
+
process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
|
|
2155
|
+
`);
|
|
2156
|
+
process.exitCode = 2;
|
|
2157
|
+
}
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
1603
2160
|
if (rawArgs[0] === "bundle" && rawArgs[1] === "verify") {
|
|
1604
2161
|
await cmdBundleVerify(rawArgs.slice(2));
|
|
1605
2162
|
return;
|
|
1606
2163
|
}
|
|
2164
|
+
if (rawArgs[0] === "verify-safe") {
|
|
2165
|
+
await cmdVerifySafe(rawArgs.slice(1));
|
|
2166
|
+
return;
|
|
2167
|
+
}
|
|
1607
2168
|
const parsedArgs = parseArgs(rawArgs);
|
|
1608
2169
|
if (parsedArgs.help) {
|
|
1609
2170
|
if (parsedArgs.command === "import-codetracebench" || parsedArgs.command === "help" && parsedArgs.input === "import-codetracebench") usageImportCodeTraceBench();
|
|
@@ -1631,6 +2192,9 @@ async function main() {
|
|
|
1631
2192
|
case "validate":
|
|
1632
2193
|
await cmdValidate(args);
|
|
1633
2194
|
break;
|
|
2195
|
+
case "check":
|
|
2196
|
+
await cmdCheck(args);
|
|
2197
|
+
break;
|
|
1634
2198
|
case "investigate":
|
|
1635
2199
|
await cmdInvestigate(args);
|
|
1636
2200
|
break;
|
|
@@ -1683,5 +2247,5 @@ async function main() {
|
|
|
1683
2247
|
main().catch((err) => {
|
|
1684
2248
|
process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
|
|
1685
2249
|
`);
|
|
1686
|
-
process.exitCode = 1;
|
|
2250
|
+
process.exitCode = process.argv[2] === "check" ? CHECK_EXIT.badContract : 1;
|
|
1687
2251
|
});
|