@tangle-network/traces 0.8.0 → 0.8.11
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 +81 -1
- package/dist/{chunk-BUD6I3ZJ.js → chunk-PRRWLKSI.js} +1943 -323
- package/dist/cli.js +309 -12
- package/dist/index.d.ts +471 -27
- package/dist/index.js +71 -5
- package/package.json +12 -13
|
@@ -1,3 +1,110 @@
|
|
|
1
|
+
// src/adoption.ts
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
function toolName(s) {
|
|
5
|
+
const n = s.attributes["tool.name"];
|
|
6
|
+
if (typeof n === "string") return n;
|
|
7
|
+
if (s.name.startsWith("tool.")) return s.name.slice("tool.".length);
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
function parseInput(s) {
|
|
11
|
+
const c = s.attributes.content;
|
|
12
|
+
if (typeof c !== "string") return {};
|
|
13
|
+
try {
|
|
14
|
+
const v = JSON.parse(c);
|
|
15
|
+
return v && typeof v === "object" ? v : {};
|
|
16
|
+
} catch {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function skillNameOf(input) {
|
|
21
|
+
const v = input.skill ?? input.command;
|
|
22
|
+
return typeof v === "string" && v.length > 0 ? v : "?";
|
|
23
|
+
}
|
|
24
|
+
function subagentTypeOf(input) {
|
|
25
|
+
const v = input.subagent_type;
|
|
26
|
+
return typeof v === "string" && v.length > 0 ? v : "?";
|
|
27
|
+
}
|
|
28
|
+
function countSkillRunsJsonl(raw) {
|
|
29
|
+
const out = {};
|
|
30
|
+
for (const line of raw.split("\n")) {
|
|
31
|
+
const t = line.trim();
|
|
32
|
+
if (!t) continue;
|
|
33
|
+
let o;
|
|
34
|
+
try {
|
|
35
|
+
o = JSON.parse(t);
|
|
36
|
+
} catch {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (!o || typeof o !== "object") continue;
|
|
40
|
+
const rec = o;
|
|
41
|
+
const names = [];
|
|
42
|
+
if (Array.isArray(rec.skills)) {
|
|
43
|
+
for (const s of rec.skills) if (typeof s === "string" && s) names.push(s);
|
|
44
|
+
}
|
|
45
|
+
if (typeof rec.skill === "string" && rec.skill) names.push(rec.skill);
|
|
46
|
+
if (names.length === 0) names.push("?");
|
|
47
|
+
for (const n of names) out[n] = (out[n] ?? 0) + 1;
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
async function readLoopRuns(cwds) {
|
|
52
|
+
const counts = {};
|
|
53
|
+
let filesRead = 0;
|
|
54
|
+
const seen = /* @__PURE__ */ new Set();
|
|
55
|
+
for (const cwd of cwds) {
|
|
56
|
+
if (!cwd || seen.has(cwd)) continue;
|
|
57
|
+
seen.add(cwd);
|
|
58
|
+
const path = join(cwd, ".evolve", "skill-runs.jsonl");
|
|
59
|
+
let raw;
|
|
60
|
+
try {
|
|
61
|
+
raw = await readFile(path, "utf8");
|
|
62
|
+
} catch {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
filesRead += 1;
|
|
66
|
+
for (const [k, v] of Object.entries(countSkillRunsJsonl(raw))) counts[k] = (counts[k] ?? 0) + v;
|
|
67
|
+
}
|
|
68
|
+
return { counts, filesRead };
|
|
69
|
+
}
|
|
70
|
+
async function analyzeAdoption(spans, opts = {}) {
|
|
71
|
+
const sessions = /* @__PURE__ */ new Set();
|
|
72
|
+
const sessionsWithSkill = /* @__PURE__ */ new Set();
|
|
73
|
+
const sessionsWithSubagent = /* @__PURE__ */ new Set();
|
|
74
|
+
const skillInvocations = {};
|
|
75
|
+
const subagentSpawns = {};
|
|
76
|
+
for (const s of spans) {
|
|
77
|
+
sessions.add(s.trace_id);
|
|
78
|
+
const tn = toolName(s);
|
|
79
|
+
if (tn === "Skill") {
|
|
80
|
+
const name = skillNameOf(parseInput(s));
|
|
81
|
+
skillInvocations[name] = (skillInvocations[name] ?? 0) + 1;
|
|
82
|
+
sessionsWithSkill.add(s.trace_id);
|
|
83
|
+
} else if (tn === "Task" || tn === "Agent") {
|
|
84
|
+
const type = subagentTypeOf(parseInput(s));
|
|
85
|
+
subagentSpawns[type] = (subagentSpawns[type] ?? 0) + 1;
|
|
86
|
+
sessionsWithSubagent.add(s.trace_id);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const { counts: loopDispatchedRuns, filesRead } = await readLoopRuns(opts.cwds ?? []);
|
|
90
|
+
const totalSkillInvocations = Object.values(skillInvocations).reduce((a, b) => a + b, 0);
|
|
91
|
+
const totalSubagentSpawns = Object.values(subagentSpawns).reduce((a, b) => a + b, 0);
|
|
92
|
+
const totalLoopDispatchedRuns = Object.values(loopDispatchedRuns).reduce((a, b) => a + b, 0);
|
|
93
|
+
return {
|
|
94
|
+
sessionCount: sessions.size,
|
|
95
|
+
sessionsWithSkill: sessionsWithSkill.size,
|
|
96
|
+
skillPenetration: sessions.size === 0 ? 0 : sessionsWithSkill.size / sessions.size,
|
|
97
|
+
skillInvocations,
|
|
98
|
+
totalSkillInvocations,
|
|
99
|
+
subagentSpawns,
|
|
100
|
+
totalSubagentSpawns,
|
|
101
|
+
sessionsWithSubagent: sessionsWithSubagent.size,
|
|
102
|
+
loopDispatchedRuns,
|
|
103
|
+
totalLoopDispatchedRuns,
|
|
104
|
+
skillRunFilesRead: filesRead
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
1
108
|
// src/otlp.ts
|
|
2
109
|
function span(input) {
|
|
3
110
|
const attributes = {
|
|
@@ -30,6 +137,9 @@ function toOpenInferenceSpan(s) {
|
|
|
30
137
|
const resourceAttrs = {};
|
|
31
138
|
if (a["service.name"] != null) resourceAttrs["service.name"] = a["service.name"];
|
|
32
139
|
if (a["agent.name"] != null) resourceAttrs["agent.name"] = a["agent.name"];
|
|
140
|
+
for (const k of ["tangle.subject.key", "git.repository", "git.branch", "git.commit", "tangle.cwd", "traces.repo_resolution_source"]) {
|
|
141
|
+
if (a[k] != null) resourceAttrs[k] = a[k];
|
|
142
|
+
}
|
|
33
143
|
return {
|
|
34
144
|
trace_id: s.trace_id,
|
|
35
145
|
span_id: s.span_id,
|
|
@@ -50,11 +160,11 @@ function serializeSpans(spans) {
|
|
|
50
160
|
`;
|
|
51
161
|
}
|
|
52
162
|
async function writeOtlpFile(spans, outPath) {
|
|
53
|
-
const { mkdtemp, writeFile:
|
|
54
|
-
const { tmpdir } = await import("os");
|
|
55
|
-
const { join:
|
|
56
|
-
const path = outPath ??
|
|
57
|
-
await
|
|
163
|
+
const { mkdtemp: mkdtemp5, writeFile: writeFile6 } = await import("fs/promises");
|
|
164
|
+
const { tmpdir: tmpdir5 } = await import("os");
|
|
165
|
+
const { join: join19 } = await import("path");
|
|
166
|
+
const path = outPath ?? join19(await mkdtemp5(join19(tmpdir5(), "traces-")), "spans.otlp.jsonl");
|
|
167
|
+
await writeFile6(path, serializeSpans(spans), "utf8");
|
|
58
168
|
return path;
|
|
59
169
|
}
|
|
60
170
|
|
|
@@ -89,150 +199,39 @@ async function analyzeSpans(spans, opts = {}) {
|
|
|
89
199
|
return { otlpPath, result };
|
|
90
200
|
}
|
|
91
201
|
|
|
92
|
-
// src/
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
out = cap(String(d), out);
|
|
126
|
-
});
|
|
127
|
-
child.stderr?.on("data", (d) => {
|
|
128
|
-
err = cap(String(d), err);
|
|
129
|
-
});
|
|
130
|
-
child.on("error", (e) => {
|
|
131
|
-
clearTimeout(timer);
|
|
132
|
-
if (!killed) reject(e);
|
|
133
|
-
});
|
|
134
|
-
child.on("close", (code) => {
|
|
135
|
-
clearTimeout(timer);
|
|
136
|
-
if (!killed) resolve({ stdout: out, stderr: err, code });
|
|
137
|
-
});
|
|
138
|
-
if (opts.input !== void 0) {
|
|
139
|
-
child.stdin?.on("error", () => {
|
|
140
|
-
});
|
|
141
|
-
child.stdin?.end(opts.input);
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
function commandAnalyzer(spec) {
|
|
146
|
-
return {
|
|
147
|
-
name: spec.name,
|
|
148
|
-
async analyze(otlpPath, opts = {}) {
|
|
149
|
-
try {
|
|
150
|
-
const res = await runCommand(spec.command, spec.args(otlpPath, opts.prompt), {
|
|
151
|
-
signal: opts.signal,
|
|
152
|
-
timeoutMs: spec.timeoutMs
|
|
153
|
-
});
|
|
154
|
-
if (res.code !== 0) {
|
|
155
|
-
return { analyzer: spec.name, ok: false, output: res.stdout, error: res.stderr.trim() || `exit ${res.code}` };
|
|
156
|
-
}
|
|
157
|
-
return { analyzer: spec.name, ok: true, output: res.stdout.trim(), findings: spec.parse?.(res.stdout) };
|
|
158
|
-
} catch (e) {
|
|
159
|
-
return { analyzer: spec.name, ok: false, output: "", error: e instanceof Error ? e.message : String(e) };
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
function toCanonicalOpenInference(otlpJsonl, opts = {}) {
|
|
165
|
-
return otlpJsonl.split("\n").filter((l) => l.trim()).map((line) => {
|
|
166
|
-
const s = JSON.parse(line);
|
|
167
|
-
const a = s.attributes ?? {};
|
|
168
|
-
const resourceAttrs = { "service.name": a["service.name"] ?? opts.service ?? "traces" };
|
|
169
|
-
if (a["agent.name"]) resourceAttrs["agent.name"] = a["agent.name"];
|
|
170
|
-
return JSON.stringify({
|
|
171
|
-
trace_id: s.trace_id,
|
|
172
|
-
span_id: s.span_id,
|
|
173
|
-
parent_span_id: s.parent_span_id ?? "",
|
|
174
|
-
trace_state: "",
|
|
175
|
-
name: s.name,
|
|
176
|
-
kind: a["openinference.span.kind"] ?? "CHAIN",
|
|
177
|
-
start_time: s.start_time,
|
|
178
|
-
end_time: s.end_time,
|
|
179
|
-
status: { code: s.status?.code ?? "OK", message: s.status?.message ?? "" },
|
|
180
|
-
resource: { attributes: resourceAttrs },
|
|
181
|
-
scope: { name: opts.scopeName ?? "tangle-traces", version: "" },
|
|
182
|
-
attributes: a
|
|
183
|
-
});
|
|
184
|
-
}).join("\n");
|
|
185
|
-
}
|
|
186
|
-
function haloAnalyzer(opts = {}) {
|
|
187
|
-
const command = opts.command ?? "halo";
|
|
188
|
-
const defaultPrompt = opts.defaultPrompt ?? "diagnose";
|
|
189
|
-
return {
|
|
190
|
-
name: "halo",
|
|
191
|
-
async analyze(otlpPath, o = {}) {
|
|
192
|
-
try {
|
|
193
|
-
const canonical = toCanonicalOpenInference(await readFile(otlpPath, "utf8"));
|
|
194
|
-
const canonPath = `${otlpPath}.halo.jsonl`;
|
|
195
|
-
await writeFile(canonPath, canonical);
|
|
196
|
-
const args = [canonPath, "-p", o.prompt ?? defaultPrompt];
|
|
197
|
-
if (opts.model) args.push("-m", opts.model);
|
|
198
|
-
const res = await runCommand(command, args, { signal: o.signal, timeoutMs: opts.timeoutMs });
|
|
199
|
-
if (res.code !== 0) {
|
|
200
|
-
return { analyzer: "halo", ok: false, output: res.stdout, error: res.stderr.trim() || `exit ${res.code}` };
|
|
201
|
-
}
|
|
202
|
-
return { analyzer: "halo", ok: true, output: res.stdout.trim() };
|
|
203
|
-
} catch (e) {
|
|
204
|
-
return { analyzer: "halo", ok: false, output: "", error: e instanceof Error ? e.message : String(e) };
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
function runExternalAnalyzers(otlpPath, analyzers, opts = {}) {
|
|
210
|
-
return Promise.all(analyzers.map((a) => a.analyze(otlpPath, opts)));
|
|
211
|
-
}
|
|
212
|
-
function commandRedactor(spec) {
|
|
213
|
-
const name = spec.name ?? spec.command;
|
|
214
|
-
return {
|
|
215
|
-
name,
|
|
216
|
-
async redactText(texts) {
|
|
217
|
-
if (texts.length === 0) return [];
|
|
218
|
-
const res = await runCommand(spec.command, spec.args ?? [], {
|
|
219
|
-
input: JSON.stringify(texts),
|
|
220
|
-
timeoutMs: spec.timeoutMs
|
|
221
|
-
});
|
|
222
|
-
if (res.code !== 0) throw new Error(`redactor ${name}: exit ${res.code}: ${res.stderr.trim()}`);
|
|
223
|
-
let parsed;
|
|
224
|
-
try {
|
|
225
|
-
parsed = JSON.parse(res.stdout);
|
|
226
|
-
} catch {
|
|
227
|
-
throw new Error(`redactor ${name}: stdout was not JSON`);
|
|
228
|
-
}
|
|
229
|
-
if (!Array.isArray(parsed) || parsed.length !== texts.length) {
|
|
230
|
-
throw new Error(`redactor ${name}: expected a JSON array of ${texts.length} strings`);
|
|
231
|
-
}
|
|
232
|
-
return parsed.map(String);
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
}
|
|
202
|
+
// src/attributes.ts
|
|
203
|
+
var ATTR = {
|
|
204
|
+
/** = the trace/session id. Wire contract (dedup linkage). */
|
|
205
|
+
SESSION_ID: "tangle.sessionId",
|
|
206
|
+
/** "cli" marks a CLI upload so the platform can drop a re-upload of a session
|
|
207
|
+
* that also streamed live. Wire contract. */
|
|
208
|
+
INGEST_SOURCE: "tangle.ingest_source",
|
|
209
|
+
HARNESS: "tangle.harness",
|
|
210
|
+
CWD: "tangle.cwd",
|
|
211
|
+
/** THE per-session grouping key the spine derives subjects from
|
|
212
|
+
* (`deriveSubjectKey` reads `tangle.subject.key` first). Resolves to the git
|
|
213
|
+
* remote (host/owner/repo) when readable, else the cwd path basename. */
|
|
214
|
+
SUBJECT_KEY: "tangle.subject.key",
|
|
215
|
+
/** Normalized git remote (e.g. `github.com/tangle-network/agent-dev-container`). */
|
|
216
|
+
GIT_REPOSITORY: "git.repository",
|
|
217
|
+
/** Current branch at conversion time. */
|
|
218
|
+
GIT_BRANCH_NAME: "git.branch",
|
|
219
|
+
/** HEAD short sha at conversion time. */
|
|
220
|
+
GIT_COMMIT: "git.commit",
|
|
221
|
+
/** How traces resolved the session cwd: recorded cwd, repaired cwd, or span path. */
|
|
222
|
+
REPO_RESOLUTION_SOURCE: "traces.repo_resolution_source",
|
|
223
|
+
GIT_BRANCH: "tangle.git_branch",
|
|
224
|
+
HOST: "tangle.host",
|
|
225
|
+
/** Basename of the session file. (Renamed from the ambiguous `tangle.source`,
|
|
226
|
+
* which collided with `tangle.ingest_source`.) */
|
|
227
|
+
SESSION_FILE: "tangle.session_file",
|
|
228
|
+
UPLOADED_AT: "tangle.uploaded_at",
|
|
229
|
+
UPLOADER: "tangle.uploader",
|
|
230
|
+
REDACTION_VERSION: "redaction.version",
|
|
231
|
+
REDACTION_COUNT: "redaction.count"
|
|
232
|
+
};
|
|
233
|
+
var INGEST_SOURCE_CLI = "cli";
|
|
234
|
+
var DEFAULT_HARNESS = "claude-code";
|
|
236
235
|
|
|
237
236
|
// src/time.ts
|
|
238
237
|
function parseIsoToEpochMs(ts) {
|
|
@@ -310,6 +309,7 @@ function capText(raw) {
|
|
|
310
309
|
if (t.length <= CONTENT_CAP) return t;
|
|
311
310
|
return `${t.slice(0, CONTENT_CAP)}\u2026 [+${t.length - CONTENT_CAP} chars]`;
|
|
312
311
|
}
|
|
312
|
+
var ACTOR_ATTR = "tangle.actor";
|
|
313
313
|
function userPromptSpan(o) {
|
|
314
314
|
return span({
|
|
315
315
|
traceId: o.traceId,
|
|
@@ -321,14 +321,15 @@ function userPromptSpan(o) {
|
|
|
321
321
|
service: o.service ?? null,
|
|
322
322
|
agent: o.agent ?? null,
|
|
323
323
|
step: o.step,
|
|
324
|
-
content: o.content
|
|
324
|
+
content: o.content,
|
|
325
|
+
extra: { [ACTOR_ATTR]: o.actor ?? "human" }
|
|
325
326
|
});
|
|
326
327
|
}
|
|
327
328
|
|
|
328
329
|
// src/adapters/amp.ts
|
|
329
330
|
import { readdir, readFile as readFile2, stat } from "fs/promises";
|
|
330
331
|
import { homedir } from "os";
|
|
331
|
-
import { basename, join } from "path";
|
|
332
|
+
import { basename, join as join2 } from "path";
|
|
332
333
|
var SERVICE = "amp";
|
|
333
334
|
function textOf(content) {
|
|
334
335
|
if (typeof content === "string") return content.slice(0, CONTENT_CAP);
|
|
@@ -339,8 +340,8 @@ function textOf(content) {
|
|
|
339
340
|
var AmpAdapter = class {
|
|
340
341
|
harness = "amp";
|
|
341
342
|
threadsRoot() {
|
|
342
|
-
const base = process.env.XDG_DATA_HOME ??
|
|
343
|
-
return
|
|
343
|
+
const base = process.env.XDG_DATA_HOME ?? join2(homedir(), ".local", "share");
|
|
344
|
+
return join2(base, "amp", "threads");
|
|
344
345
|
}
|
|
345
346
|
async locate(opts = {}) {
|
|
346
347
|
let files;
|
|
@@ -352,7 +353,7 @@ var AmpAdapter = class {
|
|
|
352
353
|
const refs = [];
|
|
353
354
|
for (const f of files) {
|
|
354
355
|
if (!f.startsWith("T-") || !f.endsWith(".json")) continue;
|
|
355
|
-
const path =
|
|
356
|
+
const path = join2(this.threadsRoot(), f);
|
|
356
357
|
let st;
|
|
357
358
|
try {
|
|
358
359
|
st = await stat(path);
|
|
@@ -451,10 +452,98 @@ var AmpAdapter = class {
|
|
|
451
452
|
}
|
|
452
453
|
};
|
|
453
454
|
|
|
455
|
+
// src/adapters/actor.ts
|
|
456
|
+
var CMD_MARKERS = [
|
|
457
|
+
"<command-name>",
|
|
458
|
+
"<local-command-caveat>",
|
|
459
|
+
"<command-message>",
|
|
460
|
+
"<command-args>",
|
|
461
|
+
"<command-stdout>"
|
|
462
|
+
];
|
|
463
|
+
var INJECT_MARKERS = [
|
|
464
|
+
"<system-reminder>",
|
|
465
|
+
"Caveat: The messages below",
|
|
466
|
+
"<user-memory-input>",
|
|
467
|
+
// Harness-injected wrappers that arrive as `userType: external` user turns —
|
|
468
|
+
// they pass the structural human test but are the harness feeding the model,
|
|
469
|
+
// not a person. (Observed in Claude Code task/loop sessions.)
|
|
470
|
+
"<task-notification>",
|
|
471
|
+
"<task-prompt>",
|
|
472
|
+
"<local-command-stdout>",
|
|
473
|
+
"This session is being continued from a previous conversation",
|
|
474
|
+
// Slash-command skill bodies are expanded into a user turn (no <command-name>
|
|
475
|
+
// wrapper) — the skill prompt, not the human.
|
|
476
|
+
"Base directory for this skill:"
|
|
477
|
+
];
|
|
478
|
+
var SYNTHETIC_MARKERS = [
|
|
479
|
+
"You are in a development environment with full file system access",
|
|
480
|
+
"## What the user asked for",
|
|
481
|
+
"Build the project described below",
|
|
482
|
+
"Filesystem sandboxing defines",
|
|
483
|
+
"<permissions instructions>",
|
|
484
|
+
"You are an autonomous",
|
|
485
|
+
"You are a coding agent",
|
|
486
|
+
"Your task is to build",
|
|
487
|
+
"## Deliverable",
|
|
488
|
+
"## Acceptance criteria",
|
|
489
|
+
"Return ONLY",
|
|
490
|
+
"respond with JSON",
|
|
491
|
+
"<environment_details>",
|
|
492
|
+
"<task>",
|
|
493
|
+
"Review this change before it is pushed",
|
|
494
|
+
"Complete a security review",
|
|
495
|
+
"Focus on correctness, regressions, security issues",
|
|
496
|
+
"Read-only critical audit",
|
|
497
|
+
"Staff-engineer code review"
|
|
498
|
+
];
|
|
499
|
+
var AGENT_PROMPT = /^(you are\b|you're\b|read[- ]only\b|stop now\b|your task\b|your job\b|review the\b|audit\b|analyze the\b|return (only|a |the )|here is the\b|context:|task:|do not (revert|edit|touch)|work only in\b|you have been\b|act as\b|operate the\b|goal:|objective:)/i;
|
|
500
|
+
function textIsCmdOrInject(s) {
|
|
501
|
+
return CMD_MARKERS.some((m) => s.includes(m)) || INJECT_MARKERS.some((m) => s.includes(m));
|
|
502
|
+
}
|
|
503
|
+
function textIsSynthetic(s) {
|
|
504
|
+
const head = s.slice(0, 600);
|
|
505
|
+
return SYNTHETIC_MARKERS.some((m) => head.includes(m));
|
|
506
|
+
}
|
|
507
|
+
function looksLikeAgentPrompt(s) {
|
|
508
|
+
const t = s.trim();
|
|
509
|
+
if (t.length > 1500) return true;
|
|
510
|
+
return AGENT_PROMPT.test(t);
|
|
511
|
+
}
|
|
512
|
+
function claudeActor(args) {
|
|
513
|
+
if (args.isSidechain === true) return "subagent-spawn";
|
|
514
|
+
if (args.userType != null && args.userType !== "external") return "injected";
|
|
515
|
+
if (textIsCmdOrInject(args.text)) return "injected";
|
|
516
|
+
if (textIsSynthetic(args.text)) return "injected";
|
|
517
|
+
if (args.isFirstUserTurn && looksLikeAgentPrompt(args.text)) return "subagent-spawn";
|
|
518
|
+
return "human";
|
|
519
|
+
}
|
|
520
|
+
function codexActor(args) {
|
|
521
|
+
if (textIsCmdOrInject(args.text)) return "injected";
|
|
522
|
+
if (textIsSynthetic(args.text)) return "injected";
|
|
523
|
+
if (args.isFirstUserTurn && looksLikeAgentPrompt(args.text)) return "injected";
|
|
524
|
+
return "human";
|
|
525
|
+
}
|
|
526
|
+
var JARGON_COMPLAINT = /\b(jargon|plain english|plain language|eli5|explain like|in plain|what does that mean|what does .* mean|i don'?t (understand|get)|gibberish|english please|too technical|speak (plainly|english)|dumb(ed)? it down|simpler|simplify your|no buzzwords?)\b/;
|
|
527
|
+
var STRUCTURE_COMPLAINT = /\b(too long|tl;?dr|get to the point|bluf|lead with|bullet|bulletpoint|wall of text|too verbose|shorter|be concise|stop (yapping|rambling)|less words?|fewer words?|just (the|tell me)|cut the)\b/;
|
|
528
|
+
var CORRECTION = /(\bno[,. ]|^no\b|\bstop\b|\bdon'?t\b|you keep|i (said|told you|asked)|that'?s not (what|right|it)|not what i|didn'?t ask|incorrect|that'?s wrong|you'?re wrong|why are you|why did you|that'?s not the|missed the|not the point|ur wrong|thats not)/;
|
|
529
|
+
var FRUSTRATION = /(ugh|wtf|wth|come on|seriously|again\?|for the (last|nth) time|are you kidding|jesus|christ|frustrat|annoying|wasting|waste of)/;
|
|
530
|
+
var PRAISE = /\b(perfect|exactly|nailed it|love (it|this)|beautiful|great work|ship it|clean|nice work|excellent|that'?s it|yes!+|amazing|chef'?s kiss|brilliant)\b/;
|
|
531
|
+
function classifyReaction(text) {
|
|
532
|
+
const t = text.toLowerCase();
|
|
533
|
+
const hits = [];
|
|
534
|
+
if (JARGON_COMPLAINT.test(t)) hits.push("jargon");
|
|
535
|
+
if (STRUCTURE_COMPLAINT.test(t)) hits.push("structure");
|
|
536
|
+
if (CORRECTION.test(t)) hits.push("correction");
|
|
537
|
+
if (FRUSTRATION.test(t)) hits.push("frustration");
|
|
538
|
+
if (PRAISE.test(t)) hits.push("praise");
|
|
539
|
+
return hits;
|
|
540
|
+
}
|
|
541
|
+
var CORRECTIVE_REACTIONS = ["correction", "frustration", "jargon", "structure"];
|
|
542
|
+
|
|
454
543
|
// src/adapters/claude.ts
|
|
455
544
|
import { readdir as readdir2, readFile as readFile3, stat as stat2 } from "fs/promises";
|
|
456
545
|
import { homedir as homedir2 } from "os";
|
|
457
|
-
import { basename as basename2, join as
|
|
546
|
+
import { basename as basename2, join as join3 } from "path";
|
|
458
547
|
var SERVICE2 = "claude-code";
|
|
459
548
|
function parseLines(raw) {
|
|
460
549
|
const out = [];
|
|
@@ -492,6 +581,7 @@ function parseClaudeStream(events, ctx) {
|
|
|
492
581
|
const spans = [];
|
|
493
582
|
const toolSpanByUseId = /* @__PURE__ */ new Map();
|
|
494
583
|
let step = ctx.startStep;
|
|
584
|
+
let sawUserTurn = false;
|
|
495
585
|
for (const ev of events) {
|
|
496
586
|
const ts = ev.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
497
587
|
const uid = ev.uuid ?? `${ctx.idPrefix}step${step}`;
|
|
@@ -539,6 +629,13 @@ function parseClaudeStream(events, ctx) {
|
|
|
539
629
|
} else if (ev.type === "user" && ev.message) {
|
|
540
630
|
const prompt = textOf2(ev.message.content);
|
|
541
631
|
if (prompt) {
|
|
632
|
+
const actor = claudeActor({
|
|
633
|
+
text: prompt,
|
|
634
|
+
isSidechain: ev.isSidechain,
|
|
635
|
+
userType: ev.userType ?? null,
|
|
636
|
+
isFirstUserTurn: !sawUserTurn
|
|
637
|
+
});
|
|
638
|
+
sawUserTurn = true;
|
|
542
639
|
spans.push(
|
|
543
640
|
userPromptSpan({
|
|
544
641
|
traceId: ctx.traceId,
|
|
@@ -548,7 +645,8 @@ function parseClaudeStream(events, ctx) {
|
|
|
548
645
|
service: SERVICE2,
|
|
549
646
|
agent: ctx.agent,
|
|
550
647
|
step,
|
|
551
|
-
content: prompt
|
|
648
|
+
content: prompt,
|
|
649
|
+
actor
|
|
552
650
|
})
|
|
553
651
|
);
|
|
554
652
|
step += 1;
|
|
@@ -575,7 +673,7 @@ var ClaudeAdapter = class {
|
|
|
575
673
|
harness = "claude-code";
|
|
576
674
|
aliases = ["claude", "claudish", "openclaw", "nanoclaw"];
|
|
577
675
|
root() {
|
|
578
|
-
return
|
|
676
|
+
return join3(homedir2(), ".claude", "projects");
|
|
579
677
|
}
|
|
580
678
|
async locate(opts = {}) {
|
|
581
679
|
const root = this.root();
|
|
@@ -587,7 +685,7 @@ var ClaudeAdapter = class {
|
|
|
587
685
|
}
|
|
588
686
|
const refs = [];
|
|
589
687
|
for (const dir of projectDirs) {
|
|
590
|
-
const dirPath =
|
|
688
|
+
const dirPath = join3(root, dir);
|
|
591
689
|
let files;
|
|
592
690
|
try {
|
|
593
691
|
files = await readdir2(dirPath);
|
|
@@ -596,7 +694,7 @@ var ClaudeAdapter = class {
|
|
|
596
694
|
}
|
|
597
695
|
for (const f of files) {
|
|
598
696
|
if (!f.endsWith(".jsonl")) continue;
|
|
599
|
-
const path =
|
|
697
|
+
const path = join3(dirPath, f);
|
|
600
698
|
let st;
|
|
601
699
|
try {
|
|
602
700
|
st = await stat2(path);
|
|
@@ -643,7 +741,7 @@ var ClaudeAdapter = class {
|
|
|
643
741
|
}
|
|
644
742
|
/** Parse `<session>/subagents/agent-*.jsonl`, parenting each under its Agent call. */
|
|
645
743
|
async foldSubagents(ref, traceId, main, out) {
|
|
646
|
-
const subDir =
|
|
744
|
+
const subDir = join3(ref.path.replace(/\.jsonl$/, ""), "subagents");
|
|
647
745
|
let files;
|
|
648
746
|
try {
|
|
649
747
|
files = await readdir2(subDir);
|
|
@@ -656,13 +754,13 @@ var ClaudeAdapter = class {
|
|
|
656
754
|
const hash = basename2(f, ".jsonl");
|
|
657
755
|
let meta = {};
|
|
658
756
|
try {
|
|
659
|
-
meta = JSON.parse(await readFile3(
|
|
757
|
+
meta = JSON.parse(await readFile3(join3(subDir, `${hash}.meta.json`), "utf8"));
|
|
660
758
|
} catch {
|
|
661
759
|
}
|
|
662
760
|
const parent = meta.toolUseId && main.toolSpanByUseId.get(meta.toolUseId)?.span_id || `root:${traceId}`;
|
|
663
761
|
let raw;
|
|
664
762
|
try {
|
|
665
|
-
raw = await readFile3(
|
|
763
|
+
raw = await readFile3(join3(subDir, f), "utf8");
|
|
666
764
|
} catch {
|
|
667
765
|
continue;
|
|
668
766
|
}
|
|
@@ -682,7 +780,7 @@ var ClaudeAdapter = class {
|
|
|
682
780
|
// src/adapters/codex.ts
|
|
683
781
|
import { readdir as readdir3, readFile as readFile4, stat as stat3 } from "fs/promises";
|
|
684
782
|
import { homedir as homedir3 } from "os";
|
|
685
|
-
import { basename as basename3, join as
|
|
783
|
+
import { basename as basename3, join as join4 } from "path";
|
|
686
784
|
var SERVICE3 = "codex";
|
|
687
785
|
function parseLines2(raw) {
|
|
688
786
|
const out = [];
|
|
@@ -718,7 +816,7 @@ async function* walkRollouts(root) {
|
|
|
718
816
|
return;
|
|
719
817
|
}
|
|
720
818
|
for (const y of years) {
|
|
721
|
-
const yp =
|
|
819
|
+
const yp = join4(root, y);
|
|
722
820
|
let months;
|
|
723
821
|
try {
|
|
724
822
|
months = await readdir3(yp);
|
|
@@ -726,7 +824,7 @@ async function* walkRollouts(root) {
|
|
|
726
824
|
continue;
|
|
727
825
|
}
|
|
728
826
|
for (const m of months) {
|
|
729
|
-
const mp =
|
|
827
|
+
const mp = join4(yp, m);
|
|
730
828
|
let days;
|
|
731
829
|
try {
|
|
732
830
|
days = await readdir3(mp);
|
|
@@ -734,7 +832,7 @@ async function* walkRollouts(root) {
|
|
|
734
832
|
continue;
|
|
735
833
|
}
|
|
736
834
|
for (const d of days) {
|
|
737
|
-
const dp =
|
|
835
|
+
const dp = join4(mp, d);
|
|
738
836
|
let files;
|
|
739
837
|
try {
|
|
740
838
|
files = await readdir3(dp);
|
|
@@ -742,7 +840,7 @@ async function* walkRollouts(root) {
|
|
|
742
840
|
continue;
|
|
743
841
|
}
|
|
744
842
|
for (const f of files) {
|
|
745
|
-
if (f.startsWith("rollout-") && f.endsWith(".jsonl")) yield
|
|
843
|
+
if (f.startsWith("rollout-") && f.endsWith(".jsonl")) yield join4(dp, f);
|
|
746
844
|
}
|
|
747
845
|
}
|
|
748
846
|
}
|
|
@@ -752,7 +850,7 @@ var CodexAdapter = class {
|
|
|
752
850
|
harness = "codex";
|
|
753
851
|
aliases = ["codex-acp"];
|
|
754
852
|
root() {
|
|
755
|
-
return process.env.CODEX_HOME ?
|
|
853
|
+
return process.env.CODEX_HOME ? join4(process.env.CODEX_HOME, "sessions") : join4(homedir3(), ".codex", "sessions");
|
|
756
854
|
}
|
|
757
855
|
async locate(opts = {}) {
|
|
758
856
|
const refs = [];
|
|
@@ -764,14 +862,18 @@ var CodexAdapter = class {
|
|
|
764
862
|
continue;
|
|
765
863
|
}
|
|
766
864
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
767
|
-
const head = (await readFile4(path, "utf8").catch(() => "")).split("\n",
|
|
865
|
+
const head = (await readFile4(path, "utf8").catch(() => "")).split("\n", 40);
|
|
768
866
|
let cwd = null;
|
|
769
867
|
let id = basename3(path).replace(/^rollout-[\dT-]+-/, "").replace(/\.jsonl$/, "");
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
868
|
+
for (const line of head) {
|
|
869
|
+
if (!line) continue;
|
|
870
|
+
try {
|
|
871
|
+
const l = JSON.parse(line);
|
|
872
|
+
if (l.type === "session_meta" && l.payload?.id) id = l.payload.id;
|
|
873
|
+
if (!cwd && l.payload?.cwd) cwd = l.payload.cwd;
|
|
874
|
+
} catch {
|
|
875
|
+
}
|
|
876
|
+
if (cwd) break;
|
|
775
877
|
}
|
|
776
878
|
if (opts.cwd && (!cwd || !cwd.startsWith(opts.cwd))) continue;
|
|
777
879
|
refs.push({ harness: this.harness, sessionId: id, path, cwd, mtimeMs: st.mtimeMs });
|
|
@@ -801,6 +903,7 @@ var CodexAdapter = class {
|
|
|
801
903
|
const toolByCallId = /* @__PURE__ */ new Map();
|
|
802
904
|
let step = 0;
|
|
803
905
|
let lastLlm = rootId;
|
|
906
|
+
let sawUserTurn = false;
|
|
804
907
|
for (const l of lines) {
|
|
805
908
|
const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
806
909
|
if (l.type === "event_msg" && l.payload?.type === "token_count") {
|
|
@@ -855,6 +958,8 @@ var CodexAdapter = class {
|
|
|
855
958
|
} else if (l.type === "response_item" && l.payload?.type === "message" && l.payload.role === "user") {
|
|
856
959
|
const prompt = textOf3(l.payload.content);
|
|
857
960
|
if (prompt) {
|
|
961
|
+
const actor = codexActor({ text: prompt, isFirstUserTurn: !sawUserTurn });
|
|
962
|
+
sawUserTurn = true;
|
|
858
963
|
spans.push(
|
|
859
964
|
userPromptSpan({
|
|
860
965
|
traceId,
|
|
@@ -864,7 +969,8 @@ var CodexAdapter = class {
|
|
|
864
969
|
content: prompt,
|
|
865
970
|
service: SERVICE3,
|
|
866
971
|
agent: SERVICE3,
|
|
867
|
-
step
|
|
972
|
+
step,
|
|
973
|
+
actor
|
|
868
974
|
})
|
|
869
975
|
);
|
|
870
976
|
step += 1;
|
|
@@ -897,7 +1003,7 @@ var CodexAdapter = class {
|
|
|
897
1003
|
// src/adapters/copilot.ts
|
|
898
1004
|
import { readdir as readdir4, readFile as readFile5, stat as stat4 } from "fs/promises";
|
|
899
1005
|
import { homedir as homedir4 } from "os";
|
|
900
|
-
import { join as
|
|
1006
|
+
import { join as join5 } from "path";
|
|
901
1007
|
var SERVICE4 = "github-copilot";
|
|
902
1008
|
function parseLines3(raw) {
|
|
903
1009
|
const out = [];
|
|
@@ -914,7 +1020,7 @@ var CopilotAdapter = class {
|
|
|
914
1020
|
harness = "github-copilot";
|
|
915
1021
|
aliases = ["copilot"];
|
|
916
1022
|
root() {
|
|
917
|
-
return
|
|
1023
|
+
return join5(process.env.COPILOT_HOME ?? join5(homedir4(), ".copilot"), "session-state");
|
|
918
1024
|
}
|
|
919
1025
|
async locate(opts = {}) {
|
|
920
1026
|
let dirs;
|
|
@@ -925,7 +1031,7 @@ var CopilotAdapter = class {
|
|
|
925
1031
|
}
|
|
926
1032
|
const refs = [];
|
|
927
1033
|
for (const id of dirs) {
|
|
928
|
-
const path =
|
|
1034
|
+
const path = join5(this.root(), id, "events.jsonl");
|
|
929
1035
|
let st;
|
|
930
1036
|
try {
|
|
931
1037
|
st = await stat4(path);
|
|
@@ -1023,7 +1129,7 @@ var CopilotAdapter = class {
|
|
|
1023
1129
|
// src/adapters/factory.ts
|
|
1024
1130
|
import { readdir as readdir5, readFile as readFile6, stat as stat5 } from "fs/promises";
|
|
1025
1131
|
import { homedir as homedir5 } from "os";
|
|
1026
|
-
import { basename as basename4, join as
|
|
1132
|
+
import { basename as basename4, join as join6 } from "path";
|
|
1027
1133
|
var SERVICE5 = "factory";
|
|
1028
1134
|
function textOf4(content) {
|
|
1029
1135
|
return capText(
|
|
@@ -1045,7 +1151,7 @@ var FactoryAdapter = class {
|
|
|
1045
1151
|
harness = "factory";
|
|
1046
1152
|
aliases = ["factory-droids", "droid"];
|
|
1047
1153
|
root() {
|
|
1048
|
-
return
|
|
1154
|
+
return join6(homedir5(), ".factory", "sessions");
|
|
1049
1155
|
}
|
|
1050
1156
|
async locate(opts = {}) {
|
|
1051
1157
|
const root = this.root();
|
|
@@ -1057,7 +1163,7 @@ var FactoryAdapter = class {
|
|
|
1057
1163
|
}
|
|
1058
1164
|
const refs = [];
|
|
1059
1165
|
for (const dir of dirs) {
|
|
1060
|
-
const dp =
|
|
1166
|
+
const dp = join6(root, dir);
|
|
1061
1167
|
let files;
|
|
1062
1168
|
try {
|
|
1063
1169
|
files = await readdir5(dp);
|
|
@@ -1068,7 +1174,7 @@ var FactoryAdapter = class {
|
|
|
1068
1174
|
if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
|
|
1069
1175
|
for (const f of files) {
|
|
1070
1176
|
if (!f.endsWith(".jsonl")) continue;
|
|
1071
|
-
const path =
|
|
1177
|
+
const path = join6(dp, f);
|
|
1072
1178
|
let st;
|
|
1073
1179
|
try {
|
|
1074
1180
|
st = await stat5(path);
|
|
@@ -1184,7 +1290,7 @@ var FactoryAdapter = class {
|
|
|
1184
1290
|
// src/adapters/forge.ts
|
|
1185
1291
|
import { readdir as readdir6, readFile as readFile7, stat as stat6 } from "fs/promises";
|
|
1186
1292
|
import { homedir as homedir6 } from "os";
|
|
1187
|
-
import { basename as basename5, join as
|
|
1293
|
+
import { basename as basename5, join as join7 } from "path";
|
|
1188
1294
|
var SERVICE6 = "forge";
|
|
1189
1295
|
function tokens(t) {
|
|
1190
1296
|
if (!t) return null;
|
|
@@ -1198,7 +1304,7 @@ var ForgeAdapter = class {
|
|
|
1198
1304
|
aliases = ["forgecode"];
|
|
1199
1305
|
/** Directories scanned for `*-dump.json` exports. */
|
|
1200
1306
|
dumpDirs() {
|
|
1201
|
-
return [process.cwd(),
|
|
1307
|
+
return [process.cwd(), join7(homedir6(), ".forge")];
|
|
1202
1308
|
}
|
|
1203
1309
|
async locate(opts = {}) {
|
|
1204
1310
|
const refs = [];
|
|
@@ -1212,7 +1318,7 @@ var ForgeAdapter = class {
|
|
|
1212
1318
|
}
|
|
1213
1319
|
for (const f of files) {
|
|
1214
1320
|
if (!f.endsWith("-dump.json")) continue;
|
|
1215
|
-
const path =
|
|
1321
|
+
const path = join7(dir, f);
|
|
1216
1322
|
let st;
|
|
1217
1323
|
try {
|
|
1218
1324
|
st = await stat6(path);
|
|
@@ -1314,7 +1420,7 @@ var ForgeAdapter = class {
|
|
|
1314
1420
|
// src/adapters/gemini.ts
|
|
1315
1421
|
import { readdir as readdir7, readFile as readFile8, stat as stat7 } from "fs/promises";
|
|
1316
1422
|
import { homedir as homedir7 } from "os";
|
|
1317
|
-
import { join as
|
|
1423
|
+
import { join as join8 } from "path";
|
|
1318
1424
|
function iso(ts, fallback) {
|
|
1319
1425
|
if (typeof ts === "number") return new Date(ts).toISOString();
|
|
1320
1426
|
if (typeof ts === "string" && ts.length > 0) return ts;
|
|
@@ -1339,7 +1445,21 @@ var GeminiFamilyAdapter = class {
|
|
|
1339
1445
|
this.homeDirName = cfg.homeDirName;
|
|
1340
1446
|
}
|
|
1341
1447
|
chatsRoot() {
|
|
1342
|
-
return
|
|
1448
|
+
return join8(homedir7(), this.homeDirName, "tmp");
|
|
1449
|
+
}
|
|
1450
|
+
/** `<home>/projects.json` maps `{ <absolute-cwd>: <projectName> }`. The session
|
|
1451
|
+
* dir name (`tmp/<projectHash>/`) is that projectName for registered projects,
|
|
1452
|
+
* so inverting the map recovers cwd. (Unregistered/older dirs are opaque cwd
|
|
1453
|
+
* digests and stay null.) Loaded once per locate(). */
|
|
1454
|
+
async loadProjectCwds() {
|
|
1455
|
+
const nameToPath = /* @__PURE__ */ new Map();
|
|
1456
|
+
try {
|
|
1457
|
+
const raw = await readFile8(join8(homedir7(), this.homeDirName, "projects.json"), "utf8");
|
|
1458
|
+
const projects = JSON.parse(raw).projects ?? {};
|
|
1459
|
+
for (const [path, name] of Object.entries(projects)) nameToPath.set(name, path);
|
|
1460
|
+
} catch {
|
|
1461
|
+
}
|
|
1462
|
+
return nameToPath;
|
|
1343
1463
|
}
|
|
1344
1464
|
async locate(opts = {}) {
|
|
1345
1465
|
const root = this.chatsRoot();
|
|
@@ -1349,9 +1469,11 @@ var GeminiFamilyAdapter = class {
|
|
|
1349
1469
|
} catch {
|
|
1350
1470
|
return [];
|
|
1351
1471
|
}
|
|
1472
|
+
const projectCwds = await this.loadProjectCwds();
|
|
1352
1473
|
const refs = [];
|
|
1353
1474
|
for (const pd of projectDirs) {
|
|
1354
|
-
const
|
|
1475
|
+
const cwd = projectCwds.get(pd) ?? null;
|
|
1476
|
+
const chatsDir = join8(root, pd, "chats");
|
|
1355
1477
|
let files;
|
|
1356
1478
|
try {
|
|
1357
1479
|
files = await readdir7(chatsDir);
|
|
@@ -1360,7 +1482,7 @@ var GeminiFamilyAdapter = class {
|
|
|
1360
1482
|
}
|
|
1361
1483
|
for (const f of files) {
|
|
1362
1484
|
if (!f.startsWith("session-") || !f.endsWith(".json")) continue;
|
|
1363
|
-
const path =
|
|
1485
|
+
const path = join8(chatsDir, f);
|
|
1364
1486
|
let st;
|
|
1365
1487
|
try {
|
|
1366
1488
|
st = await stat7(path);
|
|
@@ -1368,8 +1490,8 @@ var GeminiFamilyAdapter = class {
|
|
|
1368
1490
|
continue;
|
|
1369
1491
|
}
|
|
1370
1492
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
1371
|
-
if (opts.cwd) continue;
|
|
1372
|
-
refs.push({ harness: this.harness, sessionId: f.replace(/\.json$/, ""), path, cwd
|
|
1493
|
+
if (opts.cwd && (!cwd || !cwd.startsWith(opts.cwd))) continue;
|
|
1494
|
+
refs.push({ harness: this.harness, sessionId: f.replace(/\.json$/, ""), path, cwd, mtimeMs: st.mtimeMs });
|
|
1373
1495
|
}
|
|
1374
1496
|
}
|
|
1375
1497
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
@@ -1473,7 +1595,7 @@ var GeminiAdapter = class extends GeminiFamilyAdapter {
|
|
|
1473
1595
|
// src/adapters/opencode.ts
|
|
1474
1596
|
import { readdir as readdir8, readFile as readFile9, stat as stat8 } from "fs/promises";
|
|
1475
1597
|
import { homedir as homedir8 } from "os";
|
|
1476
|
-
import { join as
|
|
1598
|
+
import { join as join9 } from "path";
|
|
1477
1599
|
var SERVICE7 = "opencode";
|
|
1478
1600
|
function isoOf(ms) {
|
|
1479
1601
|
return new Date(ms ?? 0).toISOString();
|
|
@@ -1486,11 +1608,11 @@ function textOf7(parts) {
|
|
|
1486
1608
|
var OpencodeAdapter = class {
|
|
1487
1609
|
harness = "opencode";
|
|
1488
1610
|
storage() {
|
|
1489
|
-
const base = process.env.XDG_DATA_HOME ??
|
|
1490
|
-
return
|
|
1611
|
+
const base = process.env.XDG_DATA_HOME ?? join9(homedir8(), ".local", "share");
|
|
1612
|
+
return join9(base, "opencode", "storage");
|
|
1491
1613
|
}
|
|
1492
1614
|
async locate(opts = {}) {
|
|
1493
|
-
const msgRoot =
|
|
1615
|
+
const msgRoot = join9(this.storage(), "message");
|
|
1494
1616
|
let sessionDirs;
|
|
1495
1617
|
try {
|
|
1496
1618
|
sessionDirs = await readdir8(msgRoot);
|
|
@@ -1499,7 +1621,7 @@ var OpencodeAdapter = class {
|
|
|
1499
1621
|
}
|
|
1500
1622
|
const refs = [];
|
|
1501
1623
|
for (const sid of sessionDirs) {
|
|
1502
|
-
const dir =
|
|
1624
|
+
const dir = join9(msgRoot, sid);
|
|
1503
1625
|
let st;
|
|
1504
1626
|
try {
|
|
1505
1627
|
st = await stat8(dir);
|
|
@@ -1516,7 +1638,7 @@ var OpencodeAdapter = class {
|
|
|
1516
1638
|
async parse(ref) {
|
|
1517
1639
|
const traceId = ref.sessionId;
|
|
1518
1640
|
const rootId = `root:${traceId}`;
|
|
1519
|
-
const partRoot =
|
|
1641
|
+
const partRoot = join9(this.storage(), "part");
|
|
1520
1642
|
let msgFiles;
|
|
1521
1643
|
try {
|
|
1522
1644
|
msgFiles = (await readdir8(ref.path)).filter((f) => f.endsWith(".json"));
|
|
@@ -1526,7 +1648,7 @@ var OpencodeAdapter = class {
|
|
|
1526
1648
|
const messages = [];
|
|
1527
1649
|
for (const f of msgFiles) {
|
|
1528
1650
|
try {
|
|
1529
|
-
messages.push({ msg: JSON.parse(await readFile9(
|
|
1651
|
+
messages.push({ msg: JSON.parse(await readFile9(join9(ref.path, f), "utf8")), file: f });
|
|
1530
1652
|
} catch {
|
|
1531
1653
|
}
|
|
1532
1654
|
}
|
|
@@ -1548,13 +1670,13 @@ var OpencodeAdapter = class {
|
|
|
1548
1670
|
for (const { msg } of messages) {
|
|
1549
1671
|
const mid = msg.id ?? `m${step}`;
|
|
1550
1672
|
const llmId = `llm:${mid}`;
|
|
1551
|
-
const pdir =
|
|
1673
|
+
const pdir = join9(partRoot, mid);
|
|
1552
1674
|
const parts = [];
|
|
1553
1675
|
try {
|
|
1554
1676
|
const partFiles = (await readdir8(pdir)).filter((f) => f.endsWith(".json"));
|
|
1555
1677
|
for (const pf of partFiles) {
|
|
1556
1678
|
try {
|
|
1557
|
-
parts.push(JSON.parse(await readFile9(
|
|
1679
|
+
parts.push(JSON.parse(await readFile9(join9(pdir, pf), "utf8")));
|
|
1558
1680
|
} catch {
|
|
1559
1681
|
}
|
|
1560
1682
|
}
|
|
@@ -1626,7 +1748,7 @@ var OpencodeAdapter = class {
|
|
|
1626
1748
|
// src/adapters/pi.ts
|
|
1627
1749
|
import { readdir as readdir9, readFile as readFile10, stat as stat9 } from "fs/promises";
|
|
1628
1750
|
import { homedir as homedir9 } from "os";
|
|
1629
|
-
import { basename as basename6, join as
|
|
1751
|
+
import { basename as basename6, join as join10 } from "path";
|
|
1630
1752
|
var SERVICE8 = "pi";
|
|
1631
1753
|
function parseLines5(raw) {
|
|
1632
1754
|
const out = [];
|
|
@@ -1654,7 +1776,7 @@ function textOf8(content) {
|
|
|
1654
1776
|
var PiAdapter = class {
|
|
1655
1777
|
harness = "pi";
|
|
1656
1778
|
root() {
|
|
1657
|
-
return
|
|
1779
|
+
return join10(homedir9(), ".pi", "agent", "sessions");
|
|
1658
1780
|
}
|
|
1659
1781
|
async locate(opts = {}) {
|
|
1660
1782
|
const root = this.root();
|
|
@@ -1666,7 +1788,7 @@ var PiAdapter = class {
|
|
|
1666
1788
|
}
|
|
1667
1789
|
const refs = [];
|
|
1668
1790
|
for (const dir of dirs) {
|
|
1669
|
-
const dp =
|
|
1791
|
+
const dp = join10(root, dir);
|
|
1670
1792
|
let files;
|
|
1671
1793
|
try {
|
|
1672
1794
|
files = await readdir9(dp);
|
|
@@ -1677,7 +1799,7 @@ var PiAdapter = class {
|
|
|
1677
1799
|
if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
|
|
1678
1800
|
for (const f of files) {
|
|
1679
1801
|
if (!f.endsWith(".jsonl")) continue;
|
|
1680
|
-
const path =
|
|
1802
|
+
const path = join10(dp, f);
|
|
1681
1803
|
let st;
|
|
1682
1804
|
try {
|
|
1683
1805
|
st = await stat9(path);
|
|
@@ -1795,7 +1917,7 @@ var PiAdapter = class {
|
|
|
1795
1917
|
// src/adapters/qwen.ts
|
|
1796
1918
|
import { readdir as readdir10, readFile as readFile11, stat as stat10 } from "fs/promises";
|
|
1797
1919
|
import { homedir as homedir10 } from "os";
|
|
1798
|
-
import { basename as basename7, join as
|
|
1920
|
+
import { basename as basename7, join as join11 } from "path";
|
|
1799
1921
|
var SERVICE9 = "qwen";
|
|
1800
1922
|
function parseLines6(raw) {
|
|
1801
1923
|
const out = [];
|
|
@@ -1816,7 +1938,7 @@ var QwenAdapter = class {
|
|
|
1816
1938
|
harness = "qwen";
|
|
1817
1939
|
aliases = ["qwen-code"];
|
|
1818
1940
|
root() {
|
|
1819
|
-
return
|
|
1941
|
+
return join11(homedir10(), ".qwen", "projects");
|
|
1820
1942
|
}
|
|
1821
1943
|
async locate(opts = {}) {
|
|
1822
1944
|
const root = this.root();
|
|
@@ -1828,7 +1950,7 @@ var QwenAdapter = class {
|
|
|
1828
1950
|
}
|
|
1829
1951
|
const refs = [];
|
|
1830
1952
|
for (const pd of projectDirs) {
|
|
1831
|
-
const chatsDir =
|
|
1953
|
+
const chatsDir = join11(root, pd, "chats");
|
|
1832
1954
|
let files;
|
|
1833
1955
|
try {
|
|
1834
1956
|
files = await readdir10(chatsDir);
|
|
@@ -1838,7 +1960,7 @@ var QwenAdapter = class {
|
|
|
1838
1960
|
if (opts.cwd) continue;
|
|
1839
1961
|
for (const f of files) {
|
|
1840
1962
|
if (!f.endsWith(".jsonl")) continue;
|
|
1841
|
-
const path =
|
|
1963
|
+
const path = join11(chatsDir, f);
|
|
1842
1964
|
let st;
|
|
1843
1965
|
try {
|
|
1844
1966
|
st = await stat10(path);
|
|
@@ -1988,7 +2110,279 @@ function selectAdapters(opts) {
|
|
|
1988
2110
|
});
|
|
1989
2111
|
}
|
|
1990
2112
|
|
|
2113
|
+
// src/repo.ts
|
|
2114
|
+
import { stat as stat11 } from "fs/promises";
|
|
2115
|
+
import { dirname, isAbsolute, join as join12, normalize, parse as parsePath } from "path";
|
|
2116
|
+
function normalizeRemote(url) {
|
|
2117
|
+
const raw = url.trim();
|
|
2118
|
+
if (!raw) return null;
|
|
2119
|
+
const url2 = raw.match(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/(.+)$/);
|
|
2120
|
+
if (url2) {
|
|
2121
|
+
let rest = url2[1];
|
|
2122
|
+
const at = rest.lastIndexOf("@");
|
|
2123
|
+
if (at !== -1) rest = rest.slice(at + 1);
|
|
2124
|
+
rest = rest.replace(/\.git$/, "").replace(/\/+$/g, "");
|
|
2125
|
+
const slash = rest.indexOf("/");
|
|
2126
|
+
if (slash === -1) return null;
|
|
2127
|
+
const host = rest.slice(0, slash).replace(/:\d+$/, "");
|
|
2128
|
+
const path = rest.slice(slash + 1).replace(/^\/+/, "");
|
|
2129
|
+
return host && path ? `${host}/${path}` : null;
|
|
2130
|
+
}
|
|
2131
|
+
const scp = raw.match(/^[^@/]+@([^:/]+):(.+)$/);
|
|
2132
|
+
if (scp) {
|
|
2133
|
+
const path = scp[2].replace(/\.git$/, "").replace(/^\/+|\/+$/g, "");
|
|
2134
|
+
return path ? `${scp[1]}/${path}` : null;
|
|
2135
|
+
}
|
|
2136
|
+
return null;
|
|
2137
|
+
}
|
|
2138
|
+
async function readGit(cwd) {
|
|
2139
|
+
await stat11(`${cwd}/.git`);
|
|
2140
|
+
const { execFile } = await import("child_process");
|
|
2141
|
+
const { promisify } = await import("util");
|
|
2142
|
+
const run = promisify(execFile);
|
|
2143
|
+
const git = async (args) => {
|
|
2144
|
+
try {
|
|
2145
|
+
const { stdout } = await run("git", ["-C", cwd, ...args], { timeout: 5e3 });
|
|
2146
|
+
const v = stdout.trim();
|
|
2147
|
+
return v.length > 0 ? v : null;
|
|
2148
|
+
} catch {
|
|
2149
|
+
return null;
|
|
2150
|
+
}
|
|
2151
|
+
};
|
|
2152
|
+
let remote = await git(["remote", "get-url", "origin"]);
|
|
2153
|
+
if (!remote) {
|
|
2154
|
+
const first = await git(["remote"]);
|
|
2155
|
+
const name = first?.split("\n")[0]?.trim();
|
|
2156
|
+
if (name) remote = await git(["remote", "get-url", name]);
|
|
2157
|
+
}
|
|
2158
|
+
const branch = await git(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
2159
|
+
const commit = await git(["rev-parse", "--short", "HEAD"]);
|
|
2160
|
+
return {
|
|
2161
|
+
remote,
|
|
2162
|
+
branch: branch === "HEAD" ? null : branch,
|
|
2163
|
+
// detached HEAD → no branch name
|
|
2164
|
+
commit
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
async function pathStat(path) {
|
|
2168
|
+
try {
|
|
2169
|
+
return await stat11(path);
|
|
2170
|
+
} catch {
|
|
2171
|
+
return null;
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
async function directoryExists(path) {
|
|
2175
|
+
const s = await pathStat(path);
|
|
2176
|
+
return Boolean(s?.isDirectory());
|
|
2177
|
+
}
|
|
2178
|
+
async function nearestExistingDir(path) {
|
|
2179
|
+
if (!path || !isAbsolute(path)) return null;
|
|
2180
|
+
let cur = normalize(path);
|
|
2181
|
+
for (; ; ) {
|
|
2182
|
+
const s = await pathStat(cur);
|
|
2183
|
+
if (s?.isDirectory()) return cur;
|
|
2184
|
+
if (s?.isFile()) return dirname(cur);
|
|
2185
|
+
const parent = dirname(cur);
|
|
2186
|
+
if (parent === cur) return null;
|
|
2187
|
+
cur = parent;
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
async function findGitRoot(path) {
|
|
2191
|
+
let cur = await nearestExistingDir(path);
|
|
2192
|
+
while (cur) {
|
|
2193
|
+
if (await pathStat(join12(cur, ".git"))) return cur;
|
|
2194
|
+
const parent = dirname(cur);
|
|
2195
|
+
if (parent === cur) return null;
|
|
2196
|
+
cur = parent;
|
|
2197
|
+
}
|
|
2198
|
+
return null;
|
|
2199
|
+
}
|
|
2200
|
+
async function repairDashedPath(cwd) {
|
|
2201
|
+
if (!isAbsolute(cwd) || await directoryExists(cwd)) return null;
|
|
2202
|
+
const normalized = normalize(cwd);
|
|
2203
|
+
const parsed = parsePath(normalized);
|
|
2204
|
+
const parts = normalized.slice(parsed.root.length).split("/").filter(Boolean);
|
|
2205
|
+
if (parts.length === 0) return null;
|
|
2206
|
+
let current = parsed.root;
|
|
2207
|
+
let index = 0;
|
|
2208
|
+
while (index < parts.length) {
|
|
2209
|
+
const nextPart = parts[index];
|
|
2210
|
+
if (!nextPart) break;
|
|
2211
|
+
const exact = join12(current, nextPart);
|
|
2212
|
+
if (!await directoryExists(exact)) break;
|
|
2213
|
+
current = exact;
|
|
2214
|
+
index += 1;
|
|
2215
|
+
}
|
|
2216
|
+
while (index < parts.length) {
|
|
2217
|
+
let match = null;
|
|
2218
|
+
let consumed = 0;
|
|
2219
|
+
for (let n = parts.length - index; n >= 1; n -= 1) {
|
|
2220
|
+
const segment = parts.slice(index, index + n).join("-");
|
|
2221
|
+
const candidate = join12(current, segment);
|
|
2222
|
+
if (await directoryExists(candidate)) {
|
|
2223
|
+
match = candidate;
|
|
2224
|
+
consumed = n;
|
|
2225
|
+
break;
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
if (!match) return null;
|
|
2229
|
+
current = match;
|
|
2230
|
+
index += consumed;
|
|
2231
|
+
}
|
|
2232
|
+
return current !== normalized ? current : null;
|
|
2233
|
+
}
|
|
2234
|
+
var ABSOLUTE_PATH_RE = /\/(?:home|tmp|private\/tmp|Users|work|workspace|workspaces|mnt|srv|var|opt|repo|app)\/[^\s"'`<>|;]+/g;
|
|
2235
|
+
var MAX_SPAN_PATH_CANDIDATES = 64;
|
|
2236
|
+
var MAX_SPAN_TEXT_CHARS = 2e5;
|
|
2237
|
+
var SOURCE_WEIGHT = {
|
|
2238
|
+
none: 0,
|
|
2239
|
+
"span-path": 5,
|
|
2240
|
+
"ref-cwd": 50,
|
|
2241
|
+
"repaired-cwd": 90,
|
|
2242
|
+
"span-workdir": 100
|
|
2243
|
+
};
|
|
2244
|
+
function cleanPathCandidate(raw) {
|
|
2245
|
+
let value = raw.trim().replace(/\\n/g, "").replace(/[),\].}]+$/g, "").replace(/:\d+(?::\d+)?$/g, "");
|
|
2246
|
+
value = normalize(value);
|
|
2247
|
+
return isAbsolute(value) ? value : null;
|
|
2248
|
+
}
|
|
2249
|
+
function looksLikeWorkdirKey(key) {
|
|
2250
|
+
return key === "cwd" || key === "workdir" || key === "workingDirectory" || key === "working_directory";
|
|
2251
|
+
}
|
|
2252
|
+
function* pathEvidenceFromText(text, source) {
|
|
2253
|
+
if (source === "span-workdir") {
|
|
2254
|
+
const direct = cleanPathCandidate(text);
|
|
2255
|
+
if (direct) yield { path: direct, source, weight: SOURCE_WEIGHT[source] };
|
|
2256
|
+
}
|
|
2257
|
+
for (const match of text.matchAll(ABSOLUTE_PATH_RE)) {
|
|
2258
|
+
const candidate = cleanPathCandidate(match[0]);
|
|
2259
|
+
if (candidate) yield { path: candidate, source: "span-path", weight: SOURCE_WEIGHT["span-path"] };
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
function* pathsFromUnknown(value, key, depth = 0) {
|
|
2263
|
+
if (depth > 4 || value == null) return;
|
|
2264
|
+
if (typeof value === "string") {
|
|
2265
|
+
yield* pathEvidenceFromText(value, looksLikeWorkdirKey(key) ? "span-workdir" : "span-path");
|
|
2266
|
+
const trimmed = value.trim();
|
|
2267
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
2268
|
+
try {
|
|
2269
|
+
yield* pathsFromUnknown(JSON.parse(trimmed), key, depth + 1);
|
|
2270
|
+
} catch {
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
if (Array.isArray(value)) {
|
|
2276
|
+
for (const item of value) yield* pathsFromUnknown(item, key, depth + 1);
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
if (typeof value === "object") {
|
|
2280
|
+
for (const [childKey, item] of Object.entries(value)) yield* pathsFromUnknown(item, childKey, depth + 1);
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
function extractAbsolutePaths(spans) {
|
|
2284
|
+
const paths = [];
|
|
2285
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2286
|
+
let scannedChars = 0;
|
|
2287
|
+
for (const span2 of spans) {
|
|
2288
|
+
for (const value of [span2.name, span2.status.message, ...Object.values(span2.attributes)]) {
|
|
2289
|
+
for (const evidence of pathsFromUnknown(value)) {
|
|
2290
|
+
scannedChars += evidence.path.length;
|
|
2291
|
+
if (scannedChars > MAX_SPAN_TEXT_CHARS) return paths;
|
|
2292
|
+
const key = `${evidence.source}:${evidence.path}`;
|
|
2293
|
+
if (seen.has(key)) continue;
|
|
2294
|
+
seen.add(key);
|
|
2295
|
+
paths.push(evidence);
|
|
2296
|
+
if (paths.length >= MAX_SPAN_PATH_CANDIDATES) return paths;
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
return paths;
|
|
2301
|
+
}
|
|
2302
|
+
async function resolveRepoAttrs(cwd) {
|
|
2303
|
+
if (!cwd) return {};
|
|
2304
|
+
const attrs = { [ATTR.CWD]: cwd };
|
|
2305
|
+
const basename10 = (() => {
|
|
2306
|
+
const trimmed = cwd.replace(/\/+$/g, "");
|
|
2307
|
+
const idx = trimmed.lastIndexOf("/");
|
|
2308
|
+
const name = idx === -1 ? trimmed : trimmed.slice(idx + 1);
|
|
2309
|
+
return name || trimmed;
|
|
2310
|
+
})();
|
|
2311
|
+
try {
|
|
2312
|
+
const gitRoot = await findGitRoot(cwd);
|
|
2313
|
+
if (!gitRoot) throw new Error("no git root");
|
|
2314
|
+
const { remote, branch, commit } = await readGit(gitRoot);
|
|
2315
|
+
const normalized = remote ? normalizeRemote(remote) : null;
|
|
2316
|
+
if (normalized) {
|
|
2317
|
+
attrs[ATTR.SUBJECT_KEY] = normalized;
|
|
2318
|
+
attrs[ATTR.GIT_REPOSITORY] = normalized;
|
|
2319
|
+
if (branch) attrs[ATTR.GIT_BRANCH_NAME] = branch;
|
|
2320
|
+
if (commit) attrs[ATTR.GIT_COMMIT] = commit;
|
|
2321
|
+
return attrs;
|
|
2322
|
+
}
|
|
2323
|
+
if (branch) attrs[ATTR.GIT_BRANCH_NAME] = branch;
|
|
2324
|
+
if (commit) attrs[ATTR.GIT_COMMIT] = commit;
|
|
2325
|
+
} catch {
|
|
2326
|
+
}
|
|
2327
|
+
attrs[ATTR.SUBJECT_KEY] = basename10;
|
|
2328
|
+
return attrs;
|
|
2329
|
+
}
|
|
2330
|
+
async function repoCandidateForPath(path) {
|
|
2331
|
+
return await findGitRoot(path) ?? await nearestExistingDir(path);
|
|
2332
|
+
}
|
|
2333
|
+
async function resolveSessionRepoAttrs(cwd, spans) {
|
|
2334
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
2335
|
+
const add = (value, source, weight = SOURCE_WEIGHT[source]) => {
|
|
2336
|
+
if (!value) return;
|
|
2337
|
+
const existing = candidates.get(value);
|
|
2338
|
+
if (existing) {
|
|
2339
|
+
existing.score += weight;
|
|
2340
|
+
if (SOURCE_WEIGHT[source] > SOURCE_WEIGHT[existing.source]) existing.source = source;
|
|
2341
|
+
return;
|
|
2342
|
+
}
|
|
2343
|
+
candidates.set(value, { cwd: value, source, score: weight, order: candidates.size });
|
|
2344
|
+
};
|
|
2345
|
+
if (cwd) {
|
|
2346
|
+
add(await repairDashedPath(cwd), "repaired-cwd");
|
|
2347
|
+
add(cwd, "ref-cwd");
|
|
2348
|
+
}
|
|
2349
|
+
for (const evidence of extractAbsolutePaths(spans)) {
|
|
2350
|
+
add(await repoCandidateForPath(evidence.path), evidence.source, evidence.weight);
|
|
2351
|
+
}
|
|
2352
|
+
const resolved = [];
|
|
2353
|
+
for (const candidate of candidates.values()) {
|
|
2354
|
+
const attrs = await resolveRepoAttrs(candidate.cwd);
|
|
2355
|
+
resolved.push({ ...candidate, attrs });
|
|
2356
|
+
}
|
|
2357
|
+
const byScore = (a, b) => b.score - a.score || a.order - b.order;
|
|
2358
|
+
const withGit = resolved.filter((candidate) => candidate.attrs[ATTR.GIT_REPOSITORY]).sort(byScore);
|
|
2359
|
+
const selected = withGit[0] ?? resolved.filter((candidate) => candidate.attrs[ATTR.SUBJECT_KEY]).sort(byScore)[0];
|
|
2360
|
+
if (!selected) return { attrs: {}, cwd: null, source: "none" };
|
|
2361
|
+
return {
|
|
2362
|
+
attrs: { ...selected.attrs, [ATTR.REPO_RESOLUTION_SOURCE]: selected.source },
|
|
2363
|
+
cwd: selected.attrs[ATTR.CWD] ?? selected.cwd,
|
|
2364
|
+
source: selected.source
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
function stampRepoAttrs(spans, attrs) {
|
|
2368
|
+
const keys = Object.keys(attrs);
|
|
2369
|
+
if (keys.length === 0) return spans;
|
|
2370
|
+
for (const s of spans) {
|
|
2371
|
+
for (const k of keys) {
|
|
2372
|
+
if (s.attributes[k] === void 0) s.attributes[k] = attrs[k];
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
return spans;
|
|
2376
|
+
}
|
|
2377
|
+
|
|
1991
2378
|
// src/session-source.ts
|
|
2379
|
+
async function parseSession(adapter, ref) {
|
|
2380
|
+
const spans = await adapter.parse(ref);
|
|
2381
|
+
const repo = await resolveSessionRepoAttrs(ref.cwd, spans);
|
|
2382
|
+
if (repo.cwd) ref.cwd = repo.cwd;
|
|
2383
|
+
stampRepoAttrs(spans, repo.attrs);
|
|
2384
|
+
return spans;
|
|
2385
|
+
}
|
|
1992
2386
|
async function* scanSessions(opts) {
|
|
1993
2387
|
for (const adapter of selectAdapters(opts)) {
|
|
1994
2388
|
if (opts.signal?.aborted) return;
|
|
@@ -2004,7 +2398,7 @@ async function* scanSessions(opts) {
|
|
|
2004
2398
|
if (opts.signal?.aborted) return;
|
|
2005
2399
|
let spans;
|
|
2006
2400
|
try {
|
|
2007
|
-
spans = await adapter
|
|
2401
|
+
spans = await parseSession(adapter, ref);
|
|
2008
2402
|
} catch (err) {
|
|
2009
2403
|
opts.onError?.(err, ref);
|
|
2010
2404
|
continue;
|
|
@@ -2015,106 +2409,235 @@ async function* scanSessions(opts) {
|
|
|
2015
2409
|
}
|
|
2016
2410
|
}
|
|
2017
2411
|
|
|
2018
|
-
// src/
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2412
|
+
// src/evidence.ts
|
|
2413
|
+
import { mkdtemp, writeFile } from "fs/promises";
|
|
2414
|
+
import { tmpdir } from "os";
|
|
2415
|
+
import { join as join13 } from "path";
|
|
2416
|
+
function stringAttr(span2, key) {
|
|
2417
|
+
const value = span2.attributes[key];
|
|
2418
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2419
|
+
}
|
|
2420
|
+
function numberAttr(span2, key) {
|
|
2421
|
+
const value = span2.attributes[key];
|
|
2422
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
2423
|
+
}
|
|
2424
|
+
function spanKind(span2) {
|
|
2425
|
+
return stringAttr(span2, "openinference.span.kind");
|
|
2426
|
+
}
|
|
2427
|
+
function repoFromSpans(spans) {
|
|
2428
|
+
const attrs = {};
|
|
2429
|
+
for (const span2 of spans) {
|
|
2430
|
+
attrs.subjectKey ??= stringAttr(span2, ATTR.SUBJECT_KEY);
|
|
2431
|
+
attrs.repository ??= stringAttr(span2, ATTR.GIT_REPOSITORY);
|
|
2432
|
+
attrs.branch ??= stringAttr(span2, ATTR.GIT_BRANCH_NAME);
|
|
2433
|
+
attrs.commit ??= stringAttr(span2, ATTR.GIT_COMMIT);
|
|
2434
|
+
attrs.cwd ??= stringAttr(span2, ATTR.CWD);
|
|
2435
|
+
attrs.resolutionSource ??= stringAttr(span2, ATTR.REPO_RESOLUTION_SOURCE);
|
|
2436
|
+
if (attrs.subjectKey && attrs.repository && attrs.branch && attrs.commit && attrs.cwd && attrs.resolutionSource) break;
|
|
2437
|
+
}
|
|
2438
|
+
return attrs;
|
|
2439
|
+
}
|
|
2440
|
+
function timeBounds(spans) {
|
|
2441
|
+
const times = spans.flatMap((span2) => [span2.start_time, span2.end_time]).filter((value) => value && value !== "now").sort();
|
|
2442
|
+
return {
|
|
2443
|
+
firstSpanAt: times[0] ?? null,
|
|
2444
|
+
lastSpanAt: times[times.length - 1] ?? null
|
|
2023
2445
|
};
|
|
2024
|
-
const t = setTimeout(() => {
|
|
2025
|
-
signal?.removeEventListener("abort", onAbort);
|
|
2026
|
-
resolve();
|
|
2027
|
-
}, ms);
|
|
2028
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2029
|
-
});
|
|
2030
|
-
function loopKey(sessionId, toolName, argHash) {
|
|
2031
|
-
return `${sessionId}\0${toolName}\0${argHash}`;
|
|
2032
2446
|
}
|
|
2033
|
-
|
|
2034
|
-
const
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2447
|
+
function summarizeTools(spans) {
|
|
2448
|
+
const byTool = /* @__PURE__ */ new Map();
|
|
2449
|
+
for (const span2 of spans) {
|
|
2450
|
+
if (spanKind(span2) !== "TOOL") continue;
|
|
2451
|
+
const name = stringAttr(span2, "tool.name") ?? span2.name.replace(/^tool\./, "");
|
|
2452
|
+
const current = byTool.get(name) ?? { calls: 0, errors: 0 };
|
|
2453
|
+
current.calls += 1;
|
|
2454
|
+
if (span2.status.code === "ERROR") current.errors += 1;
|
|
2455
|
+
byTool.set(name, current);
|
|
2456
|
+
}
|
|
2457
|
+
return [...byTool.entries()].map(([name, row]) => ({ name, calls: row.calls, errors: row.errors })).sort((a, b) => b.calls - a.calls || a.name.localeCompare(b.name));
|
|
2458
|
+
}
|
|
2459
|
+
async function buildPolicyEvidenceRecord(ref, spans, opts = {}) {
|
|
2460
|
+
const llmSpans = spans.filter((span2) => spanKind(span2) === "LLM");
|
|
2461
|
+
const toolSpans = spans.filter((span2) => spanKind(span2) === "TOOL");
|
|
2462
|
+
const erroredToolCallCount = toolSpans.filter((span2) => span2.status.code === "ERROR").length;
|
|
2463
|
+
const pipelines = await runPipelines(spans, { minLoopOccurrences: opts.minLoopOccurrences });
|
|
2464
|
+
const loopLimit = opts.maxLoopExamples ?? 25;
|
|
2465
|
+
const loopFindings = pipelines.stuckLoops.findings;
|
|
2466
|
+
const { firstSpanAt, lastSpanAt } = timeBounds(spans);
|
|
2467
|
+
const repo = repoFromSpans(spans);
|
|
2468
|
+
return {
|
|
2469
|
+
schemaVersion: 1,
|
|
2470
|
+
kind: "traces.policy_evidence.session",
|
|
2471
|
+
generatedAt: opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2472
|
+
session: {
|
|
2473
|
+
harness: ref.harness,
|
|
2474
|
+
sessionId: ref.sessionId,
|
|
2475
|
+
path: ref.path,
|
|
2476
|
+
cwd: repo.cwd ?? ref.cwd,
|
|
2477
|
+
mtimeMs: ref.mtimeMs
|
|
2478
|
+
},
|
|
2479
|
+
repo,
|
|
2480
|
+
metrics: {
|
|
2481
|
+
spanCount: spans.length,
|
|
2482
|
+
llmTurnCount: llmSpans.length,
|
|
2483
|
+
toolCallCount: toolSpans.length,
|
|
2484
|
+
erroredToolCallCount,
|
|
2485
|
+
inputTokens: llmSpans.reduce((sum, span2) => sum + numberAttr(span2, "llm.input_tokens"), 0),
|
|
2486
|
+
outputTokens: llmSpans.reduce((sum, span2) => sum + numberAttr(span2, "llm.output_tokens"), 0),
|
|
2487
|
+
models: [...new Set(llmSpans.map((span2) => stringAttr(span2, "llm.model_name")).filter((value) => Boolean(value)))].sort(),
|
|
2488
|
+
tools: summarizeTools(spans),
|
|
2489
|
+
firstSpanAt,
|
|
2490
|
+
lastSpanAt
|
|
2491
|
+
},
|
|
2492
|
+
signals: {
|
|
2493
|
+
stuckLoopCount: loopFindings.length,
|
|
2494
|
+
affectedRunRatio: pipelines.stuckLoops.affectedRunRatio,
|
|
2495
|
+
stuckLoops: loopFindings.slice(0, loopLimit).map((finding) => ({
|
|
2496
|
+
toolName: finding.toolName,
|
|
2497
|
+
occurrences: finding.occurrences
|
|
2498
|
+
})),
|
|
2499
|
+
stuckLoopsOmitted: Math.max(0, loopFindings.length - loopLimit),
|
|
2500
|
+
toolErrorRate: toolSpans.length === 0 ? 0 : erroredToolCallCount / toolSpans.length
|
|
2501
|
+
},
|
|
2502
|
+
provenance: {
|
|
2503
|
+
source: "traces",
|
|
2504
|
+
evidenceKind: "session-summary",
|
|
2505
|
+
...opts.otlpPath ? { otlpPath: opts.otlpPath } : {},
|
|
2506
|
+
notCampaignCell: true,
|
|
2507
|
+
note: "This is normalized coding-agent session evidence for downstream policy mining; it is not an eval campaign cell."
|
|
2508
|
+
}
|
|
2509
|
+
};
|
|
2510
|
+
}
|
|
2511
|
+
async function collectPolicyEvidence(opts) {
|
|
2512
|
+
const records = [];
|
|
2513
|
+
for await (const session of scanSessions(opts)) {
|
|
2514
|
+
records.push(await buildPolicyEvidenceRecord(session.ref, session.spans, opts));
|
|
2515
|
+
}
|
|
2516
|
+
return records;
|
|
2517
|
+
}
|
|
2518
|
+
function serializePolicyEvidence(records) {
|
|
2519
|
+
if (records.length === 0) return "";
|
|
2520
|
+
return `${records.map((record) => JSON.stringify(record)).join("\n")}
|
|
2521
|
+
`;
|
|
2522
|
+
}
|
|
2523
|
+
async function writePolicyEvidenceFile(records, outPath) {
|
|
2524
|
+
const path = outPath ?? join13(await mkdtemp(join13(tmpdir(), "traces-evidence-")), "policy-evidence.jsonl");
|
|
2525
|
+
await writeFile(path, serializePolicyEvidence(records), "utf8");
|
|
2526
|
+
return path;
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
// src/external.ts
|
|
2530
|
+
import { spawn } from "child_process";
|
|
2531
|
+
function runCommand(command, args = [], opts = {}) {
|
|
2532
|
+
const timeoutMs = opts.timeoutMs ?? 12e4;
|
|
2533
|
+
const maxBuffer = opts.maxBuffer ?? 32 * 1024 * 1024;
|
|
2534
|
+
return new Promise((resolve, reject) => {
|
|
2535
|
+
let child;
|
|
2536
|
+
try {
|
|
2537
|
+
child = spawn(command, [...args], { signal: opts.signal });
|
|
2538
|
+
} catch (err2) {
|
|
2539
|
+
reject(err2 instanceof Error ? err2 : new Error(String(err2)));
|
|
2540
|
+
return;
|
|
2541
|
+
}
|
|
2542
|
+
let out = "";
|
|
2543
|
+
let err = "";
|
|
2544
|
+
let killed = false;
|
|
2545
|
+
const timer = setTimeout(() => {
|
|
2546
|
+
killed = true;
|
|
2547
|
+
child.kill("SIGKILL");
|
|
2548
|
+
reject(new Error(`${command}: timed out after ${timeoutMs}ms`));
|
|
2549
|
+
}, timeoutMs);
|
|
2550
|
+
const cap = (add, buf) => {
|
|
2551
|
+
const next = buf + add;
|
|
2552
|
+
if (next.length > maxBuffer) {
|
|
2553
|
+
killed = true;
|
|
2554
|
+
child.kill("SIGKILL");
|
|
2555
|
+
clearTimeout(timer);
|
|
2556
|
+
reject(new Error(`${command}: output exceeded ${maxBuffer} bytes`));
|
|
2063
2557
|
}
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2558
|
+
return next;
|
|
2559
|
+
};
|
|
2560
|
+
child.stdout?.on("data", (d) => {
|
|
2561
|
+
out = cap(String(d), out);
|
|
2562
|
+
});
|
|
2563
|
+
child.stderr?.on("data", (d) => {
|
|
2564
|
+
err = cap(String(d), err);
|
|
2565
|
+
});
|
|
2566
|
+
child.on("error", (e) => {
|
|
2567
|
+
clearTimeout(timer);
|
|
2568
|
+
if (!killed) reject(e);
|
|
2569
|
+
});
|
|
2570
|
+
child.on("close", (code) => {
|
|
2571
|
+
clearTimeout(timer);
|
|
2572
|
+
if (!killed) resolve({ stdout: out, stderr: err, code });
|
|
2573
|
+
});
|
|
2574
|
+
if (opts.input !== void 0) {
|
|
2575
|
+
child.stdin?.on("error", () => {
|
|
2576
|
+
});
|
|
2577
|
+
child.stdin?.end(opts.input);
|
|
2578
|
+
}
|
|
2579
|
+
});
|
|
2580
|
+
}
|
|
2581
|
+
function commandAnalyzer(spec) {
|
|
2582
|
+
return {
|
|
2583
|
+
name: spec.name,
|
|
2584
|
+
async analyze(otlpPath, opts = {}) {
|
|
2585
|
+
try {
|
|
2586
|
+
const res = await runCommand(spec.command, spec.args(otlpPath, opts.prompt), {
|
|
2587
|
+
signal: opts.signal,
|
|
2588
|
+
timeoutMs: spec.timeoutMs
|
|
2589
|
+
});
|
|
2590
|
+
if (res.code !== 0) {
|
|
2591
|
+
return { analyzer: spec.name, ok: false, output: res.stdout, error: res.stderr.trim() || `exit ${res.code}` };
|
|
2083
2592
|
}
|
|
2593
|
+
return { analyzer: spec.name, ok: true, output: res.stdout.trim(), findings: spec.parse?.(res.stdout) };
|
|
2594
|
+
} catch (e) {
|
|
2595
|
+
return { analyzer: spec.name, ok: false, output: "", error: e instanceof Error ? e.message : String(e) };
|
|
2084
2596
|
}
|
|
2085
2597
|
}
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2598
|
+
};
|
|
2599
|
+
}
|
|
2600
|
+
function haloAnalyzer(opts = {}) {
|
|
2601
|
+
const defaultPrompt = opts.defaultPrompt ?? "diagnose";
|
|
2602
|
+
return commandAnalyzer({
|
|
2603
|
+
name: "halo",
|
|
2604
|
+
command: opts.command ?? "halo",
|
|
2605
|
+
args: (otlpPath, prompt) => {
|
|
2606
|
+
const args = [otlpPath, "-p", prompt ?? defaultPrompt];
|
|
2607
|
+
if (opts.model) args.push("-m", opts.model);
|
|
2608
|
+
return args;
|
|
2609
|
+
},
|
|
2610
|
+
timeoutMs: opts.timeoutMs
|
|
2611
|
+
});
|
|
2612
|
+
}
|
|
2613
|
+
function runExternalAnalyzers(otlpPath, analyzers, opts = {}) {
|
|
2614
|
+
return Promise.all(analyzers.map((a) => a.analyze(otlpPath, opts)));
|
|
2615
|
+
}
|
|
2616
|
+
function commandRedactor(spec) {
|
|
2617
|
+
const name = spec.name ?? spec.command;
|
|
2618
|
+
return {
|
|
2619
|
+
name,
|
|
2620
|
+
async redactText(texts) {
|
|
2621
|
+
if (texts.length === 0) return [];
|
|
2622
|
+
const res = await runCommand(spec.command, spec.args ?? [], {
|
|
2623
|
+
input: JSON.stringify(texts),
|
|
2624
|
+
timeoutMs: spec.timeoutMs
|
|
2625
|
+
});
|
|
2626
|
+
if (res.code !== 0) throw new Error(`redactor ${name}: exit ${res.code}: ${res.stderr.trim()}`);
|
|
2627
|
+
let parsed;
|
|
2628
|
+
try {
|
|
2629
|
+
parsed = JSON.parse(res.stdout);
|
|
2630
|
+
} catch {
|
|
2631
|
+
throw new Error(`redactor ${name}: stdout was not JSON`);
|
|
2632
|
+
}
|
|
2633
|
+
if (!Array.isArray(parsed) || parsed.length !== texts.length) {
|
|
2634
|
+
throw new Error(`redactor ${name}: expected a JSON array of ${texts.length} strings`);
|
|
2635
|
+
}
|
|
2636
|
+
return parsed.map(String);
|
|
2090
2637
|
}
|
|
2091
|
-
|
|
2092
|
-
await sleep(intervalMs, opts.signal);
|
|
2093
|
-
}
|
|
2638
|
+
};
|
|
2094
2639
|
}
|
|
2095
2640
|
|
|
2096
|
-
// src/attributes.ts
|
|
2097
|
-
var ATTR = {
|
|
2098
|
-
/** = the trace/session id. Wire contract (dedup linkage). */
|
|
2099
|
-
SESSION_ID: "tangle.sessionId",
|
|
2100
|
-
/** "cli" marks a CLI upload so the platform can drop a re-upload of a session
|
|
2101
|
-
* that also streamed live. Wire contract. */
|
|
2102
|
-
INGEST_SOURCE: "tangle.ingest_source",
|
|
2103
|
-
HARNESS: "tangle.harness",
|
|
2104
|
-
CWD: "tangle.cwd",
|
|
2105
|
-
GIT_BRANCH: "tangle.git_branch",
|
|
2106
|
-
HOST: "tangle.host",
|
|
2107
|
-
/** Basename of the session file. (Renamed from the ambiguous `tangle.source`,
|
|
2108
|
-
* which collided with `tangle.ingest_source`.) */
|
|
2109
|
-
SESSION_FILE: "tangle.session_file",
|
|
2110
|
-
UPLOADED_AT: "tangle.uploaded_at",
|
|
2111
|
-
UPLOADER: "tangle.uploader",
|
|
2112
|
-
REDACTION_VERSION: "redaction.version",
|
|
2113
|
-
REDACTION_COUNT: "redaction.count"
|
|
2114
|
-
};
|
|
2115
|
-
var INGEST_SOURCE_CLI = "cli";
|
|
2116
|
-
var DEFAULT_HARNESS = "claude-code";
|
|
2117
|
-
|
|
2118
2641
|
// src/redact.ts
|
|
2119
2642
|
import { DEFAULT_REDACTION_RULES, redactValue } from "@tangle-network/agent-eval/traces";
|
|
2120
2643
|
var CODING_REDACTION_RULES = [
|
|
@@ -2175,20 +2698,1084 @@ async function applyRedactor(spans, redactor) {
|
|
|
2175
2698
|
return { spans: out, changed };
|
|
2176
2699
|
}
|
|
2177
2700
|
|
|
2178
|
-
// src/
|
|
2701
|
+
// src/file-export.ts
|
|
2179
2702
|
import { createHash } from "crypto";
|
|
2180
|
-
import {
|
|
2703
|
+
import { mkdtemp as mkdtemp2, readFile as readFile12, writeFile as writeFile2 } from "fs/promises";
|
|
2704
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
2705
|
+
import { join as join14 } from "path";
|
|
2706
|
+
function isObject(value) {
|
|
2707
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
2708
|
+
}
|
|
2709
|
+
function stringValue(value) {
|
|
2710
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2711
|
+
}
|
|
2712
|
+
function objectValue(value) {
|
|
2713
|
+
return isObject(value) ? value : void 0;
|
|
2714
|
+
}
|
|
2715
|
+
function stableJson(value) {
|
|
2716
|
+
return JSON.stringify(value, (_key, v) => {
|
|
2717
|
+
if (!isObject(v)) return v;
|
|
2718
|
+
return Object.keys(v).sort().reduce((acc, key) => {
|
|
2719
|
+
acc[key] = v[key];
|
|
2720
|
+
return acc;
|
|
2721
|
+
}, {});
|
|
2722
|
+
});
|
|
2723
|
+
}
|
|
2724
|
+
function hashId(value, chars = 16) {
|
|
2725
|
+
return createHash("sha256").update(stableJson(value)).digest("hex").slice(0, chars);
|
|
2726
|
+
}
|
|
2727
|
+
function isoTime(value) {
|
|
2728
|
+
if (typeof value === "string") {
|
|
2729
|
+
const ms = Date.parse(value);
|
|
2730
|
+
return Number.isFinite(ms) ? new Date(ms).toISOString() : void 0;
|
|
2731
|
+
}
|
|
2732
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2733
|
+
const ms = value < 1e10 ? value * 1e3 : value;
|
|
2734
|
+
return new Date(ms).toISOString();
|
|
2735
|
+
}
|
|
2736
|
+
return void 0;
|
|
2737
|
+
}
|
|
2738
|
+
function firstTime(obj, keys) {
|
|
2739
|
+
for (const key of keys) {
|
|
2740
|
+
const value = isoTime(obj[key]);
|
|
2741
|
+
if (value) return value;
|
|
2742
|
+
}
|
|
2743
|
+
return void 0;
|
|
2744
|
+
}
|
|
2745
|
+
function firstTimeDeep(value, keys, depth = 5) {
|
|
2746
|
+
if (depth < 0) return void 0;
|
|
2747
|
+
if (isObject(value)) {
|
|
2748
|
+
const direct = firstTime(value, keys);
|
|
2749
|
+
if (direct) return direct;
|
|
2750
|
+
for (const child of Object.values(value)) {
|
|
2751
|
+
const found = firstTimeDeep(child, keys, depth - 1);
|
|
2752
|
+
if (found) return found;
|
|
2753
|
+
}
|
|
2754
|
+
} else if (Array.isArray(value)) {
|
|
2755
|
+
for (const child of value) {
|
|
2756
|
+
const found = firstTimeDeep(child, keys, depth - 1);
|
|
2757
|
+
if (found) return found;
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
return void 0;
|
|
2761
|
+
}
|
|
2762
|
+
function copyDefined(target, entries) {
|
|
2763
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
2764
|
+
if (value != null && value !== "") target[key] = value;
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
function parseJsonRows(text) {
|
|
2768
|
+
const trimmed = text.trim();
|
|
2769
|
+
if (!trimmed) throw new Error("input file is empty");
|
|
2770
|
+
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
|
2771
|
+
const parsed = JSON.parse(trimmed);
|
|
2772
|
+
if (Array.isArray(parsed)) return { rows: parsed };
|
|
2773
|
+
if (isObject(parsed)) {
|
|
2774
|
+
const events = parsed.events;
|
|
2775
|
+
if (Array.isArray(events)) return { rows: events, wrapper: parsed };
|
|
2776
|
+
return { rows: [parsed] };
|
|
2777
|
+
}
|
|
2778
|
+
throw new Error("input JSON must be an object, array, or JSONL rows");
|
|
2779
|
+
}
|
|
2780
|
+
return {
|
|
2781
|
+
rows: trimmed.split(/\r?\n/).filter((line) => line.trim().length > 0).map((line) => JSON.parse(line))
|
|
2782
|
+
};
|
|
2783
|
+
}
|
|
2784
|
+
function isPolicyEvidenceRow(row) {
|
|
2785
|
+
return isObject(row) && row.kind === "traces.policy_evidence.session";
|
|
2786
|
+
}
|
|
2787
|
+
function isOpenInferenceRow(row) {
|
|
2788
|
+
if (!isObject(row)) return false;
|
|
2789
|
+
return typeof row.trace_id === "string" && typeof row.span_id === "string" && typeof row.name === "string" && typeof row.start_time === "string" && typeof row.end_time === "string" && isObject(row.status) && isObject(row.attributes) && isObject(row.resource) && isObject(row.scope);
|
|
2790
|
+
}
|
|
2791
|
+
function eventType(row) {
|
|
2792
|
+
return stringValue(row.type) ?? stringValue(row.event) ?? stringValue(row.name) ?? stringValue(row.kind);
|
|
2793
|
+
}
|
|
2794
|
+
function looksLikeSandboxEvent(row) {
|
|
2795
|
+
if (!isObject(row)) return false;
|
|
2796
|
+
const type = eventType(row)?.toLowerCase();
|
|
2797
|
+
return type === "start" || type === "raw" || type === "result" || type === "done" || type === "error";
|
|
2798
|
+
}
|
|
2799
|
+
function detectFormat(rows, requested) {
|
|
2800
|
+
if (requested !== "auto") return requested;
|
|
2801
|
+
if (rows.length === 0) throw new Error("input contains no rows");
|
|
2802
|
+
if (rows.every(isPolicyEvidenceRow)) return "policy-evidence";
|
|
2803
|
+
if (rows.every(isOpenInferenceRow)) return "openinference";
|
|
2804
|
+
if (rows.some(looksLikeSandboxEvent)) return "sandbox-events";
|
|
2805
|
+
throw new Error("could not detect input format; use --format policy-evidence, sandbox-events, or openinference");
|
|
2806
|
+
}
|
|
2807
|
+
function requirePolicyEvidenceRows(rows) {
|
|
2808
|
+
if (!rows.every(isPolicyEvidenceRow)) throw new Error("policy-evidence input must contain only traces.policy_evidence.session rows");
|
|
2809
|
+
return rows;
|
|
2810
|
+
}
|
|
2811
|
+
function requireOpenInferenceRows(rows) {
|
|
2812
|
+
if (!rows.every(isOpenInferenceRow)) throw new Error("openinference input must contain only complete OpenInference span rows");
|
|
2813
|
+
return rows;
|
|
2814
|
+
}
|
|
2815
|
+
function requireObjectRows(rows) {
|
|
2816
|
+
if (!rows.every(isObject)) throw new Error("sandbox-events input must contain only JSON object events");
|
|
2817
|
+
return rows;
|
|
2818
|
+
}
|
|
2819
|
+
function evidenceTimeBounds(record) {
|
|
2820
|
+
const sessionTime = new Date(record.session.mtimeMs).toISOString();
|
|
2821
|
+
const start = record.metrics.firstSpanAt ?? record.generatedAt ?? sessionTime;
|
|
2822
|
+
const end = record.metrics.lastSpanAt ?? start;
|
|
2823
|
+
return { start, end };
|
|
2824
|
+
}
|
|
2825
|
+
function policyEvidenceToSpans(records) {
|
|
2826
|
+
return records.map((record) => {
|
|
2827
|
+
const { start, end } = evidenceTimeBounds(record);
|
|
2828
|
+
const traceId = record.session.sessionId || hashId(record, 32);
|
|
2829
|
+
const extra = {
|
|
2830
|
+
"traces.source_format": "policy-evidence",
|
|
2831
|
+
"traces.policy_evidence.schema_version": record.schemaVersion,
|
|
2832
|
+
"traces.policy_evidence.kind": record.kind,
|
|
2833
|
+
"traces.session.id": record.session.sessionId,
|
|
2834
|
+
"traces.session.path": record.session.path,
|
|
2835
|
+
"traces.session.cwd": record.session.cwd,
|
|
2836
|
+
"traces.session.mtime_ms": record.session.mtimeMs,
|
|
2837
|
+
"traces.metrics.span_count": record.metrics.spanCount,
|
|
2838
|
+
"traces.metrics.llm_turn_count": record.metrics.llmTurnCount,
|
|
2839
|
+
"traces.metrics.tool_call_count": record.metrics.toolCallCount,
|
|
2840
|
+
"traces.metrics.errored_tool_call_count": record.metrics.erroredToolCallCount,
|
|
2841
|
+
"traces.metrics.input_tokens": record.metrics.inputTokens,
|
|
2842
|
+
"traces.metrics.output_tokens": record.metrics.outputTokens,
|
|
2843
|
+
"traces.metrics.models": record.metrics.models,
|
|
2844
|
+
"traces.metrics.tools": stableJson(record.metrics.tools),
|
|
2845
|
+
"traces.signals.stuck_loop_count": record.signals.stuckLoopCount,
|
|
2846
|
+
"traces.signals.affected_run_ratio": record.signals.affectedRunRatio,
|
|
2847
|
+
"traces.signals.stuck_loops": stableJson(record.signals.stuckLoops),
|
|
2848
|
+
"traces.signals.stuck_loops_omitted": record.signals.stuckLoopsOmitted,
|
|
2849
|
+
"traces.signals.tool_error_rate": record.signals.toolErrorRate,
|
|
2850
|
+
"traces.provenance.source": record.provenance.source,
|
|
2851
|
+
"traces.provenance.evidence_kind": record.provenance.evidenceKind,
|
|
2852
|
+
"traces.provenance.otlp_path": record.provenance.otlpPath,
|
|
2853
|
+
[ATTR.SUBJECT_KEY]: record.repo.subjectKey,
|
|
2854
|
+
[ATTR.GIT_REPOSITORY]: record.repo.repository,
|
|
2855
|
+
[ATTR.GIT_BRANCH_NAME]: record.repo.branch,
|
|
2856
|
+
[ATTR.GIT_COMMIT]: record.repo.commit,
|
|
2857
|
+
[ATTR.CWD]: record.repo.cwd ?? record.session.cwd,
|
|
2858
|
+
[ATTR.REPO_RESOLUTION_SOURCE]: record.repo.resolutionSource
|
|
2859
|
+
};
|
|
2860
|
+
return span({
|
|
2861
|
+
traceId,
|
|
2862
|
+
spanId: `policy:${hashId(record)}`,
|
|
2863
|
+
parentSpanId: null,
|
|
2864
|
+
name: "policy_evidence.session",
|
|
2865
|
+
kind: "AGENT",
|
|
2866
|
+
startTime: start,
|
|
2867
|
+
endTime: end,
|
|
2868
|
+
service: record.session.harness,
|
|
2869
|
+
agent: "traces",
|
|
2870
|
+
content: capText(stableJson({
|
|
2871
|
+
session: record.session,
|
|
2872
|
+
repo: record.repo,
|
|
2873
|
+
metrics: record.metrics,
|
|
2874
|
+
signals: record.signals,
|
|
2875
|
+
provenance: record.provenance
|
|
2876
|
+
})),
|
|
2877
|
+
extra
|
|
2878
|
+
});
|
|
2879
|
+
});
|
|
2880
|
+
}
|
|
2881
|
+
function findStringKey(value, keys, depth = 4) {
|
|
2882
|
+
if (depth < 0) return void 0;
|
|
2883
|
+
if (isObject(value)) {
|
|
2884
|
+
for (const key of keys) {
|
|
2885
|
+
const found = stringValue(value[key]);
|
|
2886
|
+
if (found) return found;
|
|
2887
|
+
}
|
|
2888
|
+
for (const child of Object.values(value)) {
|
|
2889
|
+
const found = findStringKey(child, keys, depth - 1);
|
|
2890
|
+
if (found) return found;
|
|
2891
|
+
}
|
|
2892
|
+
} else if (Array.isArray(value)) {
|
|
2893
|
+
for (const child of value) {
|
|
2894
|
+
const found = findStringKey(child, keys, depth - 1);
|
|
2895
|
+
if (found) return found;
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
return void 0;
|
|
2899
|
+
}
|
|
2900
|
+
function eventTimestamp(row) {
|
|
2901
|
+
return firstTimeDeep(row, [
|
|
2902
|
+
"timestamp",
|
|
2903
|
+
"start_time",
|
|
2904
|
+
"startTime",
|
|
2905
|
+
"startedAt",
|
|
2906
|
+
"createdAt",
|
|
2907
|
+
"created_at",
|
|
2908
|
+
"created",
|
|
2909
|
+
"time"
|
|
2910
|
+
]);
|
|
2911
|
+
}
|
|
2912
|
+
function eventEndTimestamp(row, fallback) {
|
|
2913
|
+
return firstTimeDeep(row, [
|
|
2914
|
+
"end_time",
|
|
2915
|
+
"endTime",
|
|
2916
|
+
"completedAt",
|
|
2917
|
+
"completed_at",
|
|
2918
|
+
"finishedAt",
|
|
2919
|
+
"updatedAt",
|
|
2920
|
+
"updated_at",
|
|
2921
|
+
"updated"
|
|
2922
|
+
]) ?? fallback;
|
|
2923
|
+
}
|
|
2924
|
+
function eventStatus(row, type) {
|
|
2925
|
+
const lowered = type.toLowerCase();
|
|
2926
|
+
const errorMessage = stringValue(row.error) ?? findStringKey(row, ["error", "errorMessage", "error_message"], 3);
|
|
2927
|
+
if (lowered.includes("error") || errorMessage) {
|
|
2928
|
+
const message = errorMessage ?? stringValue(row.message);
|
|
2929
|
+
return { status: "ERROR", ...message ? { message: capText(message) } : {} };
|
|
2930
|
+
}
|
|
2931
|
+
return { status: "OK" };
|
|
2932
|
+
}
|
|
2933
|
+
function eventKind(row, type) {
|
|
2934
|
+
const lowered = type.toLowerCase();
|
|
2935
|
+
const tool = findStringKey(row, ["tool", "toolName", "tool_name"], 4);
|
|
2936
|
+
if (tool || lowered.includes("tool") || stableJson(row).includes("tool-invocation")) return { kind: "TOOL", tool };
|
|
2937
|
+
if (lowered.includes("llm") || lowered === "token") return { kind: "LLM" };
|
|
2938
|
+
return { kind: "CHAIN" };
|
|
2939
|
+
}
|
|
2940
|
+
function sandboxEventsToSpans(rows, wrapper) {
|
|
2941
|
+
const context = wrapper ?? {};
|
|
2942
|
+
const traceId = findStringKey(context, ["trace_id", "traceId", "session_id", "sessionId", "run_id", "runId"], 3) ?? findStringKey(rows, ["trace_id", "traceId", "session_id", "sessionId", "run_id", "runId"], 4) ?? `sandbox:${hashId(rows, 32)}`;
|
|
2943
|
+
const service = findStringKey(context, ["service", "harness", "source"], 2) ?? "sandbox-opencode";
|
|
2944
|
+
const times = rows.flatMap((row) => {
|
|
2945
|
+
const time = eventTimestamp(row);
|
|
2946
|
+
return time ? [time] : [];
|
|
2947
|
+
}).sort();
|
|
2948
|
+
const rootId = `events:${hashId({ traceId, rows: rows.length })}`;
|
|
2949
|
+
const hasError = rows.some((row) => eventStatus(row, eventType(row) ?? "event").status === "ERROR");
|
|
2950
|
+
const rootStart = times[0] ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
2951
|
+
const rootEnd = times[times.length - 1] ?? rootStart;
|
|
2952
|
+
const spans = [
|
|
2953
|
+
span({
|
|
2954
|
+
traceId,
|
|
2955
|
+
spanId: rootId,
|
|
2956
|
+
parentSpanId: null,
|
|
2957
|
+
name: "sandbox.events",
|
|
2958
|
+
kind: "AGENT",
|
|
2959
|
+
startTime: rootStart,
|
|
2960
|
+
endTime: rootEnd,
|
|
2961
|
+
status: hasError ? "ERROR" : "OK",
|
|
2962
|
+
service,
|
|
2963
|
+
agent: service,
|
|
2964
|
+
extra: {
|
|
2965
|
+
"traces.source_format": "sandbox-events",
|
|
2966
|
+
"traces.event_count": rows.length
|
|
2967
|
+
}
|
|
2968
|
+
})
|
|
2969
|
+
];
|
|
2970
|
+
let previousTime = rootStart;
|
|
2971
|
+
rows.forEach((row, index) => {
|
|
2972
|
+
const type = eventType(row) ?? "event";
|
|
2973
|
+
const time = eventTimestamp(row) ?? previousTime;
|
|
2974
|
+
previousTime = time;
|
|
2975
|
+
const { status, message } = eventStatus(row, type);
|
|
2976
|
+
const { kind, tool } = eventKind(row, type);
|
|
2977
|
+
const name = kind === "TOOL" && tool ? `tool.${tool}` : `event.${type}`;
|
|
2978
|
+
const extra = {
|
|
2979
|
+
"traces.source_format": "sandbox-events",
|
|
2980
|
+
"traces.event.type": type,
|
|
2981
|
+
"traces.event.index": index
|
|
2982
|
+
};
|
|
2983
|
+
copyDefined(extra, {
|
|
2984
|
+
"traces.event.id": stringValue(row.id),
|
|
2985
|
+
"traces.event.raw_type": stringValue(row.type),
|
|
2986
|
+
"traces.event.raw_event": stringValue(row.event)
|
|
2987
|
+
});
|
|
2988
|
+
spans.push(span({
|
|
2989
|
+
traceId,
|
|
2990
|
+
spanId: `event:${index}:${hashId(row)}`,
|
|
2991
|
+
parentSpanId: rootId,
|
|
2992
|
+
name,
|
|
2993
|
+
kind,
|
|
2994
|
+
startTime: time,
|
|
2995
|
+
endTime: eventEndTimestamp(row, time),
|
|
2996
|
+
status,
|
|
2997
|
+
statusMessage: message,
|
|
2998
|
+
service,
|
|
2999
|
+
agent: service,
|
|
3000
|
+
tool: kind === "TOOL" ? tool ?? type : null,
|
|
3001
|
+
step: index,
|
|
3002
|
+
content: capText(stableJson(row)),
|
|
3003
|
+
extra
|
|
3004
|
+
}));
|
|
3005
|
+
});
|
|
3006
|
+
return spans;
|
|
3007
|
+
}
|
|
3008
|
+
function statusCode(value) {
|
|
3009
|
+
return value === "OK" || value === "ERROR" || value === "UNSET" ? value : "UNSET";
|
|
3010
|
+
}
|
|
3011
|
+
function openInferenceToSpans(rows) {
|
|
3012
|
+
return rows.map((row) => {
|
|
3013
|
+
const attrs = isObject(row.attributes) ? { ...row.attributes } : {};
|
|
3014
|
+
const resource = objectValue(row.resource);
|
|
3015
|
+
const resourceAttrs = objectValue(resource?.attributes);
|
|
3016
|
+
if (resourceAttrs) copyDefined(attrs, resourceAttrs);
|
|
3017
|
+
const kind = stringValue(row.kind);
|
|
3018
|
+
if (kind && attrs["openinference.span.kind"] == null) attrs["openinference.span.kind"] = kind;
|
|
3019
|
+
const status = objectValue(row.status);
|
|
3020
|
+
const message = stringValue(status?.message);
|
|
3021
|
+
return {
|
|
3022
|
+
trace_id: stringValue(row.trace_id) ?? hashId(row, 32),
|
|
3023
|
+
span_id: stringValue(row.span_id) ?? hashId({ row, field: "span" }),
|
|
3024
|
+
parent_span_id: stringValue(row.parent_span_id) || null,
|
|
3025
|
+
name: stringValue(row.name) ?? "span",
|
|
3026
|
+
start_time: stringValue(row.start_time) ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
3027
|
+
end_time: stringValue(row.end_time) ?? stringValue(row.start_time) ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
3028
|
+
status: { code: statusCode(status?.code), ...message ? { message } : {} },
|
|
3029
|
+
attributes: attrs
|
|
3030
|
+
};
|
|
3031
|
+
});
|
|
3032
|
+
}
|
|
3033
|
+
function normalizeAttributes(attributes) {
|
|
3034
|
+
const normalized = {};
|
|
3035
|
+
for (const [key, value] of Object.entries(attributes ?? {})) {
|
|
3036
|
+
if (!key || value == null) continue;
|
|
3037
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
3038
|
+
normalized[key] = value;
|
|
3039
|
+
} else if (Array.isArray(value) && value.every(
|
|
3040
|
+
(item) => typeof item === "string" || typeof item === "number" || typeof item === "boolean"
|
|
3041
|
+
)) {
|
|
3042
|
+
normalized[key] = value;
|
|
3043
|
+
} else {
|
|
3044
|
+
normalized[key] = stableJson(value);
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
return normalized;
|
|
3048
|
+
}
|
|
3049
|
+
function withExportAttributes(spans, attributes) {
|
|
3050
|
+
const normalized = normalizeAttributes(attributes);
|
|
3051
|
+
if (Object.keys(normalized).length === 0) return [...spans];
|
|
3052
|
+
return spans.map((s) => ({ ...s, attributes: { ...s.attributes, ...normalized } }));
|
|
3053
|
+
}
|
|
3054
|
+
function exportTraceEvidenceRows(rows, opts = {}, wrapper) {
|
|
3055
|
+
const format = detectFormat(rows, opts.format ?? "auto");
|
|
3056
|
+
const converted = format === "policy-evidence" ? policyEvidenceToSpans(requirePolicyEvidenceRows(rows)) : format === "openinference" ? openInferenceToSpans(requireOpenInferenceRows(rows)) : sandboxEventsToSpans(requireObjectRows(rows), wrapper);
|
|
3057
|
+
const spans = withExportAttributes(converted, opts.attributes);
|
|
3058
|
+
if (spans.length === 0) throw new Error(`no spans exported from ${format} input`);
|
|
3059
|
+
const redacted = redactSpans(spans);
|
|
3060
|
+
return {
|
|
3061
|
+
format,
|
|
3062
|
+
spans: redacted.spans,
|
|
3063
|
+
redactionCount: redacted.report.redactionCount,
|
|
3064
|
+
redactionsByRule: redacted.report.byRule
|
|
3065
|
+
};
|
|
3066
|
+
}
|
|
3067
|
+
function exportTraceEvidenceText(text, opts = {}) {
|
|
3068
|
+
const parsed = parseJsonRows(text);
|
|
3069
|
+
return exportTraceEvidenceRows(parsed.rows, opts, parsed.wrapper);
|
|
3070
|
+
}
|
|
3071
|
+
async function exportTraceEvidenceFile(inputPath, opts = {}) {
|
|
3072
|
+
const text = await readFile12(inputPath, "utf8");
|
|
3073
|
+
return exportTraceEvidenceText(text, { ...opts, sourcePath: inputPath });
|
|
3074
|
+
}
|
|
3075
|
+
async function writeTraceEvidenceExportFile(inputPath, outPath, opts = {}) {
|
|
3076
|
+
const result = await exportTraceEvidenceFile(inputPath, opts);
|
|
3077
|
+
const path = outPath ?? join14(await mkdtemp2(join14(tmpdir2(), "traces-export-")), "spans.openinference.jsonl");
|
|
3078
|
+
await writeFile2(path, serializeSpans(result.spans), "utf8");
|
|
3079
|
+
return { ...result, path };
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
// src/inspect.ts
|
|
3083
|
+
import { mkdtemp as mkdtemp3, readFile as readFile13, writeFile as writeFile3 } from "fs/promises";
|
|
3084
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
3085
|
+
import { basename as basename8, join as join15 } from "path";
|
|
3086
|
+
var severityRank = {
|
|
3087
|
+
high: 0,
|
|
3088
|
+
medium: 1,
|
|
3089
|
+
low: 2
|
|
3090
|
+
};
|
|
3091
|
+
function allContextFiles(index) {
|
|
3092
|
+
return index.context?.roots.flatMap((root) => root.files) ?? [];
|
|
3093
|
+
}
|
|
3094
|
+
function repoLabel(row) {
|
|
3095
|
+
return row.repo.subjectKey ?? row.repo.repository ?? row.repo.cwd ?? row.session.cwd ?? "(repo unknown)";
|
|
3096
|
+
}
|
|
3097
|
+
function sessionLabel(row) {
|
|
3098
|
+
const id = row.session.sessionId.length > 12 ? row.session.sessionId.slice(0, 12) : row.session.sessionId;
|
|
3099
|
+
return `${row.session.harness}:${id} (${repoLabel(row)})`;
|
|
3100
|
+
}
|
|
3101
|
+
function pct(value) {
|
|
3102
|
+
return `${(value * 100).toFixed(value >= 0.1 ? 0 : 1)}%`;
|
|
3103
|
+
}
|
|
3104
|
+
function topToolErrors(row) {
|
|
3105
|
+
const tools = row.tools.filter((tool) => tool.errors > 0).sort((a, b) => b.errors - a.errors || b.calls - a.calls || a.name.localeCompare(b.name)).slice(0, 3).map((tool) => `${tool.name} ${tool.errors}/${tool.calls}`);
|
|
3106
|
+
return tools.length > 0 ? tools.join(", ") : "no tool-level error breakdown";
|
|
3107
|
+
}
|
|
3108
|
+
function topLoopTools(row) {
|
|
3109
|
+
const byTool = /* @__PURE__ */ new Map();
|
|
3110
|
+
for (const loop of row.signals.stuckLoops) {
|
|
3111
|
+
const current = byTool.get(loop.toolName) ?? { groups: 0, maxOccurrences: 0 };
|
|
3112
|
+
current.groups += 1;
|
|
3113
|
+
current.maxOccurrences = Math.max(current.maxOccurrences, loop.occurrences);
|
|
3114
|
+
byTool.set(loop.toolName, current);
|
|
3115
|
+
}
|
|
3116
|
+
const loops = [...byTool.entries()].sort(([, a], [, b]) => b.groups - a.groups || b.maxOccurrences - a.maxOccurrences).slice(0, 3).map(([toolName2, stats]) => `${toolName2} ${stats.groups} group(s), max x${stats.maxOccurrences}`);
|
|
3117
|
+
const suffix = row.signals.stuckLoopsOmitted > 0 ? `, +${row.signals.stuckLoopsOmitted} omitted` : "";
|
|
3118
|
+
return loops.length > 0 ? `${loops.join(", ")}${suffix}` : `${row.signals.stuckLoopCount} loop(s)`;
|
|
3119
|
+
}
|
|
3120
|
+
function sortFindings(findings) {
|
|
3121
|
+
return findings.sort((a, b) => severityRank[a.severity] - severityRank[b.severity] || b.impact - a.impact || a.id.localeCompare(b.id)).map((finding) => ({ ...finding }));
|
|
3122
|
+
}
|
|
3123
|
+
function contextSourceCounts(index) {
|
|
3124
|
+
return { contextFiles: allContextFiles(index).length };
|
|
3125
|
+
}
|
|
3126
|
+
function addSessionFindings(index, findings) {
|
|
3127
|
+
const sessions = index.sessions;
|
|
3128
|
+
if (index.totals.sessions === 0) {
|
|
3129
|
+
findings.push({
|
|
3130
|
+
id: "index.no-sessions",
|
|
3131
|
+
severity: "high",
|
|
3132
|
+
area: "index",
|
|
3133
|
+
title: "No sessions were indexed",
|
|
3134
|
+
impact: 100,
|
|
3135
|
+
evidence: ["The index contains 0 session rows."],
|
|
3136
|
+
next: "Run `traces list` for the target harness, then rerun `traces index` with the right --harness, --cwd, --since, or --last."
|
|
3137
|
+
});
|
|
3138
|
+
return;
|
|
3139
|
+
}
|
|
3140
|
+
const missingRepo = sessions.filter((row) => !row.repo.subjectKey && !row.repo.repository && !row.repo.cwd && !row.session.cwd);
|
|
3141
|
+
if (missingRepo.length > 0) {
|
|
3142
|
+
findings.push({
|
|
3143
|
+
id: "repo.missing-attribution",
|
|
3144
|
+
severity: missingRepo.length / index.totals.sessions >= 0.25 ? "high" : "medium",
|
|
3145
|
+
area: "repo",
|
|
3146
|
+
title: `${missingRepo.length}/${index.totals.sessions} session(s) have no repo or cwd`,
|
|
3147
|
+
impact: missingRepo.length,
|
|
3148
|
+
evidence: missingRepo.slice(0, 5).map((row) => `${row.session.harness}:${row.session.sessionId} at ${row.session.path}`),
|
|
3149
|
+
next: "Improve adapter cwd recovery or pass --cwd for explicit session files so findings can be grouped by repo.",
|
|
3150
|
+
refs: missingRepo.slice(0, 5).map((row) => row.session.path)
|
|
3151
|
+
});
|
|
3152
|
+
}
|
|
3153
|
+
const loopSessions = sessions.filter((row) => row.signals.stuckLoopCount > 0).sort((a, b) => b.signals.stuckLoopCount - a.signals.stuckLoopCount);
|
|
3154
|
+
if (loopSessions.length > 0) {
|
|
3155
|
+
const stuckLoops = loopSessions.reduce((sum, row) => sum + row.signals.stuckLoopCount, 0);
|
|
3156
|
+
findings.push({
|
|
3157
|
+
id: "session.repeated-call-loops",
|
|
3158
|
+
severity: stuckLoops >= 10 || loopSessions.length / index.totals.sessions >= 0.3 ? "high" : "medium",
|
|
3159
|
+
area: "session",
|
|
3160
|
+
title: `Repeated tool-call loops in ${loopSessions.length}/${index.totals.sessions} session(s)`,
|
|
3161
|
+
impact: stuckLoops,
|
|
3162
|
+
evidence: [
|
|
3163
|
+
`${stuckLoops} repeated-call loop(s) across ${loopSessions.length} session(s).`,
|
|
3164
|
+
...loopSessions.slice(0, 5).map((row) => `${sessionLabel(row)}: ${row.signals.stuckLoopCount} loop(s); ${topLoopTools(row)}`)
|
|
3165
|
+
],
|
|
3166
|
+
next: "Inspect the repeated commands and results in the referenced sessions; fix the instruction or tool path that lets the same failed action repeat.",
|
|
3167
|
+
refs: loopSessions.slice(0, 5).map((row) => row.session.path)
|
|
3168
|
+
});
|
|
3169
|
+
}
|
|
3170
|
+
const errorSessions = sessions.filter((row) => row.metrics.erroredToolCallCount >= 10 || row.metrics.toolErrorRate >= 0.1).sort((a, b) => b.metrics.erroredToolCallCount - a.metrics.erroredToolCallCount || b.metrics.toolErrorRate - a.metrics.toolErrorRate);
|
|
3171
|
+
if (errorSessions.length > 0) {
|
|
3172
|
+
const errors = errorSessions.reduce((sum, row) => sum + row.metrics.erroredToolCallCount, 0);
|
|
3173
|
+
const worstRate = Math.max(...errorSessions.map((row) => row.metrics.toolErrorRate));
|
|
3174
|
+
findings.push({
|
|
3175
|
+
id: "session.tool-errors",
|
|
3176
|
+
severity: errors >= 50 || worstRate >= 0.3 ? "high" : "medium",
|
|
3177
|
+
area: "session",
|
|
3178
|
+
title: `High tool-error count/rate in ${errorSessions.length}/${index.totals.sessions} session(s)`,
|
|
3179
|
+
impact: errors,
|
|
3180
|
+
evidence: errorSessions.slice(0, 5).map((row) => `${sessionLabel(row)}: ${row.metrics.erroredToolCallCount}/${row.metrics.toolCallCount} tool errors (${pct(row.metrics.toolErrorRate)}); ${topToolErrors(row)}`),
|
|
3181
|
+
next: "Fix the most common failing command/tool first, then rerun `traces index` to confirm the error count drops.",
|
|
3182
|
+
refs: errorSessions.slice(0, 5).map((row) => row.session.path)
|
|
3183
|
+
});
|
|
3184
|
+
}
|
|
3185
|
+
const largeSessions = sessions.map((row) => ({ row, tokens: row.metrics.inputTokens + row.metrics.outputTokens })).filter(({ tokens: tokens2 }) => tokens2 >= 5e5).sort((a, b) => b.tokens - a.tokens);
|
|
3186
|
+
if (largeSessions.length > 0) {
|
|
3187
|
+
const largest = largeSessions[0]?.tokens ?? 0;
|
|
3188
|
+
findings.push({
|
|
3189
|
+
id: "session.large-token-runs",
|
|
3190
|
+
severity: largest >= 1e6 ? "medium" : "low",
|
|
3191
|
+
area: "session",
|
|
3192
|
+
title: `${largeSessions.length} session(s) reported at least 500k summed tokens`,
|
|
3193
|
+
impact: largest,
|
|
3194
|
+
evidence: largeSessions.slice(0, 5).map(({ row, tokens: tokens2 }) => `${sessionLabel(row)}: ${tokens2.toLocaleString()} reported summed tokens (${row.metrics.inputTokens.toLocaleString()} in, ${row.metrics.outputTokens.toLocaleString()} out)`),
|
|
3195
|
+
next: "Check whether these sessions need shorter handoffs, smaller files, split tasks, or clearer token accounting before optimizing anything else.",
|
|
3196
|
+
refs: largeSessions.slice(0, 5).map(({ row }) => row.session.path)
|
|
3197
|
+
});
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
function addContextFindings(index, findings) {
|
|
3201
|
+
const files = allContextFiles(index);
|
|
3202
|
+
if (index.totals.sessions > 0 && files.length === 0) {
|
|
3203
|
+
findings.push({
|
|
3204
|
+
id: "context.none-indexed",
|
|
3205
|
+
severity: "medium",
|
|
3206
|
+
area: "context",
|
|
3207
|
+
title: "No nearby repo instructions or .evolve files were indexed",
|
|
3208
|
+
impact: index.totals.sessions,
|
|
3209
|
+
evidence: ["The index has session rows but 0 context files."],
|
|
3210
|
+
next: "Add AGENTS.md or CLAUDE.md near active repos, or ensure session cwd points at the repo so traces can join sessions to local process docs."
|
|
3211
|
+
});
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
const hasContextRoots = (index.context?.totals.roots ?? 0) > 0;
|
|
3215
|
+
if (hasContextRoots && (index.context?.totals.instructionDocs ?? 0) === 0) {
|
|
3216
|
+
findings.push({
|
|
3217
|
+
id: "context.no-instruction-docs",
|
|
3218
|
+
severity: "medium",
|
|
3219
|
+
area: "context",
|
|
3220
|
+
title: "Context roots exist but no AGENTS.md, CLAUDE.md, or GEMINI.md files were found",
|
|
3221
|
+
impact: index.context?.totals.roots ?? 0,
|
|
3222
|
+
evidence: index.context?.roots.slice(0, 5).map((root) => root.root) ?? [],
|
|
3223
|
+
next: "Add a short repo-level instruction file that links to stable process docs and anti-patterns."
|
|
3224
|
+
});
|
|
3225
|
+
}
|
|
3226
|
+
const longInstructionDocs = files.filter((file) => file.kind === "instruction-doc" && (file.lines ?? 0) >= 100 && file.markdown?.hasToc === false).sort((a, b) => (b.lines ?? 0) - (a.lines ?? 0));
|
|
3227
|
+
if (longInstructionDocs.length > 0) {
|
|
3228
|
+
const longest = longInstructionDocs[0]?.lines ?? 0;
|
|
3229
|
+
findings.push({
|
|
3230
|
+
id: "context.long-docs-without-toc",
|
|
3231
|
+
severity: longest >= 200 ? "medium" : "low",
|
|
3232
|
+
area: "context",
|
|
3233
|
+
title: `${longInstructionDocs.length} long instruction doc(s) are missing a Contents section`,
|
|
3234
|
+
impact: longest,
|
|
3235
|
+
evidence: longInstructionDocs.slice(0, 5).map((file) => `${file.path}: ${file.lines ?? 0} lines, ${file.markdown?.headings ?? 0} heading(s), no Contents section`),
|
|
3236
|
+
next: "Add a compact Contents section or split stable rules into linked docs/processes and docs/anti-patterns files.",
|
|
3237
|
+
refs: longInstructionDocs.slice(0, 5).map((file) => file.path)
|
|
3238
|
+
});
|
|
3239
|
+
}
|
|
3240
|
+
const longNarrativeDocs = files.filter((file) => (file.kind === "reflection" || file.kind === "handoff") && (file.lines ?? 0) >= 40 && file.markdown?.hasToc === false).sort((a, b) => (b.lines ?? 0) - (a.lines ?? 0));
|
|
3241
|
+
if (longNarrativeDocs.length > 0) {
|
|
3242
|
+
const longest = longNarrativeDocs[0]?.lines ?? 0;
|
|
3243
|
+
findings.push({
|
|
3244
|
+
id: "context.long-narrative-docs-without-toc",
|
|
3245
|
+
severity: longest >= 120 ? "medium" : "low",
|
|
3246
|
+
area: "context",
|
|
3247
|
+
title: `${longNarrativeDocs.length} long reflection/handoff doc(s) are hard to scan`,
|
|
3248
|
+
impact: longest,
|
|
3249
|
+
evidence: longNarrativeDocs.slice(0, 5).map((file) => `${file.path}: ${file.lines ?? 0} lines, ${file.markdown?.headings ?? 0} heading(s), no Contents section`),
|
|
3250
|
+
next: "Put the decision, evidence, and next action at the top; add Contents only when the note is long enough to revisit.",
|
|
3251
|
+
refs: longNarrativeDocs.slice(0, 5).map((file) => file.path)
|
|
3252
|
+
});
|
|
3253
|
+
}
|
|
3254
|
+
const invalidJsonlFiles = files.filter((file) => (file.jsonl?.invalidRows ?? 0) > 0).sort((a, b) => (b.jsonl?.invalidRows ?? 0) - (a.jsonl?.invalidRows ?? 0));
|
|
3255
|
+
if (invalidJsonlFiles.length > 0) {
|
|
3256
|
+
const invalidRows = invalidJsonlFiles.reduce((sum, file) => sum + (file.jsonl?.invalidRows ?? 0), 0);
|
|
3257
|
+
findings.push({
|
|
3258
|
+
id: "context.invalid-jsonl",
|
|
3259
|
+
severity: "high",
|
|
3260
|
+
area: "context",
|
|
3261
|
+
title: `${invalidRows} invalid JSONL row(s) in indexed context files`,
|
|
3262
|
+
impact: invalidRows,
|
|
3263
|
+
evidence: invalidJsonlFiles.slice(0, 5).map((file) => `${file.path}: ${file.jsonl?.invalidRows ?? 0}/${file.jsonl?.rows ?? 0} invalid row(s)`),
|
|
3264
|
+
next: "Fix or quarantine invalid rows before mining these logs; malformed rows make downstream joins incomplete.",
|
|
3265
|
+
refs: invalidJsonlFiles.slice(0, 5).map((file) => file.path)
|
|
3266
|
+
});
|
|
3267
|
+
}
|
|
3268
|
+
const skillRunFiles = files.filter((file) => basename8(file.path) === "skill-runs.jsonl" && (file.jsonl?.rows ?? 0) > 0).map((file) => {
|
|
3269
|
+
const rows = file.jsonl?.rows ?? 0;
|
|
3270
|
+
const keys = file.jsonl?.keys ?? {};
|
|
3271
|
+
const linkKeyCounts = {
|
|
3272
|
+
transcriptPath: keys.transcriptPath ?? 0,
|
|
3273
|
+
traceDir: keys.traceDir ?? 0,
|
|
3274
|
+
sessionPath: keys.sessionPath ?? 0,
|
|
3275
|
+
sessionId: keys.sessionId ?? 0
|
|
3276
|
+
};
|
|
3277
|
+
const bestLinkKeyRows = Math.max(
|
|
3278
|
+
linkKeyCounts.transcriptPath,
|
|
3279
|
+
linkKeyCounts.traceDir,
|
|
3280
|
+
linkKeyCounts.sessionPath,
|
|
3281
|
+
linkKeyCounts.sessionId
|
|
3282
|
+
);
|
|
3283
|
+
return { file, rows, bestLinkKeyRows, linkKeyCounts };
|
|
3284
|
+
}).filter(({ rows, bestLinkKeyRows }) => bestLinkKeyRows < rows).sort((a, b) => b.rows - b.bestLinkKeyRows - (a.rows - a.bestLinkKeyRows));
|
|
3285
|
+
if (skillRunFiles.length > 0) {
|
|
3286
|
+
const missing = skillRunFiles.reduce((sum, row) => sum + (row.rows - row.bestLinkKeyRows), 0);
|
|
3287
|
+
findings.push({
|
|
3288
|
+
id: "context.skill-run-trace-links",
|
|
3289
|
+
severity: skillRunFiles.some((row) => row.bestLinkKeyRows === 0) ? "medium" : "low",
|
|
3290
|
+
area: "context",
|
|
3291
|
+
title: `${skillRunFiles.length} skill-run file(s) have incomplete direct session-link fields`,
|
|
3292
|
+
impact: missing,
|
|
3293
|
+
evidence: skillRunFiles.slice(0, 5).map(({ file, rows, bestLinkKeyRows, linkKeyCounts }) => `${file.path}: best link key covers ${bestLinkKeyRows}/${rows} row(s); transcriptPath ${linkKeyCounts.transcriptPath}, traceDir ${linkKeyCounts.traceDir}, sessionPath ${linkKeyCounts.sessionPath}, sessionId ${linkKeyCounts.sessionId}`),
|
|
3294
|
+
next: "Write a transcriptPath or sessionId on every skill-run row so traces can connect skill outcomes to the actual session behavior.",
|
|
3295
|
+
refs: skillRunFiles.slice(0, 5).map(({ file }) => file.path)
|
|
3296
|
+
});
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
function inspectSessionIndex(index, opts = {}) {
|
|
3300
|
+
const findings = [];
|
|
3301
|
+
addSessionFindings(index, findings);
|
|
3302
|
+
addContextFindings(index, findings);
|
|
3303
|
+
const ranked = sortFindings(findings).slice(0, opts.maxFindings ?? 25);
|
|
3304
|
+
const sourceCounts = contextSourceCounts(index);
|
|
3305
|
+
return {
|
|
3306
|
+
schemaVersion: 1,
|
|
3307
|
+
kind: "traces.inspection_report",
|
|
3308
|
+
generatedAt: opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
3309
|
+
source: {
|
|
3310
|
+
indexGeneratedAt: index.generatedAt,
|
|
3311
|
+
sessions: index.totals.sessions,
|
|
3312
|
+
contextFiles: sourceCounts.contextFiles
|
|
3313
|
+
},
|
|
3314
|
+
totals: {
|
|
3315
|
+
findings: ranked.length,
|
|
3316
|
+
high: ranked.filter((finding) => finding.severity === "high").length,
|
|
3317
|
+
medium: ranked.filter((finding) => finding.severity === "medium").length,
|
|
3318
|
+
low: ranked.filter((finding) => finding.severity === "low").length
|
|
3319
|
+
},
|
|
3320
|
+
findings: ranked
|
|
3321
|
+
};
|
|
3322
|
+
}
|
|
3323
|
+
function renderInspectionReport(report) {
|
|
3324
|
+
const lines = [];
|
|
3325
|
+
lines.push(
|
|
3326
|
+
`traces inspect - ${report.totals.findings} finding(s) from ${report.source.sessions} session(s), ${report.source.contextFiles} context file(s)`
|
|
3327
|
+
);
|
|
3328
|
+
lines.push(`Index generated: ${report.source.indexGeneratedAt ?? "unknown"}`);
|
|
3329
|
+
lines.push(`Report generated: ${report.generatedAt}`);
|
|
3330
|
+
lines.push(`Severity totals: high ${report.totals.high}, medium ${report.totals.medium}, low ${report.totals.low}`);
|
|
3331
|
+
if (report.findings.length === 0) {
|
|
3332
|
+
lines.push("");
|
|
3333
|
+
lines.push("No ranked findings. Keep rerunning this after larger or fresher indexes.");
|
|
3334
|
+
return `${lines.join("\n")}
|
|
3335
|
+
`;
|
|
3336
|
+
}
|
|
3337
|
+
report.findings.forEach((finding, i) => {
|
|
3338
|
+
lines.push("");
|
|
3339
|
+
lines.push(`## ${i + 1}. [${finding.severity}] ${finding.title}`);
|
|
3340
|
+
lines.push(`Area: ${finding.area} Impact: ${finding.impact}`);
|
|
3341
|
+
lines.push("Evidence:");
|
|
3342
|
+
for (const item of finding.evidence) lines.push(`- ${item}`);
|
|
3343
|
+
lines.push(`Next: ${finding.next}`);
|
|
3344
|
+
if (finding.refs && finding.refs.length > 0) {
|
|
3345
|
+
lines.push("Refs:");
|
|
3346
|
+
for (const ref of finding.refs.slice(0, 5)) lines.push(`- ${ref}`);
|
|
3347
|
+
}
|
|
3348
|
+
});
|
|
3349
|
+
return `${lines.join("\n")}
|
|
3350
|
+
`;
|
|
3351
|
+
}
|
|
3352
|
+
function serializeInspectionReport(report) {
|
|
3353
|
+
return `${JSON.stringify(report, null, 2)}
|
|
3354
|
+
`;
|
|
3355
|
+
}
|
|
3356
|
+
function assertSessionIndex(value) {
|
|
3357
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3358
|
+
throw new Error("session index file must contain a JSON object");
|
|
3359
|
+
}
|
|
3360
|
+
const candidate = value;
|
|
3361
|
+
if (candidate.kind !== "traces.session_index" || candidate.schemaVersion !== 1 || !Array.isArray(candidate.sessions)) {
|
|
3362
|
+
throw new Error("input is not a traces.session_index v1 file");
|
|
3363
|
+
}
|
|
3364
|
+
if (!candidate.totals || typeof candidate.totals !== "object") {
|
|
3365
|
+
throw new Error("session index is missing totals");
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
async function readSessionIndexFile(path) {
|
|
3369
|
+
const parsed = JSON.parse(await readFile13(path, "utf8"));
|
|
3370
|
+
assertSessionIndex(parsed);
|
|
3371
|
+
return parsed;
|
|
3372
|
+
}
|
|
3373
|
+
async function writeInspectionReportFile(report, outPath) {
|
|
3374
|
+
const path = outPath ?? join15(await mkdtemp3(join15(tmpdir3(), "traces-inspect-")), "inspection-report.md");
|
|
3375
|
+
await writeFile3(path, renderInspectionReport(report), "utf8");
|
|
3376
|
+
return path;
|
|
3377
|
+
}
|
|
3378
|
+
|
|
3379
|
+
// src/observer.ts
|
|
3380
|
+
var sleep = (ms, signal) => new Promise((resolve) => {
|
|
3381
|
+
const onAbort = () => {
|
|
3382
|
+
clearTimeout(t);
|
|
3383
|
+
resolve();
|
|
3384
|
+
};
|
|
3385
|
+
const t = setTimeout(() => {
|
|
3386
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3387
|
+
resolve();
|
|
3388
|
+
}, ms);
|
|
3389
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
3390
|
+
});
|
|
3391
|
+
function loopKey(sessionId, toolName2, argHash) {
|
|
3392
|
+
return `${sessionId}\0${toolName2}\0${argHash}`;
|
|
3393
|
+
}
|
|
3394
|
+
async function watchSessions(opts) {
|
|
3395
|
+
const windowMs = opts.windowMs ?? 30 * 6e4;
|
|
3396
|
+
const intervalMs = Math.max(250, opts.intervalMs ?? 5e3);
|
|
3397
|
+
const minLoop = opts.minLoopOccurrences ?? 3;
|
|
3398
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3399
|
+
while (!opts.signal?.aborted) {
|
|
3400
|
+
let sessionCount = 0;
|
|
3401
|
+
for await (const { adapter, ref, spans } of scanSessions({
|
|
3402
|
+
adapters: opts.adapters,
|
|
3403
|
+
all: opts.all,
|
|
3404
|
+
harnesses: opts.harnesses,
|
|
3405
|
+
cwd: opts.cwd,
|
|
3406
|
+
sinceMs: Date.now() - windowMs,
|
|
3407
|
+
signal: opts.signal,
|
|
3408
|
+
onError: opts.onError
|
|
3409
|
+
})) {
|
|
3410
|
+
let report;
|
|
3411
|
+
try {
|
|
3412
|
+
report = await runPipelines(spans, { minLoopOccurrences: minLoop });
|
|
3413
|
+
} catch (err) {
|
|
3414
|
+
opts.onError?.(err, ref);
|
|
3415
|
+
continue;
|
|
3416
|
+
}
|
|
3417
|
+
sessionCount += 1;
|
|
3418
|
+
if (opts.onReport) {
|
|
3419
|
+
try {
|
|
3420
|
+
await opts.onReport(ref, report);
|
|
3421
|
+
} catch (err) {
|
|
3422
|
+
opts.onError?.(err, ref);
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
for (const f of report.stuckLoops.findings) {
|
|
3426
|
+
const key = loopKey(ref.sessionId, f.toolName, f.argHash);
|
|
3427
|
+
if (f.occurrences > (seen.get(key) ?? 0)) {
|
|
3428
|
+
seen.set(key, f.occurrences);
|
|
3429
|
+
if (opts.onLoop) {
|
|
3430
|
+
try {
|
|
3431
|
+
await opts.onLoop({
|
|
3432
|
+
harness: adapter.harness,
|
|
3433
|
+
sessionId: ref.sessionId,
|
|
3434
|
+
cwd: ref.cwd,
|
|
3435
|
+
toolName: f.toolName,
|
|
3436
|
+
argHash: f.argHash,
|
|
3437
|
+
occurrences: f.occurrences,
|
|
3438
|
+
windowMs: f.windowMs
|
|
3439
|
+
});
|
|
3440
|
+
} catch (err) {
|
|
3441
|
+
opts.onError?.(err, ref);
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
try {
|
|
3448
|
+
opts.onTick?.({ sessions: sessionCount });
|
|
3449
|
+
} catch (err) {
|
|
3450
|
+
opts.onError?.(err);
|
|
3451
|
+
}
|
|
3452
|
+
if (opts.signal?.aborted) break;
|
|
3453
|
+
await sleep(intervalMs, opts.signal);
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
|
|
3457
|
+
// src/reactions.ts
|
|
3458
|
+
var REACTIONS = ["correction", "frustration", "jargon", "structure", "praise"];
|
|
3459
|
+
function emptySignals() {
|
|
3460
|
+
return { correction: 0, frustration: 0, jargon: 0, structure: 0, praise: 0 };
|
|
3461
|
+
}
|
|
3462
|
+
function spanStep(s) {
|
|
3463
|
+
const v = s.attributes.step;
|
|
3464
|
+
return typeof v === "number" ? v : Number.MAX_SAFE_INTEGER;
|
|
3465
|
+
}
|
|
3466
|
+
function ordered(spans) {
|
|
3467
|
+
return [...spans].sort((a, b) => {
|
|
3468
|
+
const ds = spanStep(a) - spanStep(b);
|
|
3469
|
+
if (ds !== 0) return ds;
|
|
3470
|
+
return a.start_time.localeCompare(b.start_time);
|
|
3471
|
+
});
|
|
3472
|
+
}
|
|
3473
|
+
function isAssistant(s) {
|
|
3474
|
+
const kind = s.attributes["openinference.span.kind"];
|
|
3475
|
+
if (s.name === "user.prompt") return false;
|
|
3476
|
+
return kind === "LLM" || s.name.startsWith("message.assistant");
|
|
3477
|
+
}
|
|
3478
|
+
function isHumanPrompt(s) {
|
|
3479
|
+
return s.name === "user.prompt" && s.attributes[ACTOR_ATTR] === "human";
|
|
3480
|
+
}
|
|
3481
|
+
function contentOf(s) {
|
|
3482
|
+
const c = s.attributes.content;
|
|
3483
|
+
return typeof c === "string" ? c : "";
|
|
3484
|
+
}
|
|
3485
|
+
var MAX_PAIRS = 10;
|
|
3486
|
+
function pairStrength(reactions) {
|
|
3487
|
+
let n = 0;
|
|
3488
|
+
for (const r of reactions) n += CORRECTIVE_REACTIONS.includes(r) ? 2 : 1;
|
|
3489
|
+
return n;
|
|
3490
|
+
}
|
|
3491
|
+
function analyzeReactions(spans) {
|
|
3492
|
+
const byTrace = /* @__PURE__ */ new Map();
|
|
3493
|
+
for (const s of spans) {
|
|
3494
|
+
const arr = byTrace.get(s.trace_id) ?? [];
|
|
3495
|
+
arr.push(s);
|
|
3496
|
+
byTrace.set(s.trace_id, arr);
|
|
3497
|
+
}
|
|
3498
|
+
const sessions = [];
|
|
3499
|
+
const corpus = emptySignals();
|
|
3500
|
+
let corpusTurns = 0;
|
|
3501
|
+
const pairs = [];
|
|
3502
|
+
for (const [sessionId, traceSpans] of byTrace) {
|
|
3503
|
+
const seq = ordered(traceSpans);
|
|
3504
|
+
const sessionSignals = emptySignals();
|
|
3505
|
+
let humanReactionTurns = 0;
|
|
3506
|
+
let lastAssistant = null;
|
|
3507
|
+
for (const s of seq) {
|
|
3508
|
+
if (isAssistant(s)) {
|
|
3509
|
+
const text = contentOf(s);
|
|
3510
|
+
if (text) lastAssistant = text;
|
|
3511
|
+
continue;
|
|
3512
|
+
}
|
|
3513
|
+
if (!isHumanPrompt(s)) continue;
|
|
3514
|
+
if (lastAssistant === null) continue;
|
|
3515
|
+
humanReactionTurns += 1;
|
|
3516
|
+
const reactions = classifyReaction(contentOf(s));
|
|
3517
|
+
if (reactions.length > 0) {
|
|
3518
|
+
for (const r of reactions) {
|
|
3519
|
+
sessionSignals[r] += 1;
|
|
3520
|
+
corpus[r] += 1;
|
|
3521
|
+
}
|
|
3522
|
+
pairs.push({ assistant: lastAssistant, human: contentOf(s), reactions, sessionId });
|
|
3523
|
+
}
|
|
3524
|
+
lastAssistant = null;
|
|
3525
|
+
}
|
|
3526
|
+
corpusTurns += humanReactionTurns;
|
|
3527
|
+
sessions.push({ sessionId, humanReactionTurns, signals: sessionSignals });
|
|
3528
|
+
}
|
|
3529
|
+
const corrective = CORRECTIVE_REACTIONS.reduce((n, r) => n + corpus[r], 0);
|
|
3530
|
+
const positive = corpus.praise;
|
|
3531
|
+
const totalSignals = corrective + positive;
|
|
3532
|
+
const ratio = totalSignals === 0 ? null : positive === 0 ? Number.POSITIVE_INFINITY : corrective / positive;
|
|
3533
|
+
pairs.sort((a, b) => pairStrength(b.reactions) - pairStrength(a.reactions));
|
|
3534
|
+
return {
|
|
3535
|
+
sessions,
|
|
3536
|
+
signals: corpus,
|
|
3537
|
+
humanReactionTurns: corpusTurns,
|
|
3538
|
+
correctiveToPositiveRatio: ratio,
|
|
3539
|
+
triggerPairs: pairs.slice(0, MAX_PAIRS)
|
|
3540
|
+
};
|
|
3541
|
+
}
|
|
3542
|
+
|
|
3543
|
+
// src/session-index.ts
|
|
3544
|
+
import { mkdtemp as mkdtemp4, readdir as readdir11, readFile as readFile14, stat as stat12, writeFile as writeFile4 } from "fs/promises";
|
|
3545
|
+
import { tmpdir as tmpdir4 } from "os";
|
|
3546
|
+
import { dirname as dirname2, join as join16 } from "path";
|
|
3547
|
+
function indexRow(record) {
|
|
3548
|
+
return {
|
|
3549
|
+
session: record.session,
|
|
3550
|
+
repo: record.repo,
|
|
3551
|
+
time: {
|
|
3552
|
+
firstSpanAt: record.metrics.firstSpanAt,
|
|
3553
|
+
lastSpanAt: record.metrics.lastSpanAt
|
|
3554
|
+
},
|
|
3555
|
+
metrics: {
|
|
3556
|
+
spanCount: record.metrics.spanCount,
|
|
3557
|
+
llmTurnCount: record.metrics.llmTurnCount,
|
|
3558
|
+
toolCallCount: record.metrics.toolCallCount,
|
|
3559
|
+
erroredToolCallCount: record.metrics.erroredToolCallCount,
|
|
3560
|
+
inputTokens: record.metrics.inputTokens,
|
|
3561
|
+
outputTokens: record.metrics.outputTokens,
|
|
3562
|
+
toolErrorRate: record.signals.toolErrorRate
|
|
3563
|
+
},
|
|
3564
|
+
models: record.metrics.models,
|
|
3565
|
+
tools: record.metrics.tools,
|
|
3566
|
+
signals: record.signals
|
|
3567
|
+
};
|
|
3568
|
+
}
|
|
3569
|
+
function uniqueSorted(values) {
|
|
3570
|
+
return [...new Set([...values].filter((value) => Boolean(value)))].sort();
|
|
3571
|
+
}
|
|
3572
|
+
function buildSessionIndex(records, opts = {}) {
|
|
3573
|
+
const sessions = records.map(indexRow);
|
|
3574
|
+
return {
|
|
3575
|
+
schemaVersion: 1,
|
|
3576
|
+
kind: "traces.session_index",
|
|
3577
|
+
generatedAt: opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
3578
|
+
...opts.selection ? { selection: opts.selection } : {},
|
|
3579
|
+
totals: {
|
|
3580
|
+
sessions: sessions.length,
|
|
3581
|
+
spans: sessions.reduce((sum, row) => sum + row.metrics.spanCount, 0),
|
|
3582
|
+
llmTurns: sessions.reduce((sum, row) => sum + row.metrics.llmTurnCount, 0),
|
|
3583
|
+
toolCalls: sessions.reduce((sum, row) => sum + row.metrics.toolCallCount, 0),
|
|
3584
|
+
erroredToolCalls: sessions.reduce((sum, row) => sum + row.metrics.erroredToolCallCount, 0),
|
|
3585
|
+
inputTokens: sessions.reduce((sum, row) => sum + row.metrics.inputTokens, 0),
|
|
3586
|
+
outputTokens: sessions.reduce((sum, row) => sum + row.metrics.outputTokens, 0),
|
|
3587
|
+
stuckLoopSessions: sessions.filter((row) => row.signals.stuckLoopCount > 0).length,
|
|
3588
|
+
stuckLoops: sessions.reduce((sum, row) => sum + row.signals.stuckLoopCount, 0),
|
|
3589
|
+
harnesses: uniqueSorted(sessions.map((row) => row.session.harness)),
|
|
3590
|
+
repos: uniqueSorted(sessions.map((row) => row.repo.subjectKey ?? row.repo.repository ?? row.repo.cwd ?? row.session.cwd ?? void 0)),
|
|
3591
|
+
models: uniqueSorted(sessions.flatMap((row) => row.models)),
|
|
3592
|
+
tools: uniqueSorted(sessions.flatMap((row) => row.tools.map((tool) => tool.name)))
|
|
3593
|
+
},
|
|
3594
|
+
...opts.context ? { context: opts.context } : {},
|
|
3595
|
+
sessions
|
|
3596
|
+
};
|
|
3597
|
+
}
|
|
3598
|
+
async function pathStat2(path) {
|
|
3599
|
+
try {
|
|
3600
|
+
return await stat12(path);
|
|
3601
|
+
} catch {
|
|
3602
|
+
return null;
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
async function fileExists(path) {
|
|
3606
|
+
return Boolean((await pathStat2(path))?.isFile());
|
|
3607
|
+
}
|
|
3608
|
+
async function dirExists(path) {
|
|
3609
|
+
return Boolean((await pathStat2(path))?.isDirectory());
|
|
3610
|
+
}
|
|
3611
|
+
async function findContextRoot(cwd) {
|
|
3612
|
+
if (!cwd) return null;
|
|
3613
|
+
let current = cwd;
|
|
3614
|
+
const s = await pathStat2(current);
|
|
3615
|
+
if (!s) return null;
|
|
3616
|
+
if (!s.isDirectory()) current = dirname2(current);
|
|
3617
|
+
for (; ; ) {
|
|
3618
|
+
if (await pathStat2(join16(current, ".git")) || await dirExists(join16(current, ".evolve")) || await fileExists(join16(current, "AGENTS.md")) || await fileExists(join16(current, "CLAUDE.md"))) {
|
|
3619
|
+
return current;
|
|
3620
|
+
}
|
|
3621
|
+
const parent = dirname2(current);
|
|
3622
|
+
if (parent === current) return s.isDirectory() ? cwd : dirname2(cwd);
|
|
3623
|
+
current = parent;
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
function countLines(text) {
|
|
3627
|
+
if (text.length === 0) return 0;
|
|
3628
|
+
return text.endsWith("\n") ? text.split("\n").length - 1 : text.split("\n").length;
|
|
3629
|
+
}
|
|
3630
|
+
function summarizeMarkdown(text) {
|
|
3631
|
+
const lines = text.split("\n");
|
|
3632
|
+
return {
|
|
3633
|
+
headings: lines.filter((line) => /^#{1,6}\s+\S/.test(line)).length,
|
|
3634
|
+
hasToc: /\btable of contents\b/i.test(text) || /^\s*-\s+\[[^\]]+\]\(#[^)]+\)/m.test(text)
|
|
3635
|
+
};
|
|
3636
|
+
}
|
|
3637
|
+
function summarizeJsonl(text) {
|
|
3638
|
+
const keys = {};
|
|
3639
|
+
let rows = 0;
|
|
3640
|
+
let invalidRows = 0;
|
|
3641
|
+
for (const line of text.split("\n")) {
|
|
3642
|
+
if (!line.trim()) continue;
|
|
3643
|
+
rows += 1;
|
|
3644
|
+
try {
|
|
3645
|
+
const parsed = JSON.parse(line);
|
|
3646
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
3647
|
+
for (const key of Object.keys(parsed)) keys[key] = (keys[key] ?? 0) + 1;
|
|
3648
|
+
}
|
|
3649
|
+
} catch {
|
|
3650
|
+
invalidRows += 1;
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
return { rows, invalidRows, keys: Object.fromEntries(Object.entries(keys).sort(([a], [b]) => a.localeCompare(b))) };
|
|
3654
|
+
}
|
|
3655
|
+
async function summarizeFile(path, kind) {
|
|
3656
|
+
const s = await pathStat2(path);
|
|
3657
|
+
if (!s?.isFile()) return null;
|
|
3658
|
+
const base = { kind, path, bytes: Number(s.size), mtimeMs: Number(s.mtimeMs) };
|
|
3659
|
+
if (!path.endsWith(".md") && !path.endsWith(".jsonl")) return base;
|
|
3660
|
+
try {
|
|
3661
|
+
const text = await readFile14(path, "utf8");
|
|
3662
|
+
const lines = countLines(text);
|
|
3663
|
+
const jsonl = path.endsWith(".jsonl") ? summarizeJsonl(text) : void 0;
|
|
3664
|
+
return {
|
|
3665
|
+
...base,
|
|
3666
|
+
lines,
|
|
3667
|
+
...path.endsWith(".md") ? { markdown: summarizeMarkdown(text) } : {},
|
|
3668
|
+
...jsonl ? { jsonlRows: jsonl.rows, jsonl } : {}
|
|
3669
|
+
};
|
|
3670
|
+
} catch {
|
|
3671
|
+
return base;
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
async function walkMarkdown(root, kind, maxFiles = 100) {
|
|
3675
|
+
const out = [];
|
|
3676
|
+
async function walk(dir) {
|
|
3677
|
+
if (out.length >= maxFiles) return;
|
|
3678
|
+
let entries;
|
|
3679
|
+
try {
|
|
3680
|
+
entries = await readdir11(dir, { withFileTypes: true });
|
|
3681
|
+
} catch {
|
|
3682
|
+
return;
|
|
3683
|
+
}
|
|
3684
|
+
for (const entry of entries) {
|
|
3685
|
+
if (out.length >= maxFiles) return;
|
|
3686
|
+
const path = join16(dir, entry.name);
|
|
3687
|
+
if (entry.isDirectory()) {
|
|
3688
|
+
await walk(path);
|
|
3689
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
3690
|
+
const summary = await summarizeFile(path, kind);
|
|
3691
|
+
if (summary) out.push(summary);
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
await walk(root);
|
|
3696
|
+
return out;
|
|
3697
|
+
}
|
|
3698
|
+
async function collectContextRoot(root) {
|
|
3699
|
+
const files = [];
|
|
3700
|
+
for (const name of ["AGENTS.md", "CLAUDE.md", "GEMINI.md"]) {
|
|
3701
|
+
const summary = await summarizeFile(join16(root, name), "instruction-doc");
|
|
3702
|
+
if (summary) files.push(summary);
|
|
3703
|
+
}
|
|
3704
|
+
const evolve = join16(root, ".evolve");
|
|
3705
|
+
for (const name of ["skill-runs.jsonl", "governor.jsonl", "experiments.jsonl"]) {
|
|
3706
|
+
const summary = await summarizeFile(join16(evolve, name), "evolve-jsonl");
|
|
3707
|
+
if (summary) files.push(summary);
|
|
3708
|
+
}
|
|
3709
|
+
for (const name of ["scorecard.json"]) {
|
|
3710
|
+
const summary = await summarizeFile(join16(evolve, name), "evolve-json");
|
|
3711
|
+
if (summary) files.push(summary);
|
|
3712
|
+
}
|
|
3713
|
+
files.push(...await walkMarkdown(join16(evolve, "reflections"), "reflection"));
|
|
3714
|
+
files.push(...await walkMarkdown(join16(evolve, "handoffs"), "handoff"));
|
|
3715
|
+
return { root, files: files.sort((a, b) => a.path.localeCompare(b.path)) };
|
|
3716
|
+
}
|
|
3717
|
+
async function collectContextIndex(records) {
|
|
3718
|
+
const rootSet = /* @__PURE__ */ new Set();
|
|
3719
|
+
for (const record of records) {
|
|
3720
|
+
const root = await findContextRoot(record.repo.cwd ?? record.session.cwd);
|
|
3721
|
+
if (root) rootSet.add(root);
|
|
3722
|
+
}
|
|
3723
|
+
const roots = await Promise.all([...rootSet].sort().map(collectContextRoot));
|
|
3724
|
+
const files = roots.flatMap((root) => root.files);
|
|
3725
|
+
return {
|
|
3726
|
+
totals: {
|
|
3727
|
+
roots: roots.length,
|
|
3728
|
+
files: files.length,
|
|
3729
|
+
instructionDocs: files.filter((file) => file.kind === "instruction-doc").length,
|
|
3730
|
+
evolveFiles: files.filter((file) => file.kind.startsWith("evolve-")).length,
|
|
3731
|
+
jsonlRows: files.reduce((sum, file) => sum + (file.jsonlRows ?? 0), 0),
|
|
3732
|
+
invalidJsonlRows: files.reduce((sum, file) => sum + (file.jsonl?.invalidRows ?? 0), 0)
|
|
3733
|
+
},
|
|
3734
|
+
roots
|
|
3735
|
+
};
|
|
3736
|
+
}
|
|
3737
|
+
async function buildSessionIndexFromRows(rows, opts = {}) {
|
|
3738
|
+
const generatedAt = opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
3739
|
+
const records = await Promise.all(rows.map(
|
|
3740
|
+
(row) => buildPolicyEvidenceRecord(row.ref, row.spans, {
|
|
3741
|
+
generatedAt,
|
|
3742
|
+
minLoopOccurrences: opts.minLoopOccurrences
|
|
3743
|
+
})
|
|
3744
|
+
));
|
|
3745
|
+
const context = opts.includeContext === false ? void 0 : await collectContextIndex(records);
|
|
3746
|
+
return buildSessionIndex(records, { ...opts, generatedAt, context });
|
|
3747
|
+
}
|
|
3748
|
+
async function collectSessionIndex(opts = {}) {
|
|
3749
|
+
const rows = [];
|
|
3750
|
+
for await (const session of scanSessions(opts)) {
|
|
3751
|
+
rows.push({ ref: session.ref, spans: session.spans });
|
|
3752
|
+
}
|
|
3753
|
+
return buildSessionIndexFromRows(rows, opts);
|
|
3754
|
+
}
|
|
3755
|
+
function serializeSessionIndex(index) {
|
|
3756
|
+
return `${JSON.stringify(index, null, 2)}
|
|
3757
|
+
`;
|
|
3758
|
+
}
|
|
3759
|
+
async function writeSessionIndexFile(index, outPath) {
|
|
3760
|
+
const path = outPath ?? join16(await mkdtemp4(join16(tmpdir4(), "traces-index-")), "session-index.json");
|
|
3761
|
+
await writeFile4(path, serializeSessionIndex(index), "utf8");
|
|
3762
|
+
return path;
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
// src/upload-state.ts
|
|
3766
|
+
import { createHash as createHash2 } from "crypto";
|
|
3767
|
+
import { mkdir, readFile as readFile15, writeFile as writeFile5 } from "fs/promises";
|
|
2181
3768
|
import { homedir as homedir11 } from "os";
|
|
2182
|
-
import { dirname, join as
|
|
3769
|
+
import { dirname as dirname3, join as join17 } from "path";
|
|
2183
3770
|
function stateFile() {
|
|
2184
|
-
const base = process.env.XDG_STATE_HOME ??
|
|
2185
|
-
return
|
|
3771
|
+
const base = process.env.XDG_STATE_HOME ?? join17(homedir11(), ".local", "state");
|
|
3772
|
+
return join17(base, "tangle-traces", "uploaded.json");
|
|
2186
3773
|
}
|
|
2187
3774
|
function uploadKey(harness, sessionId) {
|
|
2188
3775
|
return `${harness}:${sessionId}`;
|
|
2189
3776
|
}
|
|
2190
3777
|
function sessionHash(spans) {
|
|
2191
|
-
const h =
|
|
3778
|
+
const h = createHash2("sha256");
|
|
2192
3779
|
for (const s of spans) {
|
|
2193
3780
|
h.update(`${s.span_id}\0${s.name}\0${s.start_time}\0${s.status.code}
|
|
2194
3781
|
`);
|
|
@@ -2197,15 +3784,15 @@ function sessionHash(spans) {
|
|
|
2197
3784
|
}
|
|
2198
3785
|
async function loadState() {
|
|
2199
3786
|
try {
|
|
2200
|
-
return JSON.parse(await
|
|
3787
|
+
return JSON.parse(await readFile15(stateFile(), "utf8"));
|
|
2201
3788
|
} catch {
|
|
2202
3789
|
return {};
|
|
2203
3790
|
}
|
|
2204
3791
|
}
|
|
2205
3792
|
async function saveState(state) {
|
|
2206
3793
|
const f = stateFile();
|
|
2207
|
-
await mkdir(
|
|
2208
|
-
await
|
|
3794
|
+
await mkdir(dirname3(f), { recursive: true });
|
|
3795
|
+
await writeFile5(f, `${JSON.stringify(state, null, 2)}
|
|
2209
3796
|
`, "utf8");
|
|
2210
3797
|
}
|
|
2211
3798
|
function alreadyUploaded(state, harness, sessionId, hash) {
|
|
@@ -2213,10 +3800,10 @@ function alreadyUploaded(state, harness, sessionId, hash) {
|
|
|
2213
3800
|
}
|
|
2214
3801
|
|
|
2215
3802
|
// src/upload.ts
|
|
2216
|
-
import { readFile as
|
|
3803
|
+
import { readFile as readFile16 } from "fs/promises";
|
|
2217
3804
|
import { createRequire } from "module";
|
|
2218
3805
|
import { hostname } from "os";
|
|
2219
|
-
import { basename as
|
|
3806
|
+
import { basename as basename9, join as join18 } from "path";
|
|
2220
3807
|
import { hostedClientFromEnv } from "@tangle-network/agent-eval/hosted";
|
|
2221
3808
|
import { REDACTION_VERSION } from "@tangle-network/agent-eval/traces";
|
|
2222
3809
|
var require2 = createRequire(import.meta.url);
|
|
@@ -2240,7 +3827,7 @@ async function planUpload(opts = {}) {
|
|
|
2240
3827
|
async function gitBranch(cwd) {
|
|
2241
3828
|
if (!cwd) return void 0;
|
|
2242
3829
|
try {
|
|
2243
|
-
const head = await
|
|
3830
|
+
const head = await readFile16(join18(cwd, ".git", "HEAD"), "utf8");
|
|
2244
3831
|
const m = head.match(/^ref:\s*refs\/heads\/(.+)$/m);
|
|
2245
3832
|
return m?.[1]?.trim();
|
|
2246
3833
|
} catch {
|
|
@@ -2250,7 +3837,7 @@ async function gitBranch(cwd) {
|
|
|
2250
3837
|
async function sessionMeta(item, uploadedAt) {
|
|
2251
3838
|
const meta = {
|
|
2252
3839
|
[ATTR.HARNESS]: item.ref.harness,
|
|
2253
|
-
[ATTR.SESSION_FILE]:
|
|
3840
|
+
[ATTR.SESSION_FILE]: basename9(item.ref.path),
|
|
2254
3841
|
[ATTR.HOST]: hostname(),
|
|
2255
3842
|
[ATTR.UPLOADED_AT]: uploadedAt,
|
|
2256
3843
|
[ATTR.UPLOADER]: `tangle-traces@${TRACES_VERSION}`,
|
|
@@ -2348,25 +3935,31 @@ async function executeUpload(plan, opts = {}) {
|
|
|
2348
3935
|
}
|
|
2349
3936
|
|
|
2350
3937
|
export {
|
|
3938
|
+
countSkillRunsJsonl,
|
|
3939
|
+
analyzeAdoption,
|
|
2351
3940
|
span,
|
|
2352
3941
|
toOpenInferenceSpan,
|
|
2353
3942
|
serializeSpans,
|
|
2354
3943
|
writeOtlpFile,
|
|
2355
3944
|
analyzeSpans,
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
haloAnalyzer,
|
|
2360
|
-
runExternalAnalyzers,
|
|
2361
|
-
commandRedactor,
|
|
3945
|
+
ATTR,
|
|
3946
|
+
INGEST_SOURCE_CLI,
|
|
3947
|
+
DEFAULT_HARNESS,
|
|
2362
3948
|
parseIsoToEpochMs,
|
|
2363
3949
|
parseSince,
|
|
2364
3950
|
toRuntimeStore,
|
|
2365
3951
|
runPipelines,
|
|
2366
3952
|
CONTENT_CAP,
|
|
2367
3953
|
capText,
|
|
3954
|
+
ACTOR_ATTR,
|
|
2368
3955
|
userPromptSpan,
|
|
2369
3956
|
AmpAdapter,
|
|
3957
|
+
textIsSynthetic,
|
|
3958
|
+
looksLikeAgentPrompt,
|
|
3959
|
+
claudeActor,
|
|
3960
|
+
codexActor,
|
|
3961
|
+
classifyReaction,
|
|
3962
|
+
CORRECTIVE_REACTIONS,
|
|
2370
3963
|
ClaudeAdapter,
|
|
2371
3964
|
CodexAdapter,
|
|
2372
3965
|
CopilotAdapter,
|
|
@@ -2380,15 +3973,42 @@ export {
|
|
|
2380
3973
|
resolveAdapter,
|
|
2381
3974
|
knownHarnesses,
|
|
2382
3975
|
selectAdapters,
|
|
3976
|
+
normalizeRemote,
|
|
3977
|
+
resolveRepoAttrs,
|
|
3978
|
+
resolveSessionRepoAttrs,
|
|
3979
|
+
stampRepoAttrs,
|
|
3980
|
+
parseSession,
|
|
2383
3981
|
scanSessions,
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
3982
|
+
buildPolicyEvidenceRecord,
|
|
3983
|
+
collectPolicyEvidence,
|
|
3984
|
+
serializePolicyEvidence,
|
|
3985
|
+
writePolicyEvidenceFile,
|
|
3986
|
+
runCommand,
|
|
3987
|
+
commandAnalyzer,
|
|
3988
|
+
haloAnalyzer,
|
|
3989
|
+
runExternalAnalyzers,
|
|
3990
|
+
commandRedactor,
|
|
2388
3991
|
CODING_REDACTION_RULES,
|
|
2389
3992
|
TRACES_REDACTION_RULES,
|
|
2390
3993
|
redactSpans,
|
|
2391
3994
|
applyRedactor,
|
|
3995
|
+
exportTraceEvidenceRows,
|
|
3996
|
+
exportTraceEvidenceText,
|
|
3997
|
+
exportTraceEvidenceFile,
|
|
3998
|
+
writeTraceEvidenceExportFile,
|
|
3999
|
+
inspectSessionIndex,
|
|
4000
|
+
renderInspectionReport,
|
|
4001
|
+
serializeInspectionReport,
|
|
4002
|
+
readSessionIndexFile,
|
|
4003
|
+
writeInspectionReportFile,
|
|
4004
|
+
watchSessions,
|
|
4005
|
+
REACTIONS,
|
|
4006
|
+
analyzeReactions,
|
|
4007
|
+
buildSessionIndex,
|
|
4008
|
+
buildSessionIndexFromRows,
|
|
4009
|
+
collectSessionIndex,
|
|
4010
|
+
serializeSessionIndex,
|
|
4011
|
+
writeSessionIndexFile,
|
|
2392
4012
|
uploadKey,
|
|
2393
4013
|
sessionHash,
|
|
2394
4014
|
loadState,
|