@tangle-network/traces 0.8.5 → 0.8.12
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 +33 -3
- package/dist/{chunk-D5OMTM4S.js → chunk-PRRWLKSI.js} +770 -39
- package/dist/cli.js +64 -3
- package/dist/index.d.ts +152 -2
- package/dist/index.js +23 -1
- package/install.sh +102 -0
- package/package.json +3 -1
|
@@ -137,7 +137,7 @@ function toOpenInferenceSpan(s) {
|
|
|
137
137
|
const resourceAttrs = {};
|
|
138
138
|
if (a["service.name"] != null) resourceAttrs["service.name"] = a["service.name"];
|
|
139
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"]) {
|
|
140
|
+
for (const k of ["tangle.subject.key", "git.repository", "git.branch", "git.commit", "tangle.cwd", "traces.repo_resolution_source"]) {
|
|
141
141
|
if (a[k] != null) resourceAttrs[k] = a[k];
|
|
142
142
|
}
|
|
143
143
|
return {
|
|
@@ -160,11 +160,11 @@ function serializeSpans(spans) {
|
|
|
160
160
|
`;
|
|
161
161
|
}
|
|
162
162
|
async function writeOtlpFile(spans, outPath) {
|
|
163
|
-
const { mkdtemp:
|
|
164
|
-
const { tmpdir:
|
|
165
|
-
const { join:
|
|
166
|
-
const path = outPath ??
|
|
167
|
-
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");
|
|
168
168
|
return path;
|
|
169
169
|
}
|
|
170
170
|
|
|
@@ -218,6 +218,8 @@ var ATTR = {
|
|
|
218
218
|
GIT_BRANCH_NAME: "git.branch",
|
|
219
219
|
/** HEAD short sha at conversion time. */
|
|
220
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",
|
|
221
223
|
GIT_BRANCH: "tangle.git_branch",
|
|
222
224
|
HOST: "tangle.host",
|
|
223
225
|
/** Basename of the session file. (Renamed from the ambiguous `tangle.source`,
|
|
@@ -860,14 +862,18 @@ var CodexAdapter = class {
|
|
|
860
862
|
continue;
|
|
861
863
|
}
|
|
862
864
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
863
|
-
const head = (await readFile4(path, "utf8").catch(() => "")).split("\n",
|
|
865
|
+
const head = (await readFile4(path, "utf8").catch(() => "")).split("\n", 40);
|
|
864
866
|
let cwd = null;
|
|
865
867
|
let id = basename3(path).replace(/^rollout-[\dT-]+-/, "").replace(/\.jsonl$/, "");
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
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;
|
|
871
877
|
}
|
|
872
878
|
if (opts.cwd && (!cwd || !cwd.startsWith(opts.cwd))) continue;
|
|
873
879
|
refs.push({ harness: this.harness, sessionId: id, path, cwd, mtimeMs: st.mtimeMs });
|
|
@@ -1441,6 +1447,20 @@ var GeminiFamilyAdapter = class {
|
|
|
1441
1447
|
chatsRoot() {
|
|
1442
1448
|
return join8(homedir7(), this.homeDirName, "tmp");
|
|
1443
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;
|
|
1463
|
+
}
|
|
1444
1464
|
async locate(opts = {}) {
|
|
1445
1465
|
const root = this.chatsRoot();
|
|
1446
1466
|
let projectDirs;
|
|
@@ -1449,8 +1469,10 @@ var GeminiFamilyAdapter = class {
|
|
|
1449
1469
|
} catch {
|
|
1450
1470
|
return [];
|
|
1451
1471
|
}
|
|
1472
|
+
const projectCwds = await this.loadProjectCwds();
|
|
1452
1473
|
const refs = [];
|
|
1453
1474
|
for (const pd of projectDirs) {
|
|
1475
|
+
const cwd = projectCwds.get(pd) ?? null;
|
|
1454
1476
|
const chatsDir = join8(root, pd, "chats");
|
|
1455
1477
|
let files;
|
|
1456
1478
|
try {
|
|
@@ -1468,8 +1490,8 @@ var GeminiFamilyAdapter = class {
|
|
|
1468
1490
|
continue;
|
|
1469
1491
|
}
|
|
1470
1492
|
if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
|
|
1471
|
-
if (opts.cwd) continue;
|
|
1472
|
-
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 });
|
|
1473
1495
|
}
|
|
1474
1496
|
}
|
|
1475
1497
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
@@ -2089,6 +2111,8 @@ function selectAdapters(opts) {
|
|
|
2089
2111
|
}
|
|
2090
2112
|
|
|
2091
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";
|
|
2092
2116
|
function normalizeRemote(url) {
|
|
2093
2117
|
const raw = url.trim();
|
|
2094
2118
|
if (!raw) return null;
|
|
@@ -2112,7 +2136,6 @@ function normalizeRemote(url) {
|
|
|
2112
2136
|
return null;
|
|
2113
2137
|
}
|
|
2114
2138
|
async function readGit(cwd) {
|
|
2115
|
-
const { stat: stat11 } = await import("fs/promises");
|
|
2116
2139
|
await stat11(`${cwd}/.git`);
|
|
2117
2140
|
const { execFile } = await import("child_process");
|
|
2118
2141
|
const { promisify } = await import("util");
|
|
@@ -2141,17 +2164,154 @@ async function readGit(cwd) {
|
|
|
2141
2164
|
commit
|
|
2142
2165
|
};
|
|
2143
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
|
+
}
|
|
2144
2302
|
async function resolveRepoAttrs(cwd) {
|
|
2145
2303
|
if (!cwd) return {};
|
|
2146
2304
|
const attrs = { [ATTR.CWD]: cwd };
|
|
2147
|
-
const
|
|
2305
|
+
const basename10 = (() => {
|
|
2148
2306
|
const trimmed = cwd.replace(/\/+$/g, "");
|
|
2149
2307
|
const idx = trimmed.lastIndexOf("/");
|
|
2150
2308
|
const name = idx === -1 ? trimmed : trimmed.slice(idx + 1);
|
|
2151
2309
|
return name || trimmed;
|
|
2152
2310
|
})();
|
|
2153
2311
|
try {
|
|
2154
|
-
const
|
|
2312
|
+
const gitRoot = await findGitRoot(cwd);
|
|
2313
|
+
if (!gitRoot) throw new Error("no git root");
|
|
2314
|
+
const { remote, branch, commit } = await readGit(gitRoot);
|
|
2155
2315
|
const normalized = remote ? normalizeRemote(remote) : null;
|
|
2156
2316
|
if (normalized) {
|
|
2157
2317
|
attrs[ATTR.SUBJECT_KEY] = normalized;
|
|
@@ -2164,9 +2324,46 @@ async function resolveRepoAttrs(cwd) {
|
|
|
2164
2324
|
if (commit) attrs[ATTR.GIT_COMMIT] = commit;
|
|
2165
2325
|
} catch {
|
|
2166
2326
|
}
|
|
2167
|
-
attrs[ATTR.SUBJECT_KEY] =
|
|
2327
|
+
attrs[ATTR.SUBJECT_KEY] = basename10;
|
|
2168
2328
|
return attrs;
|
|
2169
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
|
+
}
|
|
2170
2367
|
function stampRepoAttrs(spans, attrs) {
|
|
2171
2368
|
const keys = Object.keys(attrs);
|
|
2172
2369
|
if (keys.length === 0) return spans;
|
|
@@ -2181,8 +2378,9 @@ function stampRepoAttrs(spans, attrs) {
|
|
|
2181
2378
|
// src/session-source.ts
|
|
2182
2379
|
async function parseSession(adapter, ref) {
|
|
2183
2380
|
const spans = await adapter.parse(ref);
|
|
2184
|
-
const
|
|
2185
|
-
|
|
2381
|
+
const repo = await resolveSessionRepoAttrs(ref.cwd, spans);
|
|
2382
|
+
if (repo.cwd) ref.cwd = repo.cwd;
|
|
2383
|
+
stampRepoAttrs(spans, repo.attrs);
|
|
2186
2384
|
return spans;
|
|
2187
2385
|
}
|
|
2188
2386
|
async function* scanSessions(opts) {
|
|
@@ -2214,7 +2412,7 @@ async function* scanSessions(opts) {
|
|
|
2214
2412
|
// src/evidence.ts
|
|
2215
2413
|
import { mkdtemp, writeFile } from "fs/promises";
|
|
2216
2414
|
import { tmpdir } from "os";
|
|
2217
|
-
import { join as
|
|
2415
|
+
import { join as join13 } from "path";
|
|
2218
2416
|
function stringAttr(span2, key) {
|
|
2219
2417
|
const value = span2.attributes[key];
|
|
2220
2418
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
@@ -2234,7 +2432,8 @@ function repoFromSpans(spans) {
|
|
|
2234
2432
|
attrs.branch ??= stringAttr(span2, ATTR.GIT_BRANCH_NAME);
|
|
2235
2433
|
attrs.commit ??= stringAttr(span2, ATTR.GIT_COMMIT);
|
|
2236
2434
|
attrs.cwd ??= stringAttr(span2, ATTR.CWD);
|
|
2237
|
-
|
|
2435
|
+
attrs.resolutionSource ??= stringAttr(span2, ATTR.REPO_RESOLUTION_SOURCE);
|
|
2436
|
+
if (attrs.subjectKey && attrs.repository && attrs.branch && attrs.commit && attrs.cwd && attrs.resolutionSource) break;
|
|
2238
2437
|
}
|
|
2239
2438
|
return attrs;
|
|
2240
2439
|
}
|
|
@@ -2265,6 +2464,7 @@ async function buildPolicyEvidenceRecord(ref, spans, opts = {}) {
|
|
|
2265
2464
|
const loopLimit = opts.maxLoopExamples ?? 25;
|
|
2266
2465
|
const loopFindings = pipelines.stuckLoops.findings;
|
|
2267
2466
|
const { firstSpanAt, lastSpanAt } = timeBounds(spans);
|
|
2467
|
+
const repo = repoFromSpans(spans);
|
|
2268
2468
|
return {
|
|
2269
2469
|
schemaVersion: 1,
|
|
2270
2470
|
kind: "traces.policy_evidence.session",
|
|
@@ -2273,10 +2473,10 @@ async function buildPolicyEvidenceRecord(ref, spans, opts = {}) {
|
|
|
2273
2473
|
harness: ref.harness,
|
|
2274
2474
|
sessionId: ref.sessionId,
|
|
2275
2475
|
path: ref.path,
|
|
2276
|
-
cwd: ref.cwd,
|
|
2476
|
+
cwd: repo.cwd ?? ref.cwd,
|
|
2277
2477
|
mtimeMs: ref.mtimeMs
|
|
2278
2478
|
},
|
|
2279
|
-
repo
|
|
2479
|
+
repo,
|
|
2280
2480
|
metrics: {
|
|
2281
2481
|
spanCount: spans.length,
|
|
2282
2482
|
llmTurnCount: llmSpans.length,
|
|
@@ -2321,7 +2521,7 @@ function serializePolicyEvidence(records) {
|
|
|
2321
2521
|
`;
|
|
2322
2522
|
}
|
|
2323
2523
|
async function writePolicyEvidenceFile(records, outPath) {
|
|
2324
|
-
const path = outPath ??
|
|
2524
|
+
const path = outPath ?? join13(await mkdtemp(join13(tmpdir(), "traces-evidence-")), "policy-evidence.jsonl");
|
|
2325
2525
|
await writeFile(path, serializePolicyEvidence(records), "utf8");
|
|
2326
2526
|
return path;
|
|
2327
2527
|
}
|
|
@@ -2502,7 +2702,7 @@ async function applyRedactor(spans, redactor) {
|
|
|
2502
2702
|
import { createHash } from "crypto";
|
|
2503
2703
|
import { mkdtemp as mkdtemp2, readFile as readFile12, writeFile as writeFile2 } from "fs/promises";
|
|
2504
2704
|
import { tmpdir as tmpdir2 } from "os";
|
|
2505
|
-
import { join as
|
|
2705
|
+
import { join as join14 } from "path";
|
|
2506
2706
|
function isObject(value) {
|
|
2507
2707
|
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
2508
2708
|
}
|
|
@@ -2654,7 +2854,8 @@ function policyEvidenceToSpans(records) {
|
|
|
2654
2854
|
[ATTR.GIT_REPOSITORY]: record.repo.repository,
|
|
2655
2855
|
[ATTR.GIT_BRANCH_NAME]: record.repo.branch,
|
|
2656
2856
|
[ATTR.GIT_COMMIT]: record.repo.commit,
|
|
2657
|
-
[ATTR.CWD]: record.repo.cwd ?? record.session.cwd
|
|
2857
|
+
[ATTR.CWD]: record.repo.cwd ?? record.session.cwd,
|
|
2858
|
+
[ATTR.REPO_RESOLUTION_SOURCE]: record.repo.resolutionSource
|
|
2658
2859
|
};
|
|
2659
2860
|
return span({
|
|
2660
2861
|
traceId,
|
|
@@ -2873,11 +3074,308 @@ async function exportTraceEvidenceFile(inputPath, opts = {}) {
|
|
|
2873
3074
|
}
|
|
2874
3075
|
async function writeTraceEvidenceExportFile(inputPath, outPath, opts = {}) {
|
|
2875
3076
|
const result = await exportTraceEvidenceFile(inputPath, opts);
|
|
2876
|
-
const path = outPath ??
|
|
3077
|
+
const path = outPath ?? join14(await mkdtemp2(join14(tmpdir2(), "traces-export-")), "spans.openinference.jsonl");
|
|
2877
3078
|
await writeFile2(path, serializeSpans(result.spans), "utf8");
|
|
2878
3079
|
return { ...result, path };
|
|
2879
3080
|
}
|
|
2880
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
|
+
|
|
2881
3379
|
// src/observer.ts
|
|
2882
3380
|
var sleep = (ms, signal) => new Promise((resolve) => {
|
|
2883
3381
|
const onAbort = () => {
|
|
@@ -3042,14 +3540,236 @@ function analyzeReactions(spans) {
|
|
|
3042
3540
|
};
|
|
3043
3541
|
}
|
|
3044
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
|
+
|
|
3045
3765
|
// src/upload-state.ts
|
|
3046
3766
|
import { createHash as createHash2 } from "crypto";
|
|
3047
|
-
import { mkdir, readFile as
|
|
3767
|
+
import { mkdir, readFile as readFile15, writeFile as writeFile5 } from "fs/promises";
|
|
3048
3768
|
import { homedir as homedir11 } from "os";
|
|
3049
|
-
import { dirname, join as
|
|
3769
|
+
import { dirname as dirname3, join as join17 } from "path";
|
|
3050
3770
|
function stateFile() {
|
|
3051
|
-
const base = process.env.XDG_STATE_HOME ??
|
|
3052
|
-
return
|
|
3771
|
+
const base = process.env.XDG_STATE_HOME ?? join17(homedir11(), ".local", "state");
|
|
3772
|
+
return join17(base, "tangle-traces", "uploaded.json");
|
|
3053
3773
|
}
|
|
3054
3774
|
function uploadKey(harness, sessionId) {
|
|
3055
3775
|
return `${harness}:${sessionId}`;
|
|
@@ -3064,15 +3784,15 @@ function sessionHash(spans) {
|
|
|
3064
3784
|
}
|
|
3065
3785
|
async function loadState() {
|
|
3066
3786
|
try {
|
|
3067
|
-
return JSON.parse(await
|
|
3787
|
+
return JSON.parse(await readFile15(stateFile(), "utf8"));
|
|
3068
3788
|
} catch {
|
|
3069
3789
|
return {};
|
|
3070
3790
|
}
|
|
3071
3791
|
}
|
|
3072
3792
|
async function saveState(state) {
|
|
3073
3793
|
const f = stateFile();
|
|
3074
|
-
await mkdir(
|
|
3075
|
-
await
|
|
3794
|
+
await mkdir(dirname3(f), { recursive: true });
|
|
3795
|
+
await writeFile5(f, `${JSON.stringify(state, null, 2)}
|
|
3076
3796
|
`, "utf8");
|
|
3077
3797
|
}
|
|
3078
3798
|
function alreadyUploaded(state, harness, sessionId, hash) {
|
|
@@ -3080,10 +3800,10 @@ function alreadyUploaded(state, harness, sessionId, hash) {
|
|
|
3080
3800
|
}
|
|
3081
3801
|
|
|
3082
3802
|
// src/upload.ts
|
|
3083
|
-
import { readFile as
|
|
3803
|
+
import { readFile as readFile16 } from "fs/promises";
|
|
3084
3804
|
import { createRequire } from "module";
|
|
3085
3805
|
import { hostname } from "os";
|
|
3086
|
-
import { basename as
|
|
3806
|
+
import { basename as basename9, join as join18 } from "path";
|
|
3087
3807
|
import { hostedClientFromEnv } from "@tangle-network/agent-eval/hosted";
|
|
3088
3808
|
import { REDACTION_VERSION } from "@tangle-network/agent-eval/traces";
|
|
3089
3809
|
var require2 = createRequire(import.meta.url);
|
|
@@ -3107,7 +3827,7 @@ async function planUpload(opts = {}) {
|
|
|
3107
3827
|
async function gitBranch(cwd) {
|
|
3108
3828
|
if (!cwd) return void 0;
|
|
3109
3829
|
try {
|
|
3110
|
-
const head = await
|
|
3830
|
+
const head = await readFile16(join18(cwd, ".git", "HEAD"), "utf8");
|
|
3111
3831
|
const m = head.match(/^ref:\s*refs\/heads\/(.+)$/m);
|
|
3112
3832
|
return m?.[1]?.trim();
|
|
3113
3833
|
} catch {
|
|
@@ -3117,7 +3837,7 @@ async function gitBranch(cwd) {
|
|
|
3117
3837
|
async function sessionMeta(item, uploadedAt) {
|
|
3118
3838
|
const meta = {
|
|
3119
3839
|
[ATTR.HARNESS]: item.ref.harness,
|
|
3120
|
-
[ATTR.SESSION_FILE]:
|
|
3840
|
+
[ATTR.SESSION_FILE]: basename9(item.ref.path),
|
|
3121
3841
|
[ATTR.HOST]: hostname(),
|
|
3122
3842
|
[ATTR.UPLOADED_AT]: uploadedAt,
|
|
3123
3843
|
[ATTR.UPLOADER]: `tangle-traces@${TRACES_VERSION}`,
|
|
@@ -3255,6 +3975,7 @@ export {
|
|
|
3255
3975
|
selectAdapters,
|
|
3256
3976
|
normalizeRemote,
|
|
3257
3977
|
resolveRepoAttrs,
|
|
3978
|
+
resolveSessionRepoAttrs,
|
|
3258
3979
|
stampRepoAttrs,
|
|
3259
3980
|
parseSession,
|
|
3260
3981
|
scanSessions,
|
|
@@ -3275,9 +3996,19 @@ export {
|
|
|
3275
3996
|
exportTraceEvidenceText,
|
|
3276
3997
|
exportTraceEvidenceFile,
|
|
3277
3998
|
writeTraceEvidenceExportFile,
|
|
3999
|
+
inspectSessionIndex,
|
|
4000
|
+
renderInspectionReport,
|
|
4001
|
+
serializeInspectionReport,
|
|
4002
|
+
readSessionIndexFile,
|
|
4003
|
+
writeInspectionReportFile,
|
|
3278
4004
|
watchSessions,
|
|
3279
4005
|
REACTIONS,
|
|
3280
4006
|
analyzeReactions,
|
|
4007
|
+
buildSessionIndex,
|
|
4008
|
+
buildSessionIndexFromRows,
|
|
4009
|
+
collectSessionIndex,
|
|
4010
|
+
serializeSessionIndex,
|
|
4011
|
+
writeSessionIndexFile,
|
|
3281
4012
|
uploadKey,
|
|
3282
4013
|
sessionHash,
|
|
3283
4014
|
loadState,
|