pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
3,032 lines • 101 kB
JavaScript
#!/usr/bin/env node
/**
* Analyze pi-lens' own logs for operational/code-quality smells across all projects.
*
* Sources:
* ~/.pi-lens/latency*.log JSONL dispatch/runner/phase timings
* ~/.pi-lens/sessionstart*.log text lifecycle/tool availability logs
* ~/.pi-lens/cascade*.log JSONL impact-cascade logs
* ~/.pi-lens/read-guard*.log JSONL read-guard friction logs
* ~/.pi-lens/tree-sitter*.log JSONL structural runner logs
* ~/.pi-lens/extension*.log JSONL extension diagnostics and console-guard telemetry
* ~/.pi-lens/actionable-warnings*.log JSONL advisory pipeline (inject/suppress)
* ~/.pi-lens/ast-grep-tools*.log JSONL MCP ast-grep search/replace telemetry
* ~/.pi-lens/logs/*.jsonl JSONL diagnostic findings
* ~/.pi-lens/projects/<slug>/worklog.jsonl JSONL fix worklog (rule/tool/model/provider, #1448)
*/
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import readline from "node:readline";
import { fileURLToPath } from "node:url";
import { minimatch } from "minimatch";
const DEFAULT_ROOT = path.join(os.homedir(), ".pi-lens");
const DEFAULT_SINCE = "2d";
const DEFAULT_LIMIT = 12;
const DEFAULT_EXCLUDE_GLOBS = [
"**/AppData/Local/Temp/claude/**",
"**/heap-corpus*/**",
"**/.plegma/work/**",
];
const args = parseArgs(process.argv.slice(2));
const root = path.resolve(expandHome(args.root ?? DEFAULT_ROOT));
const since = parseSince(args.since ?? DEFAULT_SINCE);
const limit =
Number.parseInt(args.limit ?? `${DEFAULT_LIMIT}`, 10) || DEFAULT_LIMIT;
const outputJson = Boolean(args.json);
const includeArchived = Boolean(args.archived);
const excludeGlobs = [
...DEFAULT_EXCLUDE_GLOBS,
...(Array.isArray(args.exclude)
? args.exclude
: args.exclude
? [args.exclude]
: []),
];
const thresholds = {
startupSlowMs: Number.parseInt(args.startupSlowMs ?? "500", 10),
backgroundSlowMs: Number.parseInt(args.backgroundSlowMs ?? "3000", 10),
totalSlowMs: Number.parseInt(args.totalSlowMs ?? "5000", 10),
runnerSlowMs: Number.parseInt(args.runnerSlowMs ?? "2500", 10),
cascadeGraphSlowMs: Number.parseInt(args.cascadeGraphSlowMs ?? "1000", 10),
};
main().catch((err) => {
console.error(`log-smell analysis failed: ${err?.stack || err}`);
process.exitCode = 1;
});
async function main() {
if (args.help) {
printHelp();
return;
}
const files = discoverLogFiles(root, includeArchived);
const state = createState(files);
await Promise.all([
analyzeLatency(files.latency, state),
analyzeDiagnosticLogs(files.diagnostics, state),
analyzeCascade(files.cascade, state),
analyzeReadGuard(files.readGuard, state),
analyzeTreeSitter(files.treeSitter, state),
analyzeSessionStart(files.sessionStart, state),
analyzeActionableWarnings(files.actionableWarnings, state),
analyzeAstGrepTools(files.astGrepTools, state),
analyzeWorklog(files.worklog, state),
analyzeExtension(files.extension, state),
]);
const report = buildReport(state);
if (outputJson) {
console.log(JSON.stringify(report, null, 2));
} else {
printReport(report);
}
}
function parseArgs(argv) {
const out = {};
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (arg === "--help" || arg === "-h") out.help = true;
else if (arg === "--json") out.json = true;
else if (arg === "--archived") out.archived = true;
else if (arg.startsWith("--")) {
const key = arg.slice(2);
const next = argv[i + 1];
if (!next || next.startsWith("--")) out[key] = "true";
else if (key === "exclude") {
const value = argv[++i];
out.exclude = out.exclude
? Array.isArray(out.exclude)
? [...out.exclude, value]
: [out.exclude, value]
: value;
} else out[key] = argv[++i];
}
}
return out;
}
function printHelp() {
console.log(
`Usage: node scripts/analyze-pi-lens-logs.mjs [options]\n\nOptions:\n --since <2d|24h|YYYY-MM-DD|all> Time window (default: ${DEFAULT_SINCE})\n --root <dir> pi-lens log root (default: ~/.pi-lens)\n --limit <n> Top-N rows per section (default: ${DEFAULT_LIMIT})\n --archived Include archived rotated logs too\n --json Emit machine-readable JSON\n\nThresholds:\n --startupSlowMs <n> session_start total threshold (default: ${thresholds.startupSlowMs})\n --backgroundSlowMs <n> session_start background task threshold (default: ${thresholds.backgroundSlowMs})\n --totalSlowMs <n> tool total phase threshold (default: ${thresholds.totalSlowMs})\n --runnerSlowMs <n> runner duration threshold (default: ${thresholds.runnerSlowMs})\n --cascadeGraphSlowMs <n> cascade graph build threshold (default: ${thresholds.cascadeGraphSlowMs})\n`,
);
}
function parseSince(value) {
if (!value || value === "all") return null;
const now = Date.now();
const rel = /^(\d+)([hdw])$/.exec(value);
if (rel) {
const amount = Number.parseInt(rel[1], 10);
const unitMs =
rel[2] === "h" ? 3600_000 : rel[2] === "d" ? 86_400_000 : 7 * 86_400_000;
return new Date(now - amount * unitMs);
}
const date = new Date(value);
if (!Number.isNaN(date.getTime())) return date;
throw new Error(`Invalid --since value: ${value}`);
}
function expandHome(input) {
return input.startsWith("~")
? path.join(os.homedir(), input.slice(1))
: input;
}
function discoverLogFiles(logRoot, archived) {
const allRootFiles = safeReaddir(logRoot).map((name) =>
path.join(logRoot, name),
);
const logsDir = path.join(logRoot, "logs");
const dailyLogs = safeReaddir(logsDir)
.filter((name) => name.endsWith(".jsonl"))
.map((name) => path.join(logsDir, name));
// worklog.jsonl lives per-project under projects/<slug>/ (getProjectDataDir),
// not at the log root — walk one level to find every project's file (#1448).
const projectsDir = path.join(logRoot, "projects");
const worklogs = safeReaddir(projectsDir)
.map((slug) => path.join(projectsDir, slug, "worklog.jsonl"))
.filter((file) => fs.existsSync(file));
const byPrefix = (prefix) =>
allRootFiles.filter((file) => {
const base = path.basename(file);
if (!base.startsWith(prefix)) return false;
if (!archived && /\.\d{4}-\d{2}-\d{2}T/.test(base)) return false;
return base.endsWith(".log") || base.includes(".log.");
});
return {
latency: chronologicalFiles(byPrefix("latency")),
sessionStart: chronologicalFiles(byPrefix("sessionstart")),
cascade: chronologicalFiles(byPrefix("cascade")),
readGuard: chronologicalFiles(byPrefix("read-guard")),
treeSitter: chronologicalFiles(byPrefix("tree-sitter")),
actionableWarnings: chronologicalFiles(byPrefix("actionable-warnings")),
astGrepTools: chronologicalFiles(byPrefix("ast-grep-tools")),
extension: chronologicalFiles(byPrefix("extension")),
diagnostics: dailyLogs,
worklog: worklogs,
};
}
/**
* Rotated siblings (`x.log.1`) carry older rows than the active `x.log`. The
* run-splitting detectors (D3/D4/D5) and the read-guard evidence walk (D10)
* only read correctly in chronological order, so order every multi-file stream
* oldest-first with the active file last.
*/
function chronologicalFiles(files) {
return [...files].sort((a, b) => {
const activeA = path.basename(a).endsWith(".log");
const activeB = path.basename(b).endsWith(".log");
if (activeA !== activeB) return activeA ? 1 : -1;
return a.localeCompare(b);
});
}
function safeReaddir(dir) {
try {
return fs.readdirSync(dir);
} catch {
return [];
}
}
function createState(files) {
return {
window: { since: since?.toISOString() ?? "all", root },
files,
parseErrors: counter(),
excludedRows: 0,
seen: counter(),
projects: counter(),
smellTotals: counter(),
latency: {
testRunnerVerdicts: new Map(),
runnerStatus: counter(),
runnerFailureKinds: counter(),
runnerBlockingFindings: counter(),
runnerFailures: [],
slowRunners: [],
slowTotals: [],
toolResults: counter(),
phaseCounts: counter(),
phaseTimeouts: counter(),
// D1: every latency row timestamp (ms) in window, for the coverage-gap
// scan. Sorted after all files are read.
rowTs: [],
cascadeTs: [],
// D2: real-log pollution — rows whose filePath points at a test
// home, grouped by pid.
scratchRows: new Map(),
// D5: test_runner_delivery outcomes per session, joined with the
// sessionstart firing/stale text into one delivery-health verdict.
testRunnerDelivery: new Map(),
// D6: `phase: knip` executed rows per pid, for the drift and cost rules.
knip: new Map(),
// D7: hook-await-exceeded overruns plus the per-pid degradation census.
hookAwaitExceeded: [],
degradationKinds: new Map(),
// D8: turn_end `tool_result` summaries per pid, and the retained-state text.
turnEndTools: new Map(),
retainingNewerTurn: [],
// D9: empty-candidate LSP waits, edit touches that saw no client, and
// warm-reuse selections, joined into the wait/contradiction verdict.
lspWaitEmpty: new Map(),
lspTouchEdit: new Map(),
lspTouchNoClients: [],
lspClientSelectedWarm: [],
// D11: read-set carry outcomes at a restart boundary.
carryRestart: [],
// D12: nudges and scope transitions, joined by pid + wall clock.
agentNudges: [],
sessionScopeTransitions: [],
// D13: deferred runner failures joined to their delivery row by (pid, turnId).
deferredRunnerFailed: [],
lateRunnerFindings: [],
// D14: auxiliary stuck pair (pid, filePath, serverId) recurrence.
auxStuck: new Map(),
// D15: advisory provenance decisions with unknown provenance.
advisoryProvenance: [],
// D8/D15: first latency row timestamp per pid (lifetime base).
pidFirstTs: new Map(),
pidLastTs: new Map(),
workspace: {
started: 0,
completed: 0,
aborted: 0,
timedOutSweeps: 0,
timedOutFilesTotal: 0,
// #1618: per-reason breakdown of the same total. Populated from each
// sweep's `unconfirmedByReason` metadata when present; a sweep logged
// by a pre-#1618 build carries no such field, so its files are
// bucketed under "budget (pre-#1618 build)" — the ABSENT field is
// itself the vintage signal, not an assumption that every one of
// those files really was a budget timeout.
unconfirmedByReason: counter(),
sweeps: [],
progress: [],
},
},
// #2526: the config stack's positive-observability rows. `resolved`
// counts `config_resolved` phase records; `legacyWithoutRecords` counts
// the rows that carry a deprecated document but produced no
// migration/notice record — the deprecation machinery gone silent.
//
// Round 2, F2: the session->row relation is a JOIN on the session id both
// sides carry, not a subtraction of two counts.
//
// Round 3, S1: the session-side half of the join is no longer a
// PREDICTION. `pendingSessions` holds every session id that published a
// `config_resolution_pending` mark — written by `loadLSPConfig` itself,
// at the instant a resolution is actually attempted, never by a
// `runtime-session.ts` handler guessing from `no-lsp`/subagent/warm-attach
// flags ahead of time. A session that never reaches that call (quick or
// minimal mode's second-and-later session in a process, for the same
// root) never gets a mark, so it is silently excluded rather than
// counted against. `resolvedSessions` holds every session id a
// resolution actually reported, from EITHER sink (the latency row or the
// sessionstart line — one call writes both, so this is one fact
// surviving an independent rotation of either log, not a second source
// of truth).
config: {
resolved: 0,
pendingSessions: new Map(),
resolvedSessions: new Set(),
legacyDocuments: 0,
legacyWithoutRecords: 0,
examples: [],
},
diagnostics: {
bySeverity: counter(),
byTool: counter(),
byRule: counter(),
byFile: counter(),
shownInline: counter(),
errors: [],
topMessages: counter(),
},
cascade: {
phases: counter(),
slowGraphs: [],
fallbacks: [],
missingSnapshots: counter(),
noNeighbors: counter(),
largeFanout: [],
},
readGuard: {
events: counter(),
byFile: counter(),
byReason: counter(),
preflightReasons: counter(),
snapshotStatus: counter(),
snapshotEnforcement: counter(),
blocked: [],
oldTextIssues: [],
staleRanges: [],
zeroReads: [],
unavailableSnapshots: [],
// E2: the block/warn split. These rows do not carry a host-side
// decision, so the analyzer must not invent one.
blocks: [],
warns: [],
blockByKind: counter(),
bypassedMismatch: 0,
// D10: per-(session,file) evidence that a read or a committed edit
// happened earlier in the session, so a later `zero_read` block is a
// lost read-set rather than a genuine cold edit.
fileEvidence: new Map(),
stateLost: [],
genuineZeroRead: [],
},
extension: {
// D2: extension.log rows whose payload names a test home
// directory, grouped by pid, plus the warn/error census.
scratchRows: new Map(),
warnErrorGroups: counter(),
},
treeSitter: {
phases: counter(),
failures: [],
blocking: [],
highDiagnostics: [],
queryCache: counter(),
riskFlags: counter(),
},
session: {
starts: 0,
cwds: counter(),
// E4: per-start build identity (`commit=`), so every rate can be split
// by build. Keyed by commit, with the start timestamps that carried it.
commits: counter(),
slowStarts: [],
slowTasks: [],
toolNoise: counter(),
lspNoise: counter(),
errors: [],
// D1: every sessionstart line timestamp (ms) in window.
rowTs: [],
// D16: `pi-lens loaded: Nms` lines, with their wall clock so a short
// lived pid can be marked.
slowLoads: [],
// D16: the wall clock of every `session_start fired`, so a load with no
// session start within 60 s (a short-lived pid) is visible.
firedTs: [],
// D3/D4/D5/D8: one run per `session_start fired`. Run splitting is the
// only way to attribute a turn-end test decision to the session that made it.
runs: [],
currentRun: newRun(),
},
actionable: {
events: counter(),
reports: 0,
injected: 0,
injectedAdvisories: 0,
suppressed: 0,
autoFixEligible: 0,
lspSource: counter(),
fileSkipReasons: counter(),
errors: [],
},
astGrep: {
outcomes: counter(),
errorKinds: counter(),
truncated: 0,
calls: 0,
errors: [],
slow: [],
},
worklog: {
// key: "<rule>||<model>" (model "" when the entry predates #1448 or the
// runtime didn't know it) -> { total, autoFixed }
byRuleModel: new Map(),
byModel: counter(),
byModelAutoFixed: counter(),
byProvider: counter(),
},
};
}
/**
* The failure kind of a `failed` or `crashed` runner row. `metadata.failureKind`
* is exact: inline rows carry it (#3781) and collect-later rows too (#3796).
* Rows written by released versions never carry it, so the count heuristic
* (a failure with diagnostics is found-errors) stays only as that fallback.
*/
function runnerFailureKind(entry) {
if (entry.metadata?.failureKind) return entry.metadata.failureKind;
if (entry.status === "crashed") return "crashed";
return (entry.diagnosticCount ?? 0) > 0 ? "blocking_diagnostics" : "unknown";
}
async function analyzeLatency(files, state) {
for (const file of files) {
await forEachJsonLine(file, "latency", state, (entry) => {
const ts = dateOf(entry.ts);
if (!inWindow(ts)) return;
state.seen.inc("latency");
trackLatencyProject(state, entry);
trackLatencySignals(state, entry, ts);
if (entry.type === "runner") {
const status = entry.status ?? "unknown";
const runner = entry.runnerId ?? "unknown";
state.latency.runnerStatus.inc(`${runner}:${status}`);
if (status === "failed" || status === "crashed") {
// Separate a genuine runner breakage from "the check ran and found
// blocking issues" (e.g. the LSP runner reports status:failed when a
// file has type errors).
const kind = runnerFailureKind(entry);
state.latency.runnerFailureKinds.inc(`${runner}:${kind}`);
if (kind === "blocking_diagnostics") {
state.latency.runnerBlockingFindings.inc(runner);
} else {
state.smellTotals.inc("runner-failures");
pushTop(
state.latency.runnerFailures,
summarizeLatency(entry),
limit * 3,
byDuration,
);
}
}
if (
(entry.durationMs ?? 0) >= thresholds.runnerSlowMs &&
status !== "skipped"
) {
state.smellTotals.inc("slow-runners");
pushTop(
state.latency.slowRunners,
summarizeLatency(entry),
limit * 3,
byDuration,
);
}
} else if (entry.type === "phase") {
const phase = entry.phase ?? "unknown";
state.latency.phaseCounts.inc(phase);
if (phase.endsWith("_timeout")) state.latency.phaseTimeouts.inc(phase);
if (phase === "test_runner_verdict_delivery") {
const sessionId = entry.metadata?.sessionId ?? "unknown";
const summary = state.latency.testRunnerVerdicts.get(sessionId) ?? {
total: 0,
stale: 0,
unknown: 0,
};
const verdictCount = entry.metadata?.verdictCount;
const staleCount = entry.metadata?.staleCount;
const unknownCount = entry.metadata?.unknownCount;
summary.total += typeof verdictCount === "number" ? verdictCount : 1;
if (typeof staleCount === "number") summary.stale += staleCount;
else if (entry.metadata?.stale === true) summary.stale += 1;
if (typeof unknownCount === "number") summary.unknown += unknownCount;
state.latency.testRunnerVerdicts.set(sessionId, summary);
}
if (phase === "config_resolved") {
// #2526. Counted here rather than derived from phaseCounts so the
// legacy/record cross-check reads the same row it counts.
state.config.resolved += 1;
const md = entry.metadata ?? {};
// #2526 R2 F2 / #2552 R4: the join key is (session, root), not
// session alone — see `configJoinKey`'s doc comment. `entry.filePath`
// is this row's root (the same `configResolutionKey(cwd)` value the
// pending mark's `root=` carries); a row from a build that predates
// the session id carries none, and simply joins to nothing.
if (
typeof md.sessionId === "string" &&
md.sessionId.length > 0 &&
typeof entry.filePath === "string" &&
entry.filePath.length > 0
) {
state.config.resolvedSessions.add(
configJoinKey(md.sessionId, entry.filePath),
);
}
const documents = Array.isArray(md.documents) ? md.documents : [];
const legacy = documents.filter((doc) => doc?.legacy === true);
state.config.legacyDocuments += legacy.length;
// A deprecated document that produced NO record means the
// migration notices went silent — the user is on a removal
// schedule and is never told. Zero records with zero legacy
// documents is the correct canonical-only answer, not a smell.
if (legacy.length > 0 && Number(md.recordCount ?? 0) === 0) {
state.config.legacyWithoutRecords += 1;
pushTop(
state.config.examples,
summarizeConfigResolved(entry, "legacy document with 0 records"),
limit * 3,
byDuration,
);
}
}
if (
phase === "total" &&
(entry.durationMs ?? 0) >= thresholds.totalSlowMs
) {
state.smellTotals.inc("slow-hook-path");
pushTop(
state.latency.slowTotals,
summarizeLatency(entry),
limit * 3,
byDuration,
);
} else if (phase === "lsp_workspace_diagnostics_start") {
state.latency.workspace.started += 1;
} else if (phase === "lsp_workspace_diagnostics_progress") {
// Keep the heartbeats so an incomplete sweep can show how far it
// got (completed X/Y) before it went silent — the hang forensics.
pushTop(
state.latency.workspace.progress,
summarizeWorkspaceSweep(entry),
limit * 3,
byDuration,
);
} else if (phase === "lsp_workspace_diagnostics") {
const ws = state.latency.workspace;
ws.completed += 1;
if (entry.metadata?.aborted) ws.aborted += 1;
const timedOut = Number(entry.metadata?.timedOutFiles ?? 0);
if (timedOut > 0) {
ws.timedOutSweeps += 1;
ws.timedOutFilesTotal += timedOut;
// #1618: attribute by the REAL per-reason tally when the build
// that wrote this log line has it — a service-destroyed file
// must never count toward "hit the per-file budget" the way it
// used to (the forensics tool that found #1618 in the first
// place read this exact field and mis-blamed budget exhaustion
// for what were mostly service-destroyed files).
const byReason = entry.metadata?.unconfirmedByReason;
if (byReason && typeof byReason === "object") {
for (const [reason, count] of Object.entries(byReason)) {
const n = Number(count) || 0;
if (n > 0) ws.unconfirmedByReason.inc(reason, n);
}
} else {
// Vintage-attribution fallback: no `unconfirmedByReason` means
// this line predates #1618 — the absent field IS the signal,
// not proof every file here was really a budget timeout.
ws.unconfirmedByReason.inc("budget (pre-#1618 build)", timedOut);
}
state.smellTotals.inc("lsp-workspace-file-timeouts");
pushTop(
ws.sweeps,
summarizeWorkspaceSweep(entry),
limit * 3,
byDuration,
);
}
}
} else if (entry.type === "tool_result") {
state.latency.toolResults.inc(entry.result ?? "unknown");
}
});
}
}
async function analyzeDiagnosticLogs(files, state) {
for (const file of files) {
await forEachJsonLine(file, "diagnostics", state, (entry) => {
const ts = dateOf(entry.timestamp ?? entry.ts);
if (!inWindow(ts)) return;
state.seen.inc("diagnostics");
trackProject(state, entry.filePath);
const severity = entry.severity ?? "unknown";
const tool = entry.tool ?? "unknown";
const rule = entry.ruleId ?? entry.rule ?? "unknown";
const filePath = shortPath(entry.filePath);
state.diagnostics.bySeverity.inc(severity);
state.diagnostics.byTool.inc(tool);
state.diagnostics.byRule.inc(`${tool}/${rule}`);
state.diagnostics.byFile.inc(filePath);
state.diagnostics.shownInline.inc(
`${Boolean(entry.shownInline)}:${Boolean(entry.shownToAgent)}:${Boolean(entry.unresolved)}`,
);
state.diagnostics.topMessages.inc(
`${tool}/${rule}: ${normalizeMessage(entry.message)}`,
);
if (
severity === "error" ||
entry.shownToAgent === true ||
entry.shownInline === true
) {
state.smellTotals.inc("diagnostic-blockers");
pushTop(
state.diagnostics.errors,
summarizeDiagnostic(entry),
limit * 4,
bySeverityThenLine,
);
}
});
}
}
async function analyzeCascade(files, state) {
for (const file of files) {
await forEachJsonLine(file, "cascade", state, (entry) => {
const ts = dateOf(entry.ts);
if (!inWindow(ts)) return;
state.seen.inc("cascade");
if (ts) state.latency.cascadeTs.push(ts.getTime());
trackProject(state, entry.filePath);
const phase = entry.phase ?? "unknown";
state.cascade.phases.inc(phase);
if ((entry.graphBuiltMs ?? 0) >= thresholds.cascadeGraphSlowMs) {
state.smellTotals.inc("cascade-slow-graphs");
pushTop(
state.cascade.slowGraphs,
summarizeCascade(entry),
limit * 3,
(a, b) => b.graphBuiltMs - a.graphBuiltMs,
);
}
if (entry.fallbackUsed || phase === "neighbor_fallback" || entry.error) {
state.smellTotals.inc("cascade-fallbacks");
pushTop(
state.cascade.fallbacks,
summarizeCascade(entry),
limit * 3,
byDuration,
);
}
if (entry.snapshotMissing)
state.cascade.missingSnapshots.inc(
shortPath(entry.neighborFile ?? entry.filePath),
);
if (entry.metadata?.noNeighbors)
state.cascade.noNeighbors.inc(projectOf(entry.filePath));
if (
(entry.totalNeighborCount ?? 0) >= 20 ||
(entry.neighborCount ?? 0) >= 10
) {
pushTop(
state.cascade.largeFanout,
summarizeCascade(entry),
limit * 3,
(a, b) => (b.totalNeighborCount ?? 0) - (a.totalNeighborCount ?? 0),
);
}
});
}
}
async function analyzeReadGuard(files, state) {
for (const file of files) {
await forEachJsonLine(file, "read-guard", state, (entry) => {
const ts = dateOf(entry.ts);
if (!inWindow(ts)) {
// D10 needs evidence that may precede a live-monitor window.
// Keep this lookback state private; outside rows never affect counts.
if (hasReadEvidence(entry)) {
const key = `${readGuardSessionOf(entry) ?? "?"}\u0000${entry.filePath ?? "?"}`;
state.readGuard.fileEvidence.set(key, true);
}
return;
}
state.seen.inc("read-guard");
trackProject(state, entry.filePath);
const event = entry.event ?? "unknown";
const md = entry.metadata ?? {};
state.readGuard.events.inc(event);
state.readGuard.byFile.inc(shortPath(entry.filePath));
const reasonKind = md.reasonKind;
if (reasonKind) state.readGuard.byReason.inc(reasonKind);
// D10: evidence that this file was read or edited earlier in the
// SAME session. Read before this row's own evidence is recorded, so a
// later `zero_read` block sees only the prior rows. An
// `edit_batch_summary` carries no sessionId, so its session is the
// turnId prefix before the last `:`.
const evidenceKey = `${readGuardSessionOf(entry) ?? "?"}\u0000${entry.filePath ?? "?"}`;
const hadEvidence =
state.readGuard.fileEvidence.get(evidenceKey) === true;
if (event === "edit_preflight_blocked") {
state.readGuard.preflightReasons.inc(reasonKind ?? "unknown");
registerBlock(state, entry, "preflight");
}
if (event === "edit_blocked") {
const summary = registerBlock(state, entry, "blocked");
if (reasonKind === "zero_read") {
pushTop(state.readGuard.zeroReads, summary, limit * 3, byLine);
if (hadEvidence) {
pushTop(
state.readGuard.stateLost,
{ ...summary, classification: "state-lost" },
limit * 3,
byLine,
);
} else {
pushTop(
state.readGuard.genuineZeroRead,
{ ...summary, classification: "genuine" },
limit * 3,
byLine,
);
}
}
}
if (event === "edit_warned") {
pushTop(
state.readGuard.warns,
summarizeReadGuard(entry),
limit * 3,
byLine,
);
}
if (event === "range_snapshot_validation") {
const status = md.status ?? "unknown";
const outcome = String(md.outcome ?? "");
state.readGuard.snapshotStatus.inc(status);
state.readGuard.snapshotEnforcement.inc(
md.enforced ? "enforced" : "not_enforced",
);
// E3: `bypassed-content-match` is an allowed edit, not a stale
// read. Only a mismatch the caller did NOT bypass is a smell.
if (status === "mismatch" && !outcome.startsWith("bypassed")) {
state.smellTotals.inc("read-guard-stale-ranges");
pushTop(
state.readGuard.staleRanges,
summarizeReadGuard(entry),
limit * 3,
byLine,
);
} else if (status === "mismatch") {
state.readGuard.bypassedMismatch += 1;
} else if (status === "unavailable") {
pushTop(
state.readGuard.unavailableSnapshots,
summarizeReadGuard(entry),
limit * 3,
byLine,
);
}
}
if (
event === "oldtext_not_found" ||
event === "oldtext_duplicate" ||
event === "touched_lines_missing"
) {
pushTop(
state.readGuard.oldTextIssues,
summarizeReadGuard(entry),
limit * 3,
byLine,
);
}
if (hasReadEvidence(entry))
state.readGuard.fileEvidence.set(evidenceKey, true);
});
}
}
/** D10: a row proving the file was read or edited earlier in its session. */
function hasReadEvidence(entry) {
const md = entry.metadata ?? {};
return (
entry.event === "edit_batch_summary" ||
(entry.event === "range_snapshot_validation" &&
Number(md.candidateReadCount ?? 0) > 0) ||
(entry.event === "edit_warned" && Number(md.readCount ?? 0) > 0)
);
}
/**
* D10: the session a read-guard row belongs to. `edit_batch_summary` rows carry
* no `sessionId`, so fall back to the turnId prefix before the last `:` (the
* report's own join instruction).
*/
function readGuardSessionOf(entry) {
if (typeof entry.sessionId === "string" && entry.sessionId)
return entry.sessionId;
const turnId = entry.turnId;
if (typeof turnId === "string" && turnId.includes(":"))
return turnId.slice(0, turnId.lastIndexOf(":"));
return null;
}
/** E2: one blocked edit. Host/model provenance is not present on this row. */
function registerBlock(state, entry, source) {
const summary = summarizeReadGuard(entry);
pushTop(state.readGuard.blocks, summary, limit * 3, byLine);
state.readGuard.blockByKind.inc(
`${source}:${entry.metadata?.reasonKind ?? "unknown"}`,
);
return summary;
}
async function analyzeTreeSitter(files, state) {
for (const file of files) {
await forEachJsonLine(file, "tree-sitter", state, (entry) => {
const ts = dateOf(entry.ts);
if (!inWindow(ts)) return;
state.seen.inc("tree-sitter");
trackProject(state, entry.filePath);
const phase = entry.phase ?? "unknown";
state.treeSitter.phases.inc(phase);
if (phase === "queries_loaded") {
state.treeSitter.queryCache.inc(entry.cacheHit ? "hit" : "miss");
}
if (
entry.status &&
entry.status !== "succeeded" &&
entry.status !== "skipped"
) {
state.smellTotals.inc("tree-sitter-failures");
pushTop(
state.treeSitter.failures,
summarizeTreeSitter(entry),
limit * 3,
byDiagnostics,
);
}
if ((entry.blocking ?? 0) > 0) {
state.smellTotals.inc("tree-sitter-blocking");
pushTop(
state.treeSitter.blocking,
summarizeTreeSitter(entry),
limit * 3,
byDiagnostics,
);
}
if ((entry.diagnostics ?? 0) >= 20) {
pushTop(
state.treeSitter.highDiagnostics,
summarizeTreeSitter(entry),
limit * 3,
byDiagnostics,
);
}
for (const flag of entry.metadata?.riskFlags ?? [])
state.treeSitter.riskFlags.inc(flag);
});
}
}
async function analyzeSessionStart(files, state) {
const lineRe = /^\[([^\]]+)\]\s*(.*)$/;
for (const file of files) {
await forEachLine(file, async (line) => {
const match = lineRe.exec(line);
if (!match) return;
const ts = dateOf(match[1]);
const message = match[2];
if (!inWindow(ts)) {
// A --since window can begin after the session_start that owns its
// rows. Carry only that run anchor into the visible window.
if (message.startsWith("session_start fired")) {
state.session.currentRun = newRun();
state.session.currentRun.startTs = iso(ts);
}
return;
}
if (isExcludedText(message)) {
state.excludedRows++;
return;
}
state.seen.inc("sessionstart");
if (ts) state.session.rowTs.push(ts.getTime());
// E4: starts are counted from `session_start fired`, which every mode
// writes, instead of the dead `session_start cwd:` full-mode line. The
// same line opens a D3/D4/D5/D8 run.
if (message.startsWith("session_start fired")) {
state.session.starts++;
if (ts) state.session.firedTs.push(ts.getTime());
state.session.runs.push(state.session.currentRun);
state.session.currentRun = newRun();
state.session.currentRun.startTs = iso(ts);
}
const cwd = /session_start cwd:\s*(.*)$/.exec(message)?.[1];
if (cwd) {
state.session.cwds.inc(projectOf(cwd));
trackProject(state, cwd);
}
// E4: build identity for the session that just started.
const buildIdentity =
/session_start: build identity [—-] commit=(\w+)/.exec(message);
if (buildIdentity) {
state.session.commits.inc(buildIdentity[1]);
state.session.currentRun.commit = buildIdentity[1];
}
// #2526 R3 S1: this session's config-resolution PENDING mark, published
// by `loadLSPConfig` itself at the instant a resolution is actually
// attempted — never a start-line prediction from `no-lsp`/subagent/
// warm-attach flags. A session that never calls `loadLSPConfig` (quick
// or minimal mode's second-and-later session in a process, for the
// same root) never writes this line, and is correctly absent from the
// join rather than counted against.
//
// #2552 R4: `root=` is REQUIRED in the match — a warm MCP process keeps
// one session id for its whole life but marks once per served root
// (`configJoinKey`'s doc comment), so a line missing it cannot be
// attributed to a specific root and is left out of the join rather than
// guessed into a wrong one.
const pending =
/session_start config_resolution_pending session=(\S+) root=(.*)$/.exec(
message,
);
if (pending) {
const [, sessionId, root] = pending;
state.config.pendingSessions.set(configJoinKey(sessionId, root), {
ts: iso(ts),
sessionId,
root,
});
}
// The loader's own line is the second half of the join, so a rotated
// latency.log cannot manufacture a deficit on its own.
const resolvedSession =
/config resolved .*\bsession=(\S+) root=(.*)$/.exec(message);
if (resolvedSession) {
const [, sessionId, root] = resolvedSession;
state.config.resolvedSessions.add(configJoinKey(sessionId, root));
}
const total = /session_start total:\s*(\d+)ms/.exec(message);
if (total && Number(total[1]) >= thresholds.startupSlowMs) {
state.smellTotals.inc("slow-session-start");
pushTop(
state.session.slowStarts,
{ ts: iso(ts), durationMs: Number(total[1]), message },
limit * 3,
byDuration,
);
}
// Runtime logs "success runMs=<n> queuedMs=<n>"; older logs used
// "success (<n>ms)". Match both so a format drift can't silently zero
// this smell again (it did: the `(<n>ms)` regex matched 0 of ~2k rows).
const task =
/session_start task ([^:]+): success runMs=(\d+)/.exec(message) ??
/session_start task ([^:]+): success \((\d+)ms\)/.exec(message);
if (task && Number(task[2]) >= thresholds.backgroundSlowMs) {
state.smellTotals.inc("slow-background-tasks");
pushTop(
state.session.slowTasks,
{ ts: iso(ts), task: task[1], durationMs: Number(task[2]), message },
limit * 3,
byDuration,
);
}
// D16: extension load time is invisible to `slow-session-start`, which
// reads `session_start total:` (a quick-mode total of tens of ms).
const load = /pi-lens loaded: (\d+)ms after process start/.exec(message);
if (load) {
state.session.slowLoads.push({
ts: iso(ts),
ms: ts ? ts.getTime() : null,
durationMs: Number(load[1]),
message,
});
}
const run = state.session.currentRun;
const modified = /turn_end: (\d+) file\(s\) modified/.exec(message);
if (modified) run.edits += 1;
if (message.includes("excluded by the built-in turn-end policy")) {
run.excluded += 1;
const excludedPath = /turn_end:\s*(\S+)\s+→ test target excluded/.exec(
message,
)?.[1];
if (excludedPath) run.excludedTestFiles.push(excludedPath);
}
if (message.includes("→ no test file found")) run.noTestFile += 1;
const vitest =
/^turn_end:\s+(.+?)\s+→\s+test vitest\s+(\S+)\s+\(([^)]+)\)/.exec(
message,
);
if (vitest) {
run.ran += 1;
const [, src, tgt, mode] = vitest;
if (mode.includes("failed-first")) {
run.failedFirst += 1;
const a = checkoutOf(src);
const b = checkoutOf(tgt);
if (a && b && a !== b) run.crossCheckout += 1;
}
}
if (/turn_end: firing \d+ test target\(s\) async/.test(message))
run.testFirings += 1;
if (/\(stale\s*[—-]\s*turn advanced while tests ran\)/.test(message))
run.testStale += 1;
if (/turn_end: retaining newer turn state/.test(message))
run.retainingNewerTurn += 1;
const lower = message.toLowerCase();
if (
/(auto-install|preinstall|installation).*(failed|unavailable|exception)/i.test(
message,
)
) {
state.smellTotals.inc("tool-install-noise");
state.session.toolNoise.inc(normalizeSessionNoise(message));
}
// E1: anchor to the production failure emitters so a worktree name
// such as `468-wait-timeout` inside a `cwd=`/`command=` token cannot
// match (the 20 false positives in the live session). Every emitter
// puts the failure word before any `key=` token: `lsp spawn <id>:
// unavailable|failed` (clients/lsp/index.ts) and `lsp launch
// candidate|managed|bundle|tree-bin failed` (clients/lsp/server.ts).
// `lsp read warm unavailable` is informational, and `lsp process
// <cmd>:` only ever logs `spawn-error` or `closed` (clients/lsp/launch.ts).
if (
/^lsp (?:spawn [^:]+: (?:unavailable|failed)|launch (?:candidate|managed|bundle|tree-bin) failed)/.test(
message,
)
) {
state.smellTotals.inc("lsp-availability-noise");
state.session.lspNoise.inc(normalizeSessionNoise(message));
}
if (
lower.includes("error") ||
lower.includes("exception") ||
lower.includes("timeout")
) {
pushTop(
state.session.errors,
{ ts: iso(ts), message },
limit * 4,
(a, b) => String(b.ts).localeCompare(String(a.ts)),
);
}
});
}
// Close the final run so the last `session_start fired` block is analyzed.
if (state.session.currentRun.startTs)
state.session.runs.push(state.session.currentRun);
}
async function analyzeActionableWarnings(files, state) {
for (const file of files) {
await forEachJsonLine(file, "actionable-warnings", state, (entry) => {
const ts = dateOf(entry.ts);
if (!inWindow(ts)) return;
state.seen.inc("actionable-warnings");
const event = entry.event ?? "unknown";
state.actionable.events.inc(event);
const meta = entry.metadata ?? {};
if (event === "report_complete") {
state.actionable.reports++;
const summary = meta.summary ?? {};
state.actionable.suppressed += Number(summary.suppressed ?? 0);
state.actionable.autoFixEligible += Number(
summary.autoFixEligible ?? 0,
);
}
if (event === "advisory_injected") {
state.actionable.injected++;
state.actionable.injectedAdvisories += Number(meta.unsuppressed ?? 0);
}
if (event === "lsp_file_checked" && meta.lspSource) {
state.actionable.lspSource.inc(meta.lspSource);
}
if (event === "lsp_file_skipped") {
state.actionable.fileSkipReasons.inc(meta.reason ?? "unknown");
}
if (/error|exception|failed/i.test(event) || meta.error) {
state.smellTotals.inc("actionable-warning-errors");
pushTop(
state.actionable.errors,
{
ts: iso(ts),
event,
message: String(meta.error ?? meta.message ?? "").slice(0, 200),
},
limit * 3,
(a, b) => String(b.ts).localeCompare(String(a.ts)),
);
}
});
}
}
async function analyzeAstGrepTools(files, state) {
for (const file of files) {
await forEachJsonLine(file, "ast-grep-tools", state, (entry) => {
const ts = dateOf(entry.ts);
if (!inWindow(ts)) return;
state.seen.inc("ast-grep-tools");
state.astGrep.calls++;
const tool = entry.tool ?? "unknown";
const outcome = entry.outcome ?? "unknown";
state.astGrep.outcomes.inc(`${tool}:${outcome}`);
if (entry.truncated) state.astGrep.truncated++;
if (outcome === "error") {
state.smellTotals.inc("ast-grep-tool-errors");
state.astGrep.errorKinds.inc(entry.errorKind ?? "unknown");
pushTop(
state.astGrep.errors,
{
ts: entry.ts,
tool,
errorKind: entry.errorKind,
durationMs: entry.durationMs,
message: String(entry.errorRaw ?? "")
.replace(/\s+/g, " ")
.slice(0, 180),
pattern: String(entry.pattern ?? "")
.replace(/\s+/g, " ")
.slice(0, 80),
},
limit * 3,
(a, b) => String(b.ts).localeCompare(String(a.ts)),
);
}
if ((entry.durationMs ?? 0) >= 1000) {
pushTop(
state.astGrep.slow,
{
ts: entry.ts,
tool,
durationMs: entry.durationMs,
matchCount: entry.matchCount,
pattern: String(entry.pattern ?? "")
.replace(/\s+/g, " ")
.slice(0, 80),
},
limit * 3,
byDuration,
);
}
});
}
}
/**
* Per-model rollup (#1448): rule × model counts and auto-fixed vs
* agent-required rates, from worklog.jsonl's optional `model`/`provider`
* fields. Entries predating #1448 (or written outside a live agent turn)
* have neither field — bucketed under the empty-string model/provider key so
* "unattributed" volume stays visible rather than silently dropped.
*/
async function analyzeWorklog(files, state) {
for (const file of files) {
await forEachJsonLine(file, "worklog", state, (entry) => {
const ts = dateOf(entry.timestamp);
if (!inWindow(ts)) return;
state.seen.inc("worklog");
const rule = entry.rule ?? "unknown";
const model = entry.model ?? "";
const provider = entry.provider ?? "";
const key = `${rule}||${model}`;
const row = state.worklog.byRuleModel.get(key) ?? {
rule,
model,
total: 0,
autoFixed: 0,
};
row.total += 1;
if (entry.autoFixed) row.autoFixed += 1;
state.worklog.byRuleModel.set(key, row);
state.worklog.byModel.inc(model || "(unknown)");
if (entry.autoFixed)
state.worklog.byModelAutoFixed.inc(model || "(unknown)");
state.worklog.byProvider.inc(provider || "(unknown)");
});
}
}
async function forEachJsonLine(file, bucket, state, visitor) {
await forEachLine(file, async (line) => {
if (!line.trim()) return;
try {
const entry = JSON.parse(line);
if (isExcludedEntry(entry)) {
state.excludedRows++;
return;
}
visitor(entry);
} catch {
state.parseErrors.inc(`${bucket}:${path.basename(file)}`);
}
});
}
function isExcludedEntry(entry) {
return pathValues(entry).some((value) =>
excludeGlobs.some((glob) =>
minimatch(value, glob, { nocase: true, dot: true }),
),
);
}
function pathValues(value, key = "") {
if (typeof value === "string") {
return /(file|path|cwd|root|project)/i.test(key)
? [normalizePath(value)]
: [];
}
if (!value || typeof value !== "object") return [];
return Object.entries(value).flatMap(([childKey, child]) =>
pathValues(child, childKey),
);
}
function isExcludedText(text) {
// E4: the old match only ever saw the dead `session_start cwd:` line. The
// live cwd/root carriers are the pending-resolution mark and the spawn lines.
const values = [];
const cwd = /session_start cwd:\s*(.*)$/.exec(text)?.[1];
if (cwd) values.push(cwd);
for (const match of text.matchAll(/\b(?:cwd|root)=(\S+)/g))
values.push(match[1]);
return values.some((value) =>
excludeGlobs.some((glob) =>
minimatch(normalizePath(value), glob, { nocase: true, dot: true }),
),
);
}
async function forEachLine(file, visitor) {
if (!fs.existsSync(file)) return;
const rl = readline.createInterface({
input: fs.createReadStream(file, { encoding: "utf8" }),
crlfDelay: Infinity,
});
for await (const line of rl) await visitor(line);
}
function dateOf(value) {
if (!value) return null;
const date = new Date(value);
return Number.isNaN(date.getTime()) ? null : date;
}
function inWindow(date) {
if (!since) return true;
return date && date >= since;
}
function iso(date) {
return date?.toISOString?.() ?? "unknown";
}
/**
* The config-resolution join key (#2552 review round 4). A warm MCP process
* keeps ONE session id for its entire life but calls `loadLSPConfig` once per
* SERVED ROOT (#2526 review round 2, F3's premise, reintroduced one layer up
* here) — joining on session id alone let one root's row silently clear every
* OTHER root's deficit under the same session id. `root` is whatever string
* the producer already wrote (the pending mark's `root=`, or the row's own
* `filePath` — both come from the SAME `configResolutionKey(cwd)` call in
* `clients/lsp/config.ts`, which folds separators, canonicalizes, then folds
* again — `normalizeFilePath(path.resolve(normalizeFilePath(cwd)))`, in that
* order (#2518 review F6; resolving first breaks POSIX backslash names), so they compare equal without this script re-deriving any
* path normalization of its own).
*/
function configJoinKey(sessionId, root) {
return `${sessionId} ${root}`;
}
function counter() {
const map = new Map();
return {
inc(key, by = 1) {
map.set(
String(key ?? "unknown"),
(map.get(String(key ?? "unknown")) ?? 0) + by,
);
},
entries() {
return [...map.entries()];
},
top(n = limit) {
return [...map.entries()]
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
.slice(0, n)
.map(([key, count]) => ({ key, count }));
},
toJSON() {
return Object.fromEntries(
[...map.entries()].sort(
(a, b) => b[1] - a[1] || a[0].localeCompare(b[0]),
),
);
},
get(key) {
return map.get(String(key ?? "unknown")) ?? 0;
},
get size() {
return map.size;
},
};
}
function pushTop(array, item, max, compare) {
array.push(item);
array.sort(compare);
if (array.length > max) array.length = max;
}
function byDuration(a, b) {
return (b.durationMs ?? 0) - (a.durationMs ?? 0);
}
function byDiagnostics(a, b) {
return (b.diagnostics ?? 0) - (a.diagnostics ?? 0);
}
function byLine(a, b) {
return (
String(b.ts ?? "").localeCompare(String(a.ts ?? "")) ||
(a.line ?? 0) - (b.line ?? 0)
);
}
function bySeverityThenLine(a, b) {
const rank = { error: 3, warning: 2, info: 1, hint: 0 };
return (
(rank[b.severity] ?? 0) - (rank[a.severity] ?? 0) ||
String(b.ts ?? "").localeCompare(String(a.ts ?? ""))
);
}
function summarizeLatency(entry) {
return {
ts: entry.ts,
durationMs: entry.durationMs,
type: entry.type,
phase: entry.phase,
toolName: entry.toolName,
runnerId: entry.runnerId,
status: entry.status,
result: entry.result,
diagnosticCount: entry.diagnosticCount,
filePath: shortPath(entry.filePath),
project: projectOf(entry.filePath),
metadata: pick(entry.metadata, [
"failureKind",
"failureMessage",
"skipReason",
"completed",
"finalContent",
"runners",
"totalDiagnostics",
"blockers",
]),
};
}
/**
* A `config_resolved` row (#2526), summarised for a smell example.
*
* Deliberately NOT `summarizeLatency`: that helper's metadata whitelist would
* drop every field this row carries, so the example would print a phase name
* and nothing a reader could act on.
*/
function summarizeConfigResolved(entry, note) {
const md = entry.metadata ?? {};
const documents = Array.isArray(md.documents) ? md.documents : [];
return {
ts: entry.ts,
durationMs: entry.durationMs,
phase: entry.phase,
project: projectOf(entry.filePath),
note,
documents: documents
.map(
(doc) =>
`${doc?.tier ?? "?"}:${doc?.file ?? "?"}${doc?.legacy ? " (legacy)" : ""}`,
)
.slice(0, 8),
recordCount: md.recordCount,
deniedServers: md.deniedServers,
};
}
function summarizeWorkspaceSweep(entry) {
const md = entry.metadata ?? {};
const bits = [];
if (md.completed != null)
bits.push(`completed ${md.completed}/${md.total ?? md.fileCount ?? "?"}`);
else if (md.filesChecked != null) bits.push(`checked ${md.filesChecked}`);
if (md.timedOutFiles) bits.push(`timedOutFiles=${md.timedOutFiles}`);
if (md.aborted) bits.push("aborted");
if (md.perFileMs) bits.push(`perFileMs=${md.perFileMs}`);
return {
ts: entry.ts,
durationMs: entry.durationMs,
project: projectOf(entry.filePath),
filePath: shortPath(entry.filePath),
message: bits.join(", "),
};
}
function summarizeDiagnostic(entry) {
return {
ts: entry.timestamp ?? entry.ts,
severity: entry.severity,
tool: entry.tool,
ruleId: entry.ruleId ?? entry.rule,
filePath: shortPath(entry.filePath),
project: projectOf(entry.filePath),
line: entry.line,
column: entry.column,
shownInline: Boolean(entry.shownInline),
shownToAgent: Boolean(entry.shownToAgent),
unresolved: Boolean(entry.unresolved),
message: entry.message,
};
}
function summarizeCascade(entry) {
return {
ts: entry.ts,
phase: entry.phase,
filePath: shortPath(entry.filePath),
neighborFile: shortPath(entry.neighborFile),
project: projectOf(entry.filePath),
graphBuiltMs: entry.graphBuiltMs,
durationMs: entry.durationMs,
neighborCount: entry.neighborCount,
totalNeighborCount: entry.totalNeighborCount,
diagnosticCount: entry.diagnosticCount,
fallbackUsed: Boolean(entry.fallbackUsed),
snapshotMissing: Boolean(entry.snapshotMissing),
error: entry.error,
};
}
function summarizeReadGuard(entry) {
const md = entry.metadata ?? {};
// R2: the old offset/limit/symbol fields were undefined on 100% of rows.
// `touchedLines[0]`/`range[0]` are the real line identity the rows carry.
const touched = Array.isArray(md.touchedLines) ? md.touchedLines : [];
const range = Array.isArray(md.range) ? md.range : [];
return {
ts: entry.ts,
event: entry.event,
filePath: shortPath(entry.filePath),
project: projectOf(entry.filePath),
line: touched[0] ?? range[0],
metadata: md,
};
}
function summarizeTreeSitter(entry) {
return {
ts: entry.ts,
phase: entry.phase,
status: entry.status,
filePath: shortPath(entry.filePath),
project: projectOf(entry.filePath),
languageId: entry.languageId,
diagnostics: entry.diagnostics,
blocking: entry.blocking,
queryCount: entry.queryCount,
effectiveQueryCount: entry.effectiveQueryCount,
metadata: pick(entry.metadata, [
"riskFlags",
"changedSymbols",
"neighborFiles",
"error",
]),
};
}
function pick(obj, keys) {
if (!obj || typeof obj !== "object") return undefined;
const out = {};
for (const key of keys) if (key in obj) out[key] = obj[key];
return Object.keys(out).length ? out : undefined;
}
function normalizeMessage(message) {
return String(message ?? "")
.replace(/\d+/g, "<n>")
.replace(/"[^"]+"/g, '"…"')
.replace(/'[^']+'/g, "'…'")
.slice(0, 180);
}
function normalizeSessionNoise(message) {
return message
.replace(/\([0-9]+ms\)/g, "(<ms>)")
.replace(/retryInMs":\d+/g, "retryInMs:<n>")
.replace(/C:[^\s]+/g, "<path>")
.replace(/\b\d+ms\b/g, "<ms>")
.slice(0, 220);
}
function trackProject(state, filePath) {
if (!filePath) return;
state.projects.inc(projectOf(filePath));
}
function projectOf(filePath) {
const p = normalizePath(filePath);
let match = /\/Desktop\/([^/]+)/i.exec(p);
if (match) return match[1];
match = /\/AppData\/Local\/Temp\/([^/]+)/i.exec(p);
if (match) return `temp:${match[1]}`;
match = /\/\.pi\/agent\/extensions\/([^/]+)/i.exec(p);
if (match) return `extension:${match[1]}`;
match = /\/\.pi-lens\//i.exec(p);
if (match) return ".pi-lens";
match = /^([A-Za-z]:)?\/([^/]+)/.exec(p);
return match?.[2] ?? "unknown";
}
function shortPath(filePath) {
if (!filePath) return undefined;
const p = normalizePath(filePath);
const desktop = /\/Desktop\/([^/]+\/.*)$/i.exec(p);
if (desktop) return desktop[1];
const temp = /\/AppData\/Local\/Temp\/([^/]+\/.*)$/i.exec(p);
if (temp) return `Temp/${temp[1]}`;
const home = normalizePath(os.homedir());
return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
}
function normalizePath(filePath) {
return String(filePath).replace(/\\/g, "/");
}
/** One D3/D4/D5/D8 run, opened by a `session_start fired` line. */
function newRun() {
return {
startTs: null,
commit: null,
edits: 0,
ran: 0,
failedFirst: 0,
crossCheckout: 0,
excluded: 0,
excludedTestFiles: [],
noTestFile: 0,
retainingNewerTurn: 0,
testFirings: 0,
testStale: 0,
};
}
/** The checkout a turn-end path belongs to (`.worktrees/<id>/` or the main `src/`). */
function checkoutOf(p) {
const m = /\.worktrees\/([^/]+)\//.exec(p);
if (m) return m[1];
if (/^(?:src|tests?)\//.test(p)) return "main";
return null;
}
/**
* D2: the markers only a test home carries: the #3521 fork-tree witness home
* and the suite's `pi-lens-test-*` temp dirs. `pi-lens-worktrees`,
* `pi-lens-orchestrator/tmp` and `.probe-home/` are not markers: real
* sessions edit there (the orchestrator writes its task files under
* `.probe-home/orchestration/`).
*/
const SCRATCH_PATH_RE = /(witness-home|pi-lens-test-)/;
/**
* E5/D2: the `opaque_mutation_*` phases log the bash command as `filePath`
* (clients/runtime-tool-call.ts, clients/runtime-tool-result.ts), except
* `opaque_mutation_recovered`, which logs the recovered paths.
*/
function isCommandTextRow(entry) {
const phase = String(entry.phase ?? "");
return (
phase.startsWith("opaque_mutation_") &&
phase !== "opaque_mutation_recovered"
);
}
/** E5: track a latency row's project unless its `filePath` is a shell command. */
function trackLatencyProject(state, entry) {
const filePath = entry?.filePath;
if (typeof filePath !== "string" || filePath.length === 0) return;
if (isCommandTextRow(entry)) return;
// The `<pi-lens>` sentinel is never a project path.
if (filePath.startsWith("<")) return;
state.projects.inc(projectOf(filePath));
}
function trackLatencyPollution(state, entry) {
if (isCommandTextRow(entry)) return;
const hit = pathValues(entry).some((value) => SCRATCH_PATH_RE.test(value));
if (!hit) return;
const pid = String(entry.pid ?? "unknown");
state.latency.scratchRows.set(
pid,
(state.latency.scratchRows.get(pid) ?? 0) + 1,
);
}
/**
* Every latency signal the D5/D6/D7/D9/D11-D15 detectors read. Kept in one
* function so the streaming visitor stays a thin fan-out.
*/
function trackLatencySignals(state, entry, ts) {
const ms = ts instanceof Date ? ts.getTime() : null;
if (ms != null && Number.isFinite(ms)) state.latency.rowTs.push(ms);
const pid = String(entry.pid ?? "unknown");
if (ms != null && Number.isFinite(ms)) {
const first = state.latency.pidFirstTs.get(pid);
if (first == null || ms < first) state.latency.pidFirstTs.set(pid, ms);
const last = state.latency.pidLastTs.get(pid);
if (last == null || ms > last) state.latency.pidLastTs.set(pid, ms);
}
trackLatencyPollution(state, entry);
if (entry.type === "runner") {
const md = entry.metadata ?? {};
if (
entry.status === "failed" &&
md.tier === "collect-later" &&
Number(entry.diagnosticCount ?? 0) > 0 &&
runnerFailureKind(entry) === "blocking_diagnostics"
) {
state.latency.deferredRunnerFailed.push({
ts: entry.ts,
ms,
pid,
turnId: entry.turnId,
runnerId: entry.runnerId,
diagnosticCount: Number(entry.diagnosticCount ?? 0),
filePath: entry.filePath,
});
}
return;
}
if (entry.type === "tool_result") {
// D8: the turn_end summary row carries `blockerSections`; the existing
// tool_result branch only counts `result`, so it never saw this duration.
if (entry.metadata && entry.metadata.blockerSections != null) {
const rows = state.latency.turnEndTools.get(pid) ?? [];
rows.push({
ts: entry.ts,
ms,
durationMs: Number(entry.durationMs ?? 0),
});
state.latency.turnEndTools.set(pid, rows);
}
return;
}
if (entry.type !== "phase") return;
const phase = entry.phase ?? "unknown";
const md = entry.metadata ?? {};
const nowIso = entry.ts;
if (phase === "test_runner_delivery") {
const sid = md.sessionId ?? pid;
const c = state.latency.testRunnerDelivery.get(sid) ?? counter();
c.inc(md.outcome ?? "unknown");
state.latency.testRunnerDelivery.set(sid, c);
} else if (phase === "knip") {
if (
md.execution === "executed" &&
Number.isFinite(Number(md.totalIssues))
) {
const rows = state.latency.knip.get(pid) ?? [];
rows.push({
ts: nowIso,
ms,
durationMs: Number(entry.durationMs ?? 0),
totalIssues: Number(md.totalIssues),
});
state.latency.knip.set(pid, rows);
}
} else if (phase === "degradation_ledger") {
const kind = md.kind ?? "unknown";
const perPid = state.latency.degradationKinds.get(pid) ?? new Map();
perPid.set(kind, Math.max(perPid.get(kind) ?? 0, Number(md.count ?? 0)));
state.latency.degradationKinds.set(pid, perPid);
if (kind === "hook-await-exceeded") {
const budgetMs = Number(md.budgetMs);
const elapsedMs = Number(md.elapsedMs);
state.latency.hookAwaitExceeded.push({
ts: nowIso,
pid,
hook: md.hook,
label: md.label,
budgetMs,
elapsedMs,
cause: md.cause,
ratio: budgetMs > 0 ? elapsedMs / budgetMs : 0,
});
}
} else if (phase === "lsp_client_wait_timeout") {
const serverIds = Array.isArray(md.serverIds) ? md.serverIds : [];
if (serverIds.length === 0) {
const cur = state.latency.lspWaitEmpty.get(pid) ?? { ms: 0, rows: 0 };
cur.ms += Number(entry.durationMs ?? 0);
cur.rows += 1;
state.latency.lspWaitEmpty.set(pid, cur);
}
} else if (phase === "lsp_touch_file") {
const cur = state.latency.lspTouchEdit.get(pid) ?? {
rows: 0,
noClients: 0,
};
if (md.source === "tool_call:edit") {
cur.rows += 1;
if (md.failureKind === "no_clients_none_spawning") cur.noClients += 1;
}
state.latency.lspTouchEdit.set(pid, cur);
if (
md.source === "tool_call:edit" &&
md.failureKind === "no_clients_none_spawning"
) {
state.latency.lspTouchNoClients.push({
ts: nowIso,
ms,
pid,
filePath: entry.filePath,
});
}
} else if (phase === "lsp_client_selected") {
if (md.outcome === "warm-reuse") {
state.latency.lspClientSelectedWarm.push({
ts: nowIso,
ms,
pid,
filePath: entry.filePath,
});
}
} else if (phase === "read_guard_branch_retained") {
state.latency.carryRestart.push({
ts: nowIso,
pid,
trigger: md.trigger,
source: md.source,
kept: Number(md.kept ?? 0),
dropped: Number(md.dropped ?? 0),
branchToolResults: Number(md.branchToolResults ?? 0),
branchReadable: md.branchReadable === true,
});
} else if (phase === "agent_nudge") {
state.latency.agentNudges.push({
ts: nowIso,
ms,
pid,
originCrossProcess: Number(md.originCrossProcess ?? 0),
originLocal: Number(md.originLocal ?? 0),
reasonMix: md.reasonMix,
});
} else if (phase === "session_scope_transition") {
state.latency.sessionScopeTransitions.push({
ts: nowIso,
ms,
pid,
handoffSource: md.handoffSource,
sessionId: md.sessionId,
});
} else if (phase === "late_runner_findings") {
state.latency.lateRunnerFindings.push({
ts: nowIso,
ms,
pid,
turnId: entry.turnId,
failed: Number(md.failed ?? 0),
delivered: Number(md.delivered ?? 0),
dropped: Number(md.dropped ?? 0),
pending: Number(md.pending ?? 0),
});
} else if (phase === "late_auxiliary_findings") {
const pairs = Array.isArray(md.stuckPairs) ? md.stuckPairs : [];
for (const pair of pairs) {
if (!pair?.filePath || !pair?.serverId) continue;
const key = `${pid}\u0000${pair.filePath}\u0000${pair.serverId}`;
state.latency.auxStuck.set(
key,
(state.latency.auxStuck.get(key) ?? 0) + 1,
);
}
} else if (phase === "advisory_provenance_decision") {
const reasons = Array.isArray(md.reasons) ? md.reasons : [];
// The malformed reason implies decision "historical": the validator
// returns status "unknown" with it (clients/advisory-provenance.ts) and
// the row logs every non-current status as historical
// (clients/runtime-context.ts), so the reason alone is the rule.
if (reasons.includes("malformed-or-legacy-provenance")) {
state.latency.advisoryProvenance.push({
ts: nowIso,
ms,
pid,
reasons,
provenanceStamp: md.provenanceStamp,
advisoryKind: md.advisoryKind,
});
}
}
}
/** D2: extension.log rows that name a test home. */
async function analyzeExtension(files, state) {
for (const file of files) {
await forEachJsonLine(file, "extension", state, (entry) => {
const ts = dateOf(entry.ts);
if (!inWindow(ts)) return;
state.seen.inc("extension");
if (SCRATCH_PATH_RE.test(JSON.stringify(entry))) {
const pid = String(entry.pid ?? "unknown");
state.extension.scratchRows.set(
pid,
(state.extension.scratchRows.get(pid) ?? 0) + 1,
);
}
const level = String(entry.level ?? "").toLowerCase();
if (level === "warn" || level === "error") {
const message = String(entry.message ?? "").slice(0, 80);
state.extension.warnErrorGroups.inc(
`${entry.subsystem ?? "unknown"}: ${message}`,
);
}
});
}
}
function countBetween(sorted, a, b) {
let n = 0;
for (const value of sorted) {
if (value > a && value < b) n += 1;
else if (value >= b) break;
}
return n;
}
/** D1: latency gaps >= 10 min that a live session filled with >= 20 rows. */
function computeCoverageGaps(state) {
const lat = [...state.latency.rowTs].sort((a, b) => a - b);
const ss = [...state.session.rowTs].sort((a, b) => a - b);
const cas = [...state.latency.cascadeTs].sort((a, b) => a - b);
const gaps = [];
for (let i = 1; i < lat.length; i += 1) {
const a = lat[i - 1];
const b = lat[i];
const minutes = (b - a) / 60000;
if (minutes < 10) continue;
const sessionstartRows = countBetween(ss, a, b);
if (sessionstartRows < 20) continue;
gaps.push({
start: new Date(a).toISOString(),
end: new Date(b).toISOString(),
minutes: Math.round(minutes),
sessionstartRows,
cascadeRows: countBetween(cas, a, b),
});
}
return gaps;
}
/** D5: per-session firing/stale text and per-session delivery outcomes. */
function computeTestRunnerHealth(state) {
const runs = state.session.runs.map((r) => ({
startTs: r.startTs,
firings: r.testFirings,
stale: r.testStale,
}));
const staleRuns = runs.filter(
(r) => r.firings >= 10 && r.stale / r.firings >= 0.5,
);
const firings = runs.reduce((n, r) => n + r.firings, 0);
const stale = runs.reduce((n, r) => n + r.stale, 0);
const sessions = [];
for (const [sessionId, c] of state.latency.testRunnerDelivery) {
const total = c.entries().reduce((n, [, v]) => n + v, 0);
const delivered = c.get("delivered");
sessions.push({
sessionId,
total,
staged: c.get("staged"),
eligible: c.get("eligible"),
delivered,
superseded: c.get("superseded"),
deliveredShare: total ? delivered / total : 0,
});
}
const lowDelivery = sessions.filter(
(s) => s.total >= 10 && s.deliveredShare < 0.25,
);
return {
firings,
stale,
staleShare: firings ? stale / firings : 0,
staleRuns,
sessions,
lowDelivery,
};
}
/** D6: scanner issue-count drift and turn-end knip wall cost, per pid. */
function computeKnip(state) {
const drift = [];
const cost = [];
for (const [pid, rowsRaw] of state.latency.knip) {
const rows = [...rowsRaw]
.filter((r) => r.ms != null)
.sort((a, b) => a.ms - b.ms);
if (rows.length >= 10) {
const issues = rows.map((r) => r.totalIssues);
let increments = 0;
for (let i = 1; i < issues.length; i += 1)
if (issues[i] - issues[i - 1] >= 100) increments += 1;
const min = Math.min(...issues);
const max = Math.max(...issues);
const spread = min > 0 ? (max - min) / min : 0;
if (increments >= 5 || spread >= 0.25)
drift.push({ pid, rows: rows.length, increments, min, max, spread });
}
const first = state.latency.pidFirstTs.get(pid);
const last = state.latency.pidLastTs.get(pid);
const hours = first != null && last != null ? (last - first) / 3600000 : 0;
const totalMs = rows.reduce((n, r) => n + r.durationMs, 0);
const maxRow = rows.reduce((n, r) => Math.max(n, r.durationMs), 0);
// A lifetime under an hour counts as one hour, so a short pid's few
// seconds of knip cannot extrapolate to a large hourly rate.
const perHour = totalMs / Math.max(hours, 1);
if (perHour >= 30000 || maxRow >= 5000)
cost.push({ pid, rows: rows.length, totalMs, hours, perHour, maxRow });
}
return { drift, cost };
}
/** D7: hook deopts that overran their budget, plus the degradation census. */
function computeHookAwait(state) {
const rows = [...state.latency.hookAwaitExceeded].sort((a, b) =>
String(b.ts).localeCompare(String(a.ts)),
);
const census = [];
for (const [pid, kinds] of state.latency.degradationKinds) {
census.push({
pid,
kinds: [...kinds.entries()]
.sort((a, b) => b[1] - a[1])
.slice(0, 12)
.map(([kind, count]) => `${kind}=${count}`),
});
}
return { rows, over: rows.filter((r) => r.ratio >= 1.1).length, census };
}
/** D8: turn_end hook duration per pid, and the retained-turn-state runs. */
function computeTurnEndSlow(state) {
const flagged = [];
for (const [pid, rows] of state.latency.turnEndTools) {
const slow = rows.filter((r) => r.durationMs > 3000).length;
const max = rows.reduce((n, r) => Math.max(n, r.durationMs), 0);
const share = rows.length ? slow / rows.length : 0;
if ((rows.length >= 20 && share >= 0.1) || max >= 8000)
flagged.push({ pid, rows: rows.length, slow, share, max });
}
const retainRuns = state.session.runs
.filter((r) => r.retainingNewerTurn >= 5)
.map((r) => ({ start: r.startTs, count: r.retainingNewerTurn }));
return { flagged, retainRuns };
}
/** D9: empty-candidate waits, edit touches with no client, warm-reuse clashes. */
function computeLspWait(state) {
const empty = [];
for (const [pid, cur] of state.latency.lspWaitEmpty)
if (cur.ms >= 5000) empty.push({ pid, ms: cur.ms, rows: cur.rows });
const noClients = [];
for (const [pid, cur] of state.latency.lspTouchEdit)
if (cur.rows >= 20 && cur.noClients / cur.rows >= 0.2)
noClients.push({
pid,
rows: cur.rows,
noClients: cur.noClients,
share: cur.noClients / cur.rows,
});
const contradictions = [];
for (const touch of state.latency.lspTouchNoClients) {
const match = state.latency.lspClientSelectedWarm.find(
(w) =>
w.pid === touch.pid &&
w.filePath === touch.filePath &&
touch.ms != null &&
w.ms != null &&
Math.abs(w.ms - touch.ms) <= 500,
);
if (match)
contradictions.push({
pid: touch.pid,
filePath: touch.filePath,
touchTs: touch.ts,
selectedTs: match.ts,
});
}
const pids = new Set([
...empty.map((e) => e.pid),
...noClients.map((e) => e.pid),
...contradictions.map((c) => c.pid),
]);
return { empty, noClients, contradictions, pidCount: pids.size };
}
/** D11: a restart kept nothing while the branch held a populated read set. */
function computeCarryRestart(state) {
return state.latency.carryRestart.filter(
(r) =>
["startup", "resume", "fork", "reload"].includes(r.trigger) &&
r.source !== "none" &&
r.kept + r.dropped === 0 &&
r.branchToolResults >= 50 &&
r.branchReadable,
);
}
/** D12: a cross-process nudge right after a sidecar handoff (suspect grade). */
function computeRestartSelfNudge(state) {
const flags = [];
for (const nudge of state.latency.agentNudges) {
if (nudge.originCrossProcess < 1) continue;
const transition = state.latency.sessionScopeTransitions.find(
(t) =>
t.pid === nudge.pid &&
["own-sidecar", "parent-sidecar"].includes(t.handoffSource) &&
nudge.ms != null &&
t.ms != null &&
nudge.ms >= t.ms &&
nudge.ms - t.ms <= 300000,
);
if (transition) flags.push({ ...nudge, transitionTs: transition.ts });
}
return flags;
}
/** D13: a collect-later runner failed with findings and nothing was delivered. */
function computeDeferredRunner(state) {
const flags = [];
for (const runner of state.latency.deferredRunnerFailed) {
const delivery = state.latency.lateRunnerFindings.find(
(f) =>
f.pid === runner.pid &&
f.turnId === runner.turnId &&
(runner.ms == null || f.ms == null || f.ms >= runner.ms),
);
if (
delivery &&
delivery.failed >= 1 &&
delivery.delivered === 0 &&
delivery.dropped === 0
)
flags.push({
...runner,
deliveryTs: delivery.ts,
pending: delivery.pending,
});
}
return flags;
}
/** D14: an auxiliary (file, server) pair stuck in two or more turn ends. */
function computeAuxStuck(state) {
const flagged = [];
for (const [key, count] of state.latency.auxStuck) {
if (count < 2) continue;
const [pid, filePath, serverId] = key.split("\u0000");
flagged.push({ pid, filePath, serverId, count });
}
flagged.sort((a, b) => b.count - a.count);
return flagged;
}
/** D15: `historical` provenance with a malformed/legacy stamp. */
function computeAdvisoryProvenance(state) {
return state.latency.advisoryProvenance.map((row) => {
const first = state.latency.pidFirstTs.get(row.pid);
const seconds =
first != null && row.ms != null
? Math.round((row.ms - first) / 1000)
: null;
return { ...row, secondsSinceFirstRow: seconds };
});
}
/** D16: `pi-lens loaded: Nms` loads over 2 s. A load with no
* `session_start fired` within the next 60 s is a short-lived pid. */
function computeSlowExtensionLoad(state) {
const fired = state.session.firedTs;
return state.session.slowLoads
.filter((l) => l.durationMs >= 2000)
.map((l) => ({
...l,
shortLived: !(
Number.isFinite(l.ms) &&
fired.some((f) => f >= l.ms && f - l.ms <= 60000)
),
}));
}
function buildReport(state) {
const smells = [];
const smellCount = (id) => state.smellTotals.get(id);
// D1-D16 detector computations. Kept out of the smell list so every value
// they produce is assertable on its own, not only through a smell count.
const coverageGaps = computeCoverageGaps(state);
const testRunnerHealth = computeTestRunnerHealth(state);
const knip = computeKnip(state);
const hookAwait = computeHookAwait(state);
const turnEndSlow = computeTurnEndSlow(state);
const lspWait = computeLspWait(state);
const carryRestart = computeCarryRestart(state);
const restartSelfNudge = computeRestartSelfNudge(state);
const deferredRunner = computeDeferredRunner(state);
const auxStuck = computeAuxStuck(state);
const advisoryProvenance = computeAdvisoryProvenance(state);
const slowExtensionLoad = computeSlowExtensionLoad(state);
const latencyScratch = [...state.latency.scratchRows.values()].reduce(
(n, v) => n + v,
0,
);
const extensionScratch = [...state.extension.scratchRows.values()].reduce(
(n, v) => n + v,
0,
);
const d3Runs = state.session.runs.filter((r) => {
const excludedTest = r.excludedTestFiles.some((p) =>
/\.(test|spec)\.|\/tests?\//.test(p),
);
return (r.excluded >= 1 && excludedTest) || (r.edits >= 10 && r.ran === 0);
});
const d4Cross = state.session.runs.reduce(
(n, r) => n + (r.crossCheckout >= 3 ? r.crossCheckout : 0),
0,
);
const testRunnerSmellCount =
testRunnerHealth.staleRuns.length + testRunnerHealth.lowDelivery.length;
addSmell(
smells,
"diagnostic-blockers",
smellCount("diagnostic-blockers"),
"Diagnostics shown inline/to agent or severity=error",
state.diagnostics.errors.slice(0, limit),
);
addSmell(
smells,
"runner-failures",
smellCount("runner-failures"),
"Dispatch runners failed/crashed",
state.latency.runnerFailures.slice(0, limit),
);
addSmell(
smells,
"slow-hook-path",
smellCount("slow-hook-path"),
`Tool total phases >= ${thresholds.totalSlowMs}ms`,
state.latency.slowTotals.slice(0, limit),
);
addSmell(
smells,
"slow-runners",
smellCount("slow-runners"),
`Runner durations >= ${thresholds.runnerSlowMs}ms`,
state.latency.slowRunners.slice(0, limit),
);
addSmell(
smells,
"tool-install-noise",
smellCount("tool-install-noise"),
"Repeated unavailable/failed preinstall or auto-install messages",
state.session.toolNoise.top(limit),
);
addSmell(
smells,
"lsp-availability-noise",
smellCount("lsp-availability-noise"),
"Repeated LSP unavailable/failed/timeout messages",
state.session.lspNoise.top(limit),
);
addSmell(
smells,
"slow-session-start",
smellCount("slow-session-start"),
`session_start total >= ${thresholds.startupSlowMs}ms`,
state.session.slowStarts.slice(0, limit),
);
addSmell(
smells,
"slow-background-tasks",
smellCount("slow-background-tasks"),
`session_start background tasks >= ${thresholds.backgroundSlowMs}ms`,
state.session.slowTasks.slice(0, limit),
);
// #2526: config resolution has to prove it HAPPENED. Two shapes, one smell:
//
// (a) a session that has a `config_resolution_pending` mark and produced no
// `config_resolved` row. Round 3, S1: the pending mark is written by
// `loadLSPConfig` itself, at the instant a resolution is actually
// attempted — never predicted from a handler's own flags ahead of time.
// Round 2, F2 established the JOIN itself: on the session id both sides
// carry, never a subtraction of two counts (subtracting counted quick
// sessions' rows against full sessions' start lines — different
// populations — which both masked total silence and charged
// `--no-lsp`/subagent sessions with a permanent deficit they could never
// clear).
// Round 4, #2552 review: the join is on (session, root), not session
// alone — a warm MCP process keeps one session id for its whole life but
// marks once per SERVED ROOT (`configJoinKey`'s doc comment), so one
// root's row must not clear another root's deficit under the same id.
// (b) a legacy document present with zero records — the deprecation
// machinery went silent while the user is on a removal schedule.
const pendingConfigResolution = [...state.config.pendingSessions.entries()];
const unresolvedSessions = pendingConfigResolution.filter(
([key]) => !state.config.resolvedSessions.has(key),
);
for (const [, value] of unresolvedSessions.slice(0, limit * 3)) {
state.config.examples.push({
ts: value.ts,
session: value.sessionId,
root: value.root,
note: "session had a config_resolution_pending mark and produced no config_resolved row",
});
}
addSmell(
smells,
"config-resolution",
unresolvedSessions.length + state.config.legacyWithoutRecords,
`Sessions with a config_resolution_pending mark but no config_resolved row (${unresolvedSessions.length} of ${pendingConfigResolution.length}) or a legacy config document that produced no migration record (${state.config.legacyWithoutRecords})`,
state.config.examples.slice(0, limit),
);
addSmell(
smells,
"cascade-fallbacks",
smellCount("cascade-fallbacks"),
"Cascade used fallback/degraded path or logged errors",
state.cascade.fallbacks.slice(0, limit),
);
addSmell(
smells,
"cascade-slow-graphs",
smellCount("cascade-slow-graphs"),
`Cascade graph build >= ${thresholds.cascadeGraphSlowMs}ms`,
state.cascade.slowGraphs.slice(0, limit),
);
addSmell(
smells,
"read-guard-blocks",
state.readGuard.events.get("edit_blocked") +
state.readGuard.events.get("edit_preflight_blocked"),
"Read-guard blocked edits; host/model provenance is not present on the block rows, and warns and exact-replacement misses are informational",
state.readGuard.blocks.slice(0, limit),
);
addSmell(
smells,
"read-guard-stale-ranges",
smellCount("read-guard-stale-ranges"),
`Enforced range mismatches whose read snapshot no longer matched (bypassed-content-match ${state.readGuard.bypassedMismatch} excluded as an allowed edit)`,
state.readGuard.staleRanges.slice(0, limit),
);
addSmell(
smells,
"tree-sitter-blocking",
smellCount("tree-sitter-blocking"),
"Tree-sitter runner produced blocking diagnostics",
state.treeSitter.blocking.slice(0, limit),
);
addSmell(
smells,
"tree-sitter-failures",
smellCount("tree-sitter-failures"),
"Tree-sitter runner failures",
state.treeSitter.failures.slice(0, limit),
);
addSmell(
smells,
"ast-grep-tool-errors",
smellCount("ast-grep-tool-errors"),
"MCP ast-grep search/replace calls that errored",
state.astGrep.errors.slice(0, limit),
);
addSmell(
smells,
"actionable-warning-errors",
smellCount("actionable-warning-errors"),
"Actionable-warnings advisory pipeline logged an error",
state.actionable.errors.slice(0, limit),
);
const ws = state.latency.workspace;
const incompleteSweeps = Math.max(0, ws.started - ws.completed);
addSmell(
smells,
"lsp-workspace-diagnostics-incomplete",
incompleteSweeps,
"Full LSP workspace sweeps that logged a start but never a completion (hang/kill signature — the 8h-hang class #383 hardened)",
ws.progress.slice(0, limit),
);
// #1618: "hit the per-file budget" used to be this smell's blanket
// description regardless of WHY a file was unconfirmed — the exact
// mislabeling the forensics pass on this class found (81/111 "budget"
// files were actually service-destroyed). Render the real breakdown when
// available.
const unconfirmedReasonBreakdown = ws.unconfirmedByReason
.top(limit)
.map(({ key, count }) => `${key}=${count}`)
.join(", ");
addSmell(
smells,
"lsp-workspace-file-timeouts",
smellCount("lsp-workspace-file-timeouts"),
`Full LSP sweeps produced an unconfirmed file (${ws.timedOutFilesTotal} files across ${ws.timedOutSweeps} sweeps)` +
(unconfirmedReasonBreakdown
? ` — by reason: ${unconfirmedReasonBreakdown}`
: ""),
ws.sweeps.slice(0, limit),
);
// D1: a latency gap a live session filled.
addSmell(
smells,
"log-coverage-gap",
coverageGaps.length,
"Latency stream gap >= 10 min that a live session filled with >= 20 sessionstart rows",
coverageGaps.slice(0, limit).map((g) => ({
ts: g.start,
message: `${g.minutes} min gap ${g.start} -> ${g.end}; sessionstartRows=${g.sessionstartRows}; cascadeRows=${g.cascadeRows}`,
})),
);
// D2: test home markers in a real log. Never added to the
// default denylist so the pollution stays visible.
addSmell(
smells,
"real-log-test-pollution",
latencyScratch + extensionScratch,
`Rows naming a test home (witness-home, pi-lens-test-*) (latency ${latencyScratch}, extension ${extensionScratch}); these never belong in a real log`,
[
...[...state.latency.scratchRows.entries()].map(([pid, count]) => ({
key: `latency pid ${pid}`,
count,
})),
...[...state.extension.scratchRows.entries()].map(([pid, count]) => ({
key: `extension pid ${pid}`,
count,
})),
].slice(0, limit),
);
addSmell(
smells,
"extension-warn-errors",
state.extension.warnErrorGroups.size,
"extension.log warn/error groups by subsystem and message prefix",
state.extension.warnErrorGroups.top(limit),
);
// D3: a run that excluded its own test files, or edited without running one.
addSmell(
smells,
"turn-end-tests-excluded",
d3Runs.length,
"Turn-end runs that skipped test files by policy, or edited >= 10 files and ran no test",
d3Runs.slice(0, limit).map((r) => ({
ts: r.startTs,
message: `edits=${r.edits} ran=${r.ran} excluded=${r.excluded} noTestFile=${r.noTestFile}${
r.excludedTestFiles.length ? `: ${r.excludedTestFiles.join(", ")}` : ""
}`,
})),
);
// D4: a failed-first target that resolved to a different checkout (#3649).
addSmell(
smells,
"test-target-cross-checkout",
d4Cross,
"Failed-first test targets resolved to a different checkout than the edited source (regression guard for #3649)",
state.session.runs
.filter((r) => r.crossCheckout >= 3)
.slice(0, limit)
.map((r) => ({
ts: r.startTs,
message: `${r.crossCheckout} cross-checkout of ${r.failedFirst} failed-first`,
})),
);
// D5: sessions whose turn-end verdicts went stale, or were rarely delivered.
addSmell(
smells,
"test-runner-stale-verdicts",
testRunnerSmellCount,
`Sessions with >= 10 test firings and >= 50% stale (${testRunnerHealth.staleRuns.length}), or >= 10 delivery rows and < 25% delivered (${testRunnerHealth.lowDelivery.length}); window firings ${testRunnerHealth.firings}, stale ${testRunnerHealth.stale}`,
[
...testRunnerHealth.staleRuns.map((r) => ({
ts: r.startTs,
count: r.stale,
message: `firings=${r.firings} stale=${r.stale}`,
})),
...testRunnerHealth.lowDelivery.map((s) => ({
key: s.sessionId,
count: s.delivered,
message: `delivered ${s.delivered}/${s.total} (${Math.round(
s.deliveredShare * 100,
)}%), staged ${s.staged}, eligible ${s.eligible}, superseded ${s.superseded}`,
})),
].slice(0, limit),
);
// D6: scanner-count drift and knip wall cost.
addSmell(
smells,
"scanner-count-drift",
knip.drift.length,
"knip totalIssues rose by >= 100 between consecutive executed runs at least 5 times, or its spread >= 25% (pids with >= 10 executed runs)",
knip.drift.slice(0, limit).map((d) => ({
key: `pid ${d.pid}`,
count: d.increments,
message: `issues ${d.min} -> ${d.max} (+${Math.round(
d.spread * 100,
)}%), ${d.increments} increments >= 100`,
})),
);
addSmell(
smells,
"turn-end-knip-cost",
knip.cost.length,
"knip consumed >= 30 s per hour of pid lifetime (a lifetime under an hour counts as one hour), or a single run took >= 5 s",
knip.cost.slice(0, limit).map((c) => ({
key: `pid ${c.pid}`,
count: Math.round(c.perHour),
message: `${c.totalMs}ms over ${c.hours.toFixed(2)}h = ${Math.round(
c.perHour,
)}ms/h, max ${c.maxRow}ms`,
})),
);
// D7: hook deopts that overran their budget.
addSmell(
smells,
"hook-await-exceeded",
hookAwait.rows.length,
`Hook awaits that overran their budget (${hookAwait.over} with ratio >= 1.1)`,
hookAwait.rows.slice(0, limit).map((r) => ({
ts: r.ts,
message: `${r.hook}:${r.label} ${r.elapsedMs}/${r.budgetMs}ms (${r.ratio.toFixed(
3,
)}) pid=${r.pid} cause=${r.cause}`,
})),
);
// D8: turn_end hook duration and retained-state churn.
addSmell(
smells,
"turn-end-slow",
turnEndSlow.flagged.length,
"turn_end hook duration over budget (a pid with >= 10% of summaries > 3 s, or one >= 8 s)",
turnEndSlow.flagged.slice(0, limit).map((f) => ({
key: `pid ${f.pid}`,
count: f.slow,
message: `${f.slow}/${f.rows} over 3s (${Math.round(
f.share * 100,
)}%), max ${f.max}ms`,
})),
);
addSmell(
smells,
"turn-end-retained-state",
turnEndSlow.retainRuns.length,
"Turn-end retained newer turn state at least 5 times in one session",
turnEndSlow.retainRuns.slice(0, limit).map((r) => ({
ts: r.start,
count: r.count,
})),
);
// D9: empty-candidate waits and no-client/edit warm-reuse contradictions.
addSmell(
smells,
"lsp-wait-empty-candidates",
lspWait.pidCount,
`LSP touch saw no client while a warm client was selected, or waited with an empty candidate set (empty ${lspWait.empty.length}, noClients ${lspWait.noClients.length}, contradictions ${lspWait.contradictions.length})`,
[
...lspWait.empty.map((e) => ({
key: `pid ${e.pid}`,
count: e.rows,
message: `${e.ms}ms waiting with no candidate server`,
})),
...lspWait.noClients.map((e) => ({
key: `pid ${e.pid}`,
count: Math.round(e.share * 100),
message: `${e.noClients}/${e.rows} edits saw no client (${Math.round(
e.share * 100,
)}%)`,
})),
...lspWait.contradictions.map((c) => ({
key: `pid ${c.pid}`,
count: 1,
message: `${c.filePath}: no-client touch at ${c.touchTs}, warm-reuse at ${c.selectedTs}`,
})),
].slice(0, limit),
);
// D10: a zero_read block that contradicts earlier read/edit evidence.
addSmell(
smells,
"resume-state-loss",
state.readGuard.stateLost.length,
`zero_read edits blocked despite earlier read/edit evidence in the same session (genuine ${state.readGuard.genuineZeroRead.length})`,
state.readGuard.stateLost.slice(0, limit),
);
// D11: a restart whose read-set carry was empty while the branch was populated.
addSmell(
smells,
"carry-empty-restart",
carryRestart.length,
"Restart carry kept and dropped nothing while the branch held a populated read set",
carryRestart.slice(0, limit).map((r) => ({
key: `pid ${r.pid}`,
count: r.branchToolResults,
message: `trigger=${r.trigger} source=${r.source} kept=${r.kept} dropped=${r.dropped} branchToolResults=${r.branchToolResults}`,
})),
);
// D12: a cross-process nudge right after a sidecar handoff (suspect until O7).
addSmell(
smells,
"restart-self-nudge",
restartSelfNudge.length,
"Cross-process nudge within 300 s of a sidecar handoff (suspect-grade until the nudge carries a file id, O7)",
restartSelfNudge.slice(0, limit).map((r) => ({
key: `pid ${r.pid}`,
count: r.originCrossProcess,
message: `${JSON.stringify(r.reasonMix)} crossProcess=${r.originCrossProcess} after ${r.transitionTs}`,
})),
);
// D13: a collect-later runner failed with findings and none were delivered.
addSmell(
smells,
"deferred-runner-failed-undelivered",
deferredRunner.length,
"collect-later runner failed with findings and the delivery row shows failed>=1, delivered=0, dropped=0 (#3796 witness)",
deferredRunner.slice(0, limit).map((r) => ({
ts: r.ts,
message: `${r.runnerId} pid=${r.pid} turnId=${r.turnId} diagnostics=${r.diagnosticCount} delivery=${r.deliveryTs}`,
})),
);
// D14: an auxiliary (file, server) pair stuck in >= 2 turn ends.
addSmell(
smells,
"aux-stuck-pair",
auxStuck.length,
"Auxiliary (file, server) pair stuck in >= 2 turn ends",
auxStuck.slice(0, limit).map((p) => ({
key: p.serverId,
count: p.count,
message: `${p.filePath} pid=${p.pid}`,
})),
);
// D15: historical provenance with a malformed/legacy stamp.
addSmell(
smells,
"advisory-provenance-unknown",
advisoryProvenance.length,
"Advisory provenance resolved to historical with a malformed-or-legacy stamp",
advisoryProvenance.slice(0, limit).map((r) => ({
ts: r.ts,
message: `${r.provenanceStamp} (${r.advisoryKind}) ${r.secondsSinceFirstRow}s after pid start`,
})),
);
// D16: extension load time over 2 s.
addSmell(
smells,
"slow-extension-load",
slowExtensionLoad.length,
"pi-lens loaded >= 2000ms after process start (short-lived: no session_start fired within 60 s)",
slowExtensionLoad.slice(0, limit).map((l) => ({
...l,
message: `${l.message}${l.shortLived ? " [short-lived]" : ""}`,
})),
);
return {
window: state.window,
filesScanned: Object.fromEntries(
Object.entries(state.files).map(([key, list]) => [key, list.length]),
),
rowsSeen: state.seen.toJSON(),
rowsExcluded: state.excludedRows,
parseErrors: state.parseErrors.toJSON(),
projects: state.projects.top(limit),
smells,
diagnostics: {
bySeverity: state.diagnostics.bySeverity.toJSON(),
byTool: state.diagnostics.byTool.toJSON(),
byRule: state.diagnostics.byRule.top(limit),
byFile: state.diagnostics.byFile.top(limit),
topMessages: state.diagnostics.topMessages.top(limit),
shownInlineShownAgentUnresolved: state.diagnostics.shownInline.toJSON(),
},
latency: {
testRunnerVerdicts: Object.fromEntries(
[...state.latency.testRunnerVerdicts.entries()].map(
([sessionId, summary]) => [
sessionId,
{
...summary,
denominator: summary.total,
rate: summary.total ? summary.stale / summary.total : 0,
},
],
),
),
runnerStatus: state.latency.runnerStatus.top(limit * 2),
runnerFailureKinds: state.latency.runnerFailureKinds.top(limit * 2),
runnerBlockingFindings: state.latency.runnerBlockingFindings.toJSON(),
toolResults: state.latency.toolResults.toJSON(),
phaseCounts: state.latency.phaseCounts.top(limit),
phaseTimeouts: state.latency.phaseTimeouts.toJSON(),
workspaceDiagnostics: {
started: ws.started,
completed: ws.completed,
incomplete: incompleteSweeps,
aborted: ws.aborted,
timedOutSweeps: ws.timedOutSweeps,
timedOutFilesTotal: ws.timedOutFilesTotal,
// #1618: the real per-reason breakdown of `timedOutFilesTotal`.
// "budget (pre-#1618 build)" means those log lines predate the
// per-reason tally entirely — an older build's absence of the field,
// not a claim those files really timed out.
unconfirmedByReason: ws.unconfirmedByReason.toJSON(),
},
},
cascade: {
phases: state.cascade.phases.toJSON(),
missingSnapshots: state.cascade.missingSnapshots.top(limit),
noNeighborsByProject: state.cascade.noNeighbors.top(limit),
largeFanout: state.cascade.largeFanout.slice(0, limit),
},
readGuard: {
events: state.readGuard.events.toJSON(),
byReason: state.readGuard.byReason.toJSON(),
preflightReasons: state.readGuard.preflightReasons.toJSON(),
snapshotStatus: state.readGuard.snapshotStatus.toJSON(),
snapshotEnforcement: state.readGuard.snapshotEnforcement.toJSON(),
byFile: state.readGuard.byFile.top(limit),
staleRanges: state.readGuard.staleRanges.slice(0, limit),
zeroReads: state.readGuard.zeroReads.slice(0, limit),
unavailableSnapshots: state.readGuard.unavailableSnapshots.slice(
0,
limit,
),
// E2/D10: the block/warn split and the lost-read-set classification.
warns: state.readGuard.warns.slice(0, limit),
blockByKind: state.readGuard.blockByKind.toJSON(),
bypassedMismatch: state.readGuard.bypassedMismatch,
stateLost: state.readGuard.stateLost.slice(0, limit),
genuineZeroRead: state.readGuard.genuineZeroRead.slice(0, limit),
},
treeSitter: {
phases: state.treeSitter.phases.toJSON(),
queryCache: state.treeSitter.queryCache.toJSON(),
riskFlags: state.treeSitter.riskFlags.toJSON(),
highDiagnostics: state.treeSitter.highDiagnostics.slice(0, limit),
},
session: {
starts: state.session.starts,
cwds: state.session.cwds.top(limit),
commits: state.session.commits.toJSON(),
slowLoads: state.session.slowLoads.slice(0, limit),
errors: state.session.errors.slice(0, limit),
},
// #2526: the positive-observability counters behind the
// `config-resolution` smell, so a reader can see WHY it fired (or that
// it correctly did not) without re-deriving the deficit.
config: {
resolved: state.config.resolved,
sessionsPendingResolution: pendingConfigResolution.length,
sessionsWithoutResolution: unresolvedSessions.length,
legacyDocuments: state.config.legacyDocuments,
legacyWithoutRecords: state.config.legacyWithoutRecords,
},
actionable: {
events: state.actionable.events.toJSON(),
reports: state.actionable.reports,
advisoriesInjected: state.actionable.injected,
advisoryWarningsInjected: state.actionable.injectedAdvisories,
warningsSuppressed: state.actionable.suppressed,
autoFixEligible: state.actionable.autoFixEligible,
lspSource: state.actionable.lspSource.toJSON(),
fileSkipReasons: state.actionable.fileSkipReasons.toJSON(),
errors: state.actionable.errors.slice(0, limit),
},
astGrep: {
calls: state.astGrep.calls,
outcomes: state.astGrep.outcomes.toJSON(),
errorKinds: state.astGrep.errorKinds.toJSON(),
truncated: state.astGrep.truncated,
errors: state.astGrep.errors.slice(0, limit),
slow: state.astGrep.slow.slice(0, limit),
},
worklog: {
byModel: state.worklog.byModel.top(limit * 2),
byProvider: state.worklog.byProvider.top(limit * 2),
byRuleModel: [...state.worklog.byRuleModel.values()]
.map((row) => ({
...row,
autoFixedRate: row.total ? row.autoFixed / row.total : 0,
}))
.sort((a, b) => b.total - a.total)
.slice(0, limit * 3),
},
// D1-D16: the per-detector values behind the smells above, so a test can
// pin the rule (not only the rendered count) and a reader can see WHY.
detectors: {
logCoverageGap: { gaps: coverageGaps },
realLogTestPollution: {
latencyByPid: Object.fromEntries(state.latency.scratchRows),
extensionByPid: Object.fromEntries(state.extension.scratchRows),
latencyTotal: latencyScratch,
extensionTotal: extensionScratch,
},
turnEndTestsExcluded: {
runs: d3Runs.map((r) => ({ ...r })),
},
testTargetCrossCheckout: {
runs: state.session.runs.map((r) => ({
startTs: r.startTs,
crossCheckout: r.crossCheckout,
failedFirst: r.failedFirst,
})),
},
testRunnerStaleVerdicts: testRunnerHealth,
knip,
hookAwait,
turnEndSlow,
lspWait,
resumeStateLoss: {
stateLost: state.readGuard.stateLost,
genuine: state.readGuard.genuineZeroRead,
},
carryEmptyRestart: carryRestart,
restartSelfNudge,
deferredRunnerFailedUndelivered: deferredRunner,
auxStuckPairs: auxStuck,
advisoryProvenanceUnknown: advisoryProvenance,
slowExtensionLoad,
extensionWarnErrors: state.extension.warnErrorGroups.toJSON(),
},
};
}
function addSmell(smells, id, count, description, examples) {
if (!count) return;
const severity = count >= 20 ? "high" : count >= 5 ? "medium" : "low";
smells.push({ id, severity, count, description, examples });
}
function printReport(report) {
console.log(`pi-lens log smell report`);
console.log(`window: ${report.window.since} → now`);
console.log(`root: ${report.window.root}`);
console.log(
`rows: ${
Object.entries(report.rowsSeen)
.map(([k, v]) => `${k}=${v}`)
.join(", ") || "none"
}`,
);
console.log(
`files scanned: ${Object.entries(report.filesScanned)
.map(([k, v]) => `${k}=${v}`)
.join(", ")}`,
);
console.log(`${report.rowsExcluded} rows excluded (synthetic corpora)`);
if (Object.keys(report.parseErrors).length)
console.log(`parse errors: ${JSON.stringify(report.parseErrors)}`);
section(
"Projects touched",
report.projects,
(x) => `${x.count.toString().padStart(5)} ${x.key}`,
);
console.log("\nSmells");
if (!report.smells.length) {
console.log(" none above thresholds");
} else {
for (const smell of report.smells) {
console.log(`\n [${smell.severity}] ${smell.id}: ${smell.count}`);
console.log(` ${smell.description}`);
for (const ex of smell.examples.slice(0, Math.min(5, limit)))
console.log(` - ${formatExample(ex)}`);
}
}
// D1/D2/D5/D6/D7: the non-smell breakdown behind the new detectors.
const det = report.detectors ?? {};
const gap = det.logCoverageGap;
if (gap?.gaps?.length) {
console.log("\nLog coverage");
for (const g of gap.gaps ?? [])
console.log(
` gap ${g.minutes}min ${g.start} → ${g.end} sessionstart=${g.sessionstartRows} cascade=${g.cascadeRows}`,
);
}
const warnErrors = Object.entries(det.extensionWarnErrors ?? {});
if (warnErrors.length) {
console.log("\nExtension diagnostics");
for (const [k, v] of warnErrors.slice(0, limit))
console.log(` ${String(v).padStart(5)} ${k}`);
}
const deg = det.hookAwait?.census ?? [];
if (deg.length) {
console.log("\nDegradation ledger census (max count per pid)");
for (const row of deg.slice(0, limit))
console.log(` pid ${row.pid}: ${row.kinds.join(", ")}`);
}
const delivery = det.testRunnerStaleVerdicts;
if (delivery) {
console.log("\nTest-runner delivery");
console.log(
` firings=${delivery.firings} stale=${delivery.stale} (${(
delivery.staleShare * 100
).toFixed(1)}%)`,
);
for (const s of delivery.sessions ?? [])
console.log(
` ${s.sessionId}: delivered ${s.delivered}/${s.total} staged=${s.staged} eligible=${s.eligible} superseded=${s.superseded}`,
);
}
const knip = det.knip;
if (knip && (knip.drift.length || knip.cost.length)) {
console.log("\nknip drift and cost");
for (const d of knip.drift)
console.log(
` pid ${d.pid}: issues ${d.min} → ${d.max} (${d.increments} increments >= 100)`,
);
for (const c of knip.cost)
console.log(
` pid ${c.pid}: ${Math.round(c.perHour)}ms/h, max ${c.maxRow}ms`,
);
}
const extLoads = det.slowExtensionLoad ?? [];
if (extLoads.length) {
const shortLived = extLoads.filter((l) => l.shortLived).length;
console.log("\nExtension loads");
console.log(` slow=${extLoads.length} shortLived=${shortLived}`);
}
section(
"Diagnostic rules",
report.diagnostics.byRule,
(x) => `${x.count.toString().padStart(5)} ${x.key}`,
);
section(
"Diagnostic files",
report.diagnostics.byFile,
(x) => `${x.count.toString().padStart(5)} ${x.key}`,
);
section(
"Runner statuses",
report.latency.runnerStatus,
(x) => `${x.count.toString().padStart(5)} ${x.key}`,
);
section(
"Read-guard events",
Object.entries(report.readGuard.events).map(([key, count]) => ({
key,
count,
})),
(x) => `${String(x.count).padStart(5)} ${x.key}`,
);
section(
"Read-guard block reasons",
Object.entries(report.readGuard.byReason).map(([key, count]) => ({
key,
count,
})),
(x) => `${String(x.count).padStart(5)} ${x.key}`,
);
section(
"Read-guard snapshot status",
Object.entries(report.readGuard.snapshotStatus).map(([key, count]) => ({
key,
count,
})),
(x) => `${String(x.count).padStart(5)} ${x.key}`,
);
section(
"Cascade phases",
Object.entries(report.cascade.phases).map(([key, count]) => ({
key,
count,
})),
(x) => `${String(x.count).padStart(5)} ${x.key}`,
);
section(
"Runner failure kinds (infra vs found-errors)",
report.latency.runnerFailureKinds,
(x) => `${x.count.toString().padStart(5)} ${x.key}`,
);
const verdictSessions = Object.entries(
report.latency.testRunnerVerdicts ?? {},
);
if (verdictSessions.length) {
console.log("\nTest-runner stale verdicts per session (verdict rows only)");
for (const [sessionId, summary] of verdictSessions) {
console.log(
` ${sessionId}: ${summary.stale}/${summary.total} stale (${(summary.rate * 100).toFixed(1)}%)`,
);
}
}
const a = report.actionable;
if (a.reports || a.advisoriesInjected) {
console.log("\nActionable warnings");
console.log(
` reports=${a.reports} advisoriesInjected=${a.advisoriesInjected} warningsInjected=${a.advisoryWarningsInjected} suppressed=${a.warningsSuppressed} autoFixEligible=${a.autoFixEligible}`,
);
const lspSrc = Object.entries(a.lspSource);
if (lspSrc.length)
console.log(
` lspSource: ${lspSrc.map(([k, v]) => `${k}=${v}`).join(", ")}`,
);
const skips = Object.entries(a.fileSkipReasons);
if (skips.length)
console.log(
` lsp skip reasons: ${skips.map(([k, v]) => `${k}=${v}`).join(", ")}`,
);
}
const ag = report.astGrep;
if (ag.calls) {
console.log("\nast-grep tools");
console.log(` calls=${ag.calls} truncated=${ag.truncated}`);
const outcomes = Object.entries(ag.outcomes);
if (outcomes.length)
console.log(
` outcomes: ${outcomes.map(([k, v]) => `${k}=${v}`).join(", ")}`,
);
if (Object.keys(ag.errorKinds).length)
console.log(` error kinds: ${JSON.stringify(ag.errorKinds)}`);
}
const ws = report.latency.workspaceDiagnostics;
if (ws && (ws.started || ws.completed)) {
console.log("\nLSP workspace sweeps (lens_diagnostics full)");
console.log(
` started=${ws.started} completed=${ws.completed} incomplete=${ws.incomplete} aborted=${ws.aborted} fileTimeouts=${ws.timedOutFilesTotal} (in ${ws.timedOutSweeps} sweeps)`,
);
// #1618: never let a flat count alone read as "budget exhaustion" —
// print the real per-reason split (or its absence, which itself means
// every line here predates the tally).
const reasons = Object.entries(ws.unconfirmedByReason ?? {});
if (reasons.length) {
console.log(
` by reason: ${reasons.map(([reason, count]) => `${reason}=${count}`).join(", ")}`,
);
}
}
const wl = report.worklog;
if (wl && (wl.byModel.length || wl.byRuleModel.length)) {
console.log("\nWorklog per-model rollup (#1448)");
console.log(
` by model: ${wl.byModel.map((x) => `${x.key}=${x.count}`).join(", ")}`,
);
if (wl.byProvider.length)
console.log(
` by provider: ${wl.byProvider.map((x) => `${x.key}=${x.count}`).join(", ")}`,
);
console.log(" rule × model (auto-fixed vs agent-required):");
for (const row of wl.byRuleModel.slice(0, limit))
console.log(
` ${String(row.total).padStart(5)} ${row.rule} [${row.model || "(unknown)"}] autoFixed=${row.autoFixed} (${(row.autoFixedRate * 100).toFixed(0)}%)`,
);
}
const timeouts = Object.entries(report.latency.phaseTimeouts ?? {});
if (timeouts.length) {
console.log("\nPhase timeouts");
for (const [k, v] of timeouts)
console.log(` ${String(v).padStart(5)} ${k}`);
}
}
function section(title, rows, formatter) {
console.log(`\n${title}`);
if (!rows?.length) {
console.log(" none");
return;
}
for (const row of rows.slice(0, limit)) console.log(` ${formatter(row)}`);
}
function formatExample(ex) {
if (!ex) return "";
const bits = [];
if (ex.durationMs != null) bits.push(`${ex.durationMs}ms`);
if (ex.graphBuiltMs != null) bits.push(`graph=${ex.graphBuiltMs}ms`);
if (ex.runnerId) bits.push(ex.runnerId);
if (ex.phase) bits.push(ex.phase);
if (ex.status) bits.push(ex.status);
if (ex.tool) bits.push(`${ex.tool}/${ex.ruleId}`);
if (ex.event) bits.push(ex.event);
if (ex.project) bits.push(`[${ex.project}]`);
if (ex.filePath) bits.push(ex.filePath);
if (ex.line) bits.push(`:${ex.line}`);
if (ex.message) bits.push(`— ${String(ex.message).slice(0, 160)}`);
if (ex.error) bits.push(`— ${String(ex.error).slice(0, 160)}`);
if (!bits.length && ex.key) bits.push(`${ex.count} × ${ex.key}`);
return bits.join(" ");
}
if (process.argv[1] === fileURLToPath(import.meta.url) && args.help) {
printHelp();
}