@tangle-network/traces 0.23.0 → 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.
- package/README.md +61 -1078
- package/dist/analyst-model-call-JVUOYZ5V.js +20 -0
- package/dist/ask-XLS3VANY.js +39 -0
- package/dist/check-YF6ZHPGG.js +322 -0
- package/dist/chunk-3I4PJ5Q7.js +25 -0
- package/dist/chunk-3T62R7ON.js +5695 -0
- package/dist/chunk-4VKPN432.js +138 -0
- package/dist/chunk-6CLC7DKP.js +492 -0
- package/dist/chunk-7CBV5RJ2.js +535 -0
- package/dist/chunk-B62DOFGI.js +273 -0
- package/dist/chunk-CL3OQOWX.js +407 -0
- package/dist/chunk-DLGAKCQJ.js +782 -0
- package/dist/chunk-DU534HAX.js +263 -0
- package/dist/chunk-GLUDGUGJ.js +232 -0
- package/dist/chunk-J2PM5SVN.js +503 -0
- package/dist/chunk-M3W6TZHQ.js +381 -0
- package/dist/chunk-NH7HCDLB.js +759 -0
- package/dist/chunk-NZ5Y7UEL.js +1217 -0
- package/dist/chunk-O52XK7OB.js +19 -0
- package/dist/chunk-PYH3PL3N.js +62 -0
- package/dist/chunk-RZL5GAWH.js +60 -0
- package/dist/chunk-TR6O4S4T.js +208 -0
- package/dist/chunk-TW43MR5D.js +259 -0
- package/dist/chunk-TYLFUX42.js +228 -0
- package/dist/chunk-UOLRCQHC.js +120 -0
- package/dist/chunk-UVE5ANVV.js +3921 -0
- package/dist/chunk-V2OCGX57.js +53 -0
- package/dist/cli.js +2086 -2050
- package/dist/diff-ZZ6KYBPP.js +16 -0
- package/dist/improvement-OJQ6365E.js +25 -0
- package/dist/index.d.ts +1235 -4357
- package/dist/index.js +53 -1204
- package/dist/live-Y64AJ6MA.js +14 -0
- package/dist/mcp-ZPGTJQYQ.js +85 -0
- package/dist/report-JAMXRTCW.js +338 -0
- package/dist/run-watch-PLWTIRSV.js +649 -0
- package/dist/supervisor-report-RMPLLBGZ.js +29 -0
- package/dist/supervisor-run-context-WO6VOTGV.js +30 -0
- package/dist/upload-R6NMKSGQ.js +20 -0
- package/dist/verify-safe-C5ZXZOZ2.js +8 -0
- package/docs/analyze.md +76 -0
- package/docs/ask.md +88 -0
- package/docs/bundle.md +40 -0
- package/docs/check.md +77 -0
- package/docs/convert.md +44 -0
- package/docs/develop.md +32 -0
- package/docs/diff.md +38 -0
- package/docs/facts.md +114 -0
- package/docs/mcp.md +36 -0
- package/docs/migrate-1.0.md +33 -0
- package/docs/parsers.md +66 -0
- package/docs/release-1.0.0.md +147 -0
- package/docs/sdk.md +77 -0
- package/docs/select.md +70 -0
- package/docs/skills.md +46 -0
- package/docs/upload.md +47 -0
- package/docs/watch-jsonl.v1.example.jsonl +5 -0
- package/docs/watch.md +66 -0
- package/examples/README.md +9 -0
- package/examples/custom-analyst.ts +65 -74
- package/install.sh +22 -2
- package/package.json +8 -7
- package/skills/inspect-agent-traces/SKILL.md +47 -110
- package/dist/chunk-COSWFESO.js +0 -20979
- package/docs/replay-verify.md +0 -123
- package/docs/stream-findings.svg +0 -39
- package/docs/trace-analysts.md +0 -643
- package/examples/custom-backend.ts +0 -21
- package/examples/external-engines.ts +0 -62
- package/examples/improvement-config.mjs +0 -63
- package/examples/observe-and-alert.ts +0 -19
- package/examples/register-harness.ts +0 -26
- package/skills/build-trace-analyst/SKILL.md +0 -76
- 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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
56
|
-
readOtlpInput,
|
|
57
|
-
readSessionIndexFile,
|
|
58
|
-
redactSpans,
|
|
59
|
-
renderInspectionReport,
|
|
60
|
-
renderRunDiff,
|
|
42
|
+
readSessionTurnGroups,
|
|
61
43
|
renderSessionFacts,
|
|
62
44
|
renderSkillUsage,
|
|
63
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
writeInspectionReportFile,
|
|
90
|
-
writeOtlpFile,
|
|
91
|
-
writePolicyEvidenceFile,
|
|
92
|
-
writeSessionIndexFile,
|
|
93
|
-
writeTraceEvidenceExportFile,
|
|
94
|
-
writeTraceQuestionsArtifacts
|
|
95
|
-
} from "./chunk-COSWFESO.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 {
|
|
99
|
-
import {
|
|
100
|
-
import {
|
|
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/
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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/
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
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
|
-
|
|
249
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
|
285
|
-
const
|
|
531
|
+
function callIdsOf(spans) {
|
|
532
|
+
const ids = /* @__PURE__ */ new Map();
|
|
286
533
|
for (const span of spans) {
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
375
|
-
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
|
396
|
-
}
|
|
397
|
-
function xml(value) {
|
|
398
|
-
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).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/
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
445
|
-
|
|
674
|
+
contracts: [],
|
|
675
|
+
shareSafe: false,
|
|
676
|
+
once: false,
|
|
677
|
+
dryRun: false,
|
|
678
|
+
yes: false,
|
|
679
|
+
unused: false,
|
|
680
|
+
knownSecretEnvs: []
|
|
446
681
|
};
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
a.
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
|
|
638
|
-
}
|
|
639
|
-
var
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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 (
|
|
658
|
-
|
|
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
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
"
|
|
673
|
-
"
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
|
712
|
-
|
|
713
|
-
|
|
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
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
throw new
|
|
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
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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
|
|
758
|
-
return
|
|
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
|
|
761
|
-
|
|
762
|
-
const
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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
|
-
|
|
792
|
-
if (
|
|
793
|
-
|
|
794
|
-
|
|
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
|
-
|
|
798
|
-
|
|
799
|
-
|
|
1387
|
+
return groups;
|
|
1388
|
+
}
|
|
1389
|
+
function errorText(error) {
|
|
1390
|
+
return error instanceof Error ? error.message : String(error);
|
|
800
1391
|
}
|
|
801
|
-
function
|
|
802
|
-
|
|
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
|
-
|
|
808
|
-
|
|
809
|
-
const
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
return
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
const
|
|
817
|
-
if (
|
|
818
|
-
|
|
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
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
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
|
|
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
|
|
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
|
|
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: [...
|
|
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.
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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
|
-
|
|
883
|
-
if (
|
|
884
|
-
|
|
885
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
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
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
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
|
-
|
|
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 (
|
|
964
|
-
|
|
965
|
-
|
|
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
|
-
|
|
1682
|
+
process.exitCode = verdict.exitCode;
|
|
1683
|
+
}
|
|
1684
|
+
async function otlpConformanceGate(check, subject, path) {
|
|
1685
|
+
let input;
|
|
968
1686
|
try {
|
|
969
|
-
|
|
970
|
-
if (spans.length === 0) throw new Error("the trace holds no spans");
|
|
1687
|
+
input = await readOtlpInput(path);
|
|
971
1688
|
} catch (error) {
|
|
972
|
-
|
|
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
|
-
|
|
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
|
|
990
|
-
if (spans.length === 0) throw new Error("no spans found for the given selection");
|
|
991
|
-
warnIncompleteWorkflow(workflow);
|
|
992
|
-
const
|
|
993
|
-
|
|
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 (${
|
|
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,
|
|
1016
|
-
const
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
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
|
-
|
|
1022
|
-
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
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
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
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.
|
|
1778
|
+
process.stderr.write(`traces mcp: serving ${collected.spanCount} span(s) over stdio
|
|
1116
1779
|
`);
|
|
1117
|
-
await serveTraceMcp({
|
|
1780
|
+
await serveTraceMcp({ served: served.finish() });
|
|
1118
1781
|
}
|
|
1119
|
-
async function cmdDiff(
|
|
1120
|
-
const
|
|
1121
|
-
const
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
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(
|
|
1159
|
-
|
|
1160
|
-
|
|
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"}
|
|
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
|
-
|
|
1187
|
-
|
|
1188
|
-
if (
|
|
1189
|
-
|
|
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 ?? "
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
const
|
|
1218
|
-
|
|
1219
|
-
const
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
const
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
);
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
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
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
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
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
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
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
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
|
-
|
|
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
|
|
1351
|
-
if (
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
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
|
-
|
|
1948
|
+
process.stdout.write(rendered);
|
|
1358
1949
|
}
|
|
1359
|
-
async function
|
|
1360
|
-
const
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
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
|
-
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
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
|
|
2017
|
+
return `${lines.join("\n")}
|
|
2018
|
+
`;
|
|
1374
2019
|
}
|
|
1375
2020
|
async function cmdAnalyze(args) {
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
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
|
|
2040
|
+
const artifacts = await writeAnalysisArtifacts(renderAnalysisReport(result), result.packet, args.out);
|
|
1388
2041
|
console.log(
|
|
1389
|
-
`
|
|
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
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
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(
|
|
1435
|
-
|
|
1436
|
-
const
|
|
1437
|
-
const
|
|
1438
|
-
|
|
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
|
|
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
|
|
2068
|
+
spans,
|
|
1452
2069
|
harness: result.format,
|
|
1453
|
-
|
|
1454
|
-
sources: [...sessions].map(([sessionId, spans]) => sessionReportSource({
|
|
2070
|
+
sources: [...sessions].map(([sessionId, grouped]) => sessionReportSource({
|
|
1455
2071
|
harness: result.format,
|
|
1456
2072
|
sessionId,
|
|
1457
|
-
path
|
|
2073
|
+
path,
|
|
1458
2074
|
cwd: null,
|
|
1459
2075
|
mtimeMs: 0
|
|
1460
|
-
},
|
|
2076
|
+
}, grouped, sessionId))
|
|
1461
2077
|
};
|
|
1462
2078
|
}
|
|
1463
|
-
function
|
|
1464
|
-
const
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
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
|
-
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
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
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
if (
|
|
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
|
-
|
|
2125
|
+
corpus: corpus.finish(),
|
|
1602
2126
|
engine,
|
|
1603
2127
|
harness: collected.harness,
|
|
1604
|
-
concurrency:
|
|
1605
|
-
|
|
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,
|
|
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
|
-
|
|
1619
|
-
|
|
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
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
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
|
|
1668
|
-
|
|
1669
|
-
const
|
|
1670
|
-
|
|
1671
|
-
const
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
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
|
-
|
|
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
|
|
1752
|
-
|
|
1753
|
-
|
|
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
|
-
|
|
1758
|
-
|
|
1759
|
-
);
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
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
|
|
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
|
|
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
|
|
1803
|
-
const
|
|
1804
|
-
const
|
|
1805
|
-
|
|
1806
|
-
|
|
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
|
-
|
|
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.
|
|
2342
|
+
const res2 = await executeUpload(plan, { dryRun: true, otlpOut: args.out, stripContent: args.noContent });
|
|
1839
2343
|
reportRefused(res2.refused);
|
|
1840
|
-
console.log(`
|
|
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
|
|
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
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
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 =
|
|
1932
|
-
|
|
1933
|
-
const report = buildSkillUsageReport(store.list(), await readInstalledSkills(), storePath, { since,
|
|
1934
|
-
|
|
1935
|
-
|
|
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(
|
|
2398
|
+
process.stdout.write(json ? `${JSON.stringify(report, null, 2)}
|
|
1942
2399
|
` : `${renderSkillUsage(report)}
|
|
1943
2400
|
`);
|
|
1944
2401
|
}
|
|
1945
|
-
function
|
|
1946
|
-
|
|
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
|
|
2068
|
-
|
|
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
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
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
|
});
|