pi-lens 3.8.63 → 3.8.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +12 -9
- package/dist/clients/deadline-utils.js +23 -0
- package/dist/clients/installer/index.js +18 -27
- package/dist/clients/log-cleanup.js +36 -13
- package/dist/clients/lsp/client.js +44 -0
- package/dist/clients/lsp/index.js +208 -33
- package/dist/clients/lsp/interactive-install.js +14 -4
- package/dist/clients/lsp/launch.js +29 -100
- package/dist/clients/mcp/review.js +7 -4
- package/dist/clients/package-manager.js +223 -0
- package/dist/clients/pipeline.js +55 -2
- package/dist/clients/project-metadata.js +3 -23
- package/dist/clients/safe-spawn.js +9 -0
- package/dist/clients/tree-sitter-client.js +72 -14
- package/dist/index.js +16 -0
- package/dist/mcp/server.js +4 -2
- package/dist/tools/ast-grep-search.js +10 -6
- package/dist/tools/lens-diagnostics.js +49 -10
- package/dist/tools/lsp-diagnostics.js +27 -4
- package/dist/tools/scan-progress.js +53 -0
- package/grammars/tree-sitter-bash.wasm +0 -0
- package/grammars/tree-sitter-bash.wasm.json +5 -0
- package/grammars/tree-sitter-css.wasm +0 -0
- package/grammars/tree-sitter-css.wasm.json +5 -0
- package/grammars/tree-sitter-go.wasm +0 -0
- package/grammars/tree-sitter-go.wasm.json +5 -0
- package/grammars/tree-sitter-html.wasm +0 -0
- package/grammars/tree-sitter-html.wasm.json +5 -0
- package/grammars/tree-sitter-java.wasm +0 -0
- package/grammars/tree-sitter-java.wasm.json +5 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-javascript.wasm.json +5 -0
- package/grammars/tree-sitter-json.wasm +0 -0
- package/grammars/tree-sitter-json.wasm.json +5 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-python.wasm.json +5 -0
- package/grammars/tree-sitter-rust.wasm +0 -0
- package/grammars/tree-sitter-rust.wasm.json +5 -0
- package/grammars/tree-sitter-tsx.wasm +0 -0
- package/grammars/tree-sitter-tsx.wasm.json +5 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm.json +5 -0
- package/grammars/tree-sitter-yaml.wasm +0 -0
- package/grammars/tree-sitter-yaml.wasm.json +5 -0
- package/package.json +9 -7
- package/scripts/analyze-pi-lens-logs.mjs +101 -3
- package/scripts/download-grammars.js +171 -30
- package/scripts/grammars.lock.json +32 -0
|
@@ -11,15 +11,26 @@
|
|
|
11
11
|
import * as path from "node:path";
|
|
12
12
|
import { Type } from "../clients/deps/typebox.js";
|
|
13
13
|
import { compactRenderResult } from "./render-compact.js";
|
|
14
|
+
import { combineAbortSignals } from "../clients/deadline-utils.js";
|
|
14
15
|
import { getProjectIgnoreMatcher } from "../clients/file-utils.js";
|
|
15
16
|
import { getLSPService } from "../clients/lsp/index.js";
|
|
16
17
|
import { loadProjectDiagnosticsDeltaReport, loadProjectDiagnosticsSnapshot, reconcileProjectDiagnosticsSnapshot, } from "../clients/project-diagnostics/cache.js";
|
|
17
18
|
import { scanProjectDiagnostics } from "../clients/project-diagnostics/scanner.js";
|
|
18
19
|
import { getFileDiagnosticSummaries, reconcileStaleWidgetFiles, } from "../clients/widget-state.js";
|
|
20
|
+
import { makeProgressReporter, scanningSummaryLine } from "./scan-progress.js";
|
|
19
21
|
// The widget state exposes the full per-file diagnostic set; this is the tool's
|
|
20
22
|
// own generous display budget per file (independent of the TUI's 12 cap), to
|
|
21
23
|
// keep output bounded on a pathologically broken file.
|
|
22
24
|
const MAX_DIAGNOSTICS_PER_FILE = 50;
|
|
25
|
+
// Wall-clock ceiling for the whole mode=full scan. Even with per-file budgets
|
|
26
|
+
// and abort-signal honoring, a pathological state (a language server hanging on
|
|
27
|
+
// spawn/initialize across many files) could otherwise stall the tool
|
|
28
|
+
// indefinitely — an unattended session was observed hung for ~8h. This hard cap
|
|
29
|
+
// aborts the scan so it always returns (partial) rather than never. Env-tunable.
|
|
30
|
+
const FULL_SCAN_WALL_CLOCK_MS = (() => {
|
|
31
|
+
const raw = Number(process.env.PI_LENS_LENS_DIAGNOSTICS_FULL_TIMEOUT_MS);
|
|
32
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 180_000; // 3 min default
|
|
33
|
+
})();
|
|
23
34
|
export function createLensDiagnosticsTool(cacheManager, getCwd, getLspService = getLSPService,
|
|
24
35
|
// Flush any debounced per-edit dispatches before reading, so files the agent
|
|
25
36
|
// fixed earlier in the turn are re-dispatched and the widget reflects the
|
|
@@ -50,6 +61,12 @@ flushPending = async () => { }) {
|
|
|
50
61
|
"also scans cheap project runners (tree-sitter + fact-rules) and caches them.",
|
|
51
62
|
promptSnippet: "Use lens_diagnostics mode=all to verify no blocking errors remain; use mode=full for expensive project-wide checks",
|
|
52
63
|
renderResult: compactRenderResult(({ details, args, isError, text }) => {
|
|
64
|
+
// Streaming progress partials render the live bar (see scanningSummaryLine)
|
|
65
|
+
// instead of the details-driven summary, which would show "0 diagnostics"
|
|
66
|
+
// mid-scan.
|
|
67
|
+
const scanning = scanningSummaryLine(details, text);
|
|
68
|
+
if (scanning)
|
|
69
|
+
return scanning;
|
|
53
70
|
const mode = details?.mode ?? (typeof args.mode === "string" ? args.mode : "delta");
|
|
54
71
|
if (isError) {
|
|
55
72
|
return `lens_diagnostics ${mode} — ${text.split("\n")[0] ?? "error"}`;
|
|
@@ -95,7 +112,7 @@ flushPending = async () => { }) {
|
|
|
95
112
|
description: "Filter by severity (default: all).",
|
|
96
113
|
})),
|
|
97
114
|
}),
|
|
98
|
-
async execute(_toolCallId, params, signal,
|
|
115
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
99
116
|
const mode = params.mode ?? "delta";
|
|
100
117
|
const severity = params.severity ?? "all";
|
|
101
118
|
const refreshRunners = params.refreshRunners;
|
|
@@ -105,6 +122,11 @@ flushPending = async () => { }) {
|
|
|
105
122
|
const maxProjectFiles = parsePositiveInt(params.maxProjectFiles);
|
|
106
123
|
const maxLspFiles = parsePositiveInt(params.maxLspFiles);
|
|
107
124
|
const cwd = ctx.cwd ?? getCwd();
|
|
125
|
+
// Escape aborts the agent *turn*, which fires ctx.signal (the turn-wired
|
|
126
|
+
// abort); the positional signal is the tool-call signal. A registered
|
|
127
|
+
// extension tool only reliably sees the turn abort via ctx.signal, so honor
|
|
128
|
+
// BOTH — else a long mode=full scan ignores Escape (the reported bug).
|
|
129
|
+
const abortSignal = combineAbortSignals(signal, ctx.signal);
|
|
108
130
|
// Reflect the agent's just-made fixes before reporting: flush pending
|
|
109
131
|
// per-edit dispatches (re-records fixed files), then drop entries whose
|
|
110
132
|
// file changed on disk afterwards / was deleted (stale, e.g. external
|
|
@@ -115,11 +137,21 @@ flushPending = async () => { }) {
|
|
|
115
137
|
return formatAllMode(cwd, severity, undefined, undefined, staleDropped);
|
|
116
138
|
}
|
|
117
139
|
if (mode === "full") {
|
|
140
|
+
// Fold a hard wall-clock ceiling into the abort signal so the scan
|
|
141
|
+
// always terminates (partial) even if a hung server would otherwise
|
|
142
|
+
// stall it forever. AbortSignal.timeout aborts with a TimeoutError.
|
|
143
|
+
const ceiling = AbortSignal.timeout(FULL_SCAN_WALL_CLOCK_MS);
|
|
144
|
+
const fullSignal = combineAbortSignals(abortSignal, ceiling);
|
|
145
|
+
// Stream a throttled progress bar: the full scan is opaque for minutes
|
|
146
|
+
// otherwise.
|
|
147
|
+
const onProgress = makeProgressReporter(onUpdate);
|
|
118
148
|
return formatFullMode(cwd, severity, getLspService(), {
|
|
119
149
|
refreshRunners,
|
|
120
150
|
maxProjectFiles,
|
|
121
151
|
maxLspFiles,
|
|
122
|
-
signal,
|
|
152
|
+
signal: fullSignal,
|
|
153
|
+
wallClockMs: FULL_SCAN_WALL_CLOCK_MS,
|
|
154
|
+
onProgress,
|
|
123
155
|
});
|
|
124
156
|
}
|
|
125
157
|
return formatDeltaMode(cacheManager, cwd, severity);
|
|
@@ -421,6 +453,7 @@ async function formatFullMode(cwd, severity, lspService, options = {}) {
|
|
|
421
453
|
runWorkspaceDiagnostics.call(lspService, cwd, {
|
|
422
454
|
maxFiles: options.maxLspFiles,
|
|
423
455
|
signal,
|
|
456
|
+
onProgress: options.onProgress,
|
|
424
457
|
}),
|
|
425
458
|
getProjectDiagnosticsSnapshotForFullMode(cwd, options),
|
|
426
459
|
]);
|
|
@@ -449,16 +482,22 @@ async function formatFullMode(cwd, severity, lspService, options = {}) {
|
|
|
449
482
|
turnIndex: projectDelta.turnIndex,
|
|
450
483
|
},
|
|
451
484
|
});
|
|
452
|
-
//
|
|
453
|
-
//
|
|
485
|
+
// Stopped mid-scan: the results above are whatever completed before the abort.
|
|
486
|
+
// Tell the agent so it doesn't read a partial sweep as "clean" (#341). The
|
|
487
|
+
// abort is either a user/turn cancel (Escape) or the wall-clock ceiling firing
|
|
488
|
+
// (AbortSignal.timeout → TimeoutError), which guarantees the scan can't hang
|
|
489
|
+
// indefinitely — distinguish them so the agent knows whether to just re-run.
|
|
454
490
|
if (aborted) {
|
|
455
|
-
const
|
|
456
|
-
|
|
491
|
+
const timedOut = signal
|
|
492
|
+
?.reason?.name === "TimeoutError";
|
|
493
|
+
const note = timedOut
|
|
494
|
+
? `\n\n⚠ Scan exceeded its ${Math.round((options.wallClockMs ?? 0) / 1000)}s time budget and was stopped — results are partial. ` +
|
|
495
|
+
"Narrow it with maxLspFiles, or raise PI_LENS_LENS_DIAGNOSTICS_FULL_TIMEOUT_MS."
|
|
496
|
+
: "\n\n⚠ Scan cancelled before completion — results are partial. " +
|
|
497
|
+
"Re-run with a smaller maxLspFiles to finish within budget.";
|
|
457
498
|
return {
|
|
458
|
-
content: [
|
|
459
|
-
|
|
460
|
-
],
|
|
461
|
-
details: result.details,
|
|
499
|
+
content: [{ type: "text", text: result.content[0].text + note }],
|
|
500
|
+
details: { ...result.details, timedOut },
|
|
462
501
|
};
|
|
463
502
|
}
|
|
464
503
|
return result;
|
|
@@ -9,7 +9,9 @@ import * as path from "node:path";
|
|
|
9
9
|
import { Type } from "../clients/deps/typebox.js";
|
|
10
10
|
import { getProjectIgnoreMatcher, isExcludedDirName, } from "../clients/file-utils.js";
|
|
11
11
|
import { getLSPService } from "../clients/lsp/index.js";
|
|
12
|
+
import { combineAbortSignals } from "../clients/deadline-utils.js";
|
|
12
13
|
import { baseName, compactRenderResult } from "./render-compact.js";
|
|
14
|
+
import { makeProgressReporter, scanningSummaryLine } from "./scan-progress.js";
|
|
13
15
|
const LANG_EXTENSIONS = {
|
|
14
16
|
".ts": [".ts", ".tsx", ".mts", ".cts"],
|
|
15
17
|
".tsx": [".ts", ".tsx", ".mts", ".cts"],
|
|
@@ -76,17 +78,24 @@ function boundedPositiveInt(value, fallback, min, max) {
|
|
|
76
78
|
return fallback;
|
|
77
79
|
return Math.max(min, Math.min(max, parsed));
|
|
78
80
|
}
|
|
79
|
-
async function mapWithConcurrency(items, concurrency, mapper) {
|
|
81
|
+
async function mapWithConcurrency(items, concurrency, mapper, signal, onProgress) {
|
|
80
82
|
const results = [];
|
|
81
83
|
let nextIndex = 0;
|
|
84
|
+
let completed = 0;
|
|
82
85
|
const workers = Math.min(Math.max(1, concurrency), items.length);
|
|
83
86
|
await Promise.all(Array.from({ length: workers }, async () => {
|
|
84
87
|
while (true) {
|
|
88
|
+
// Honor cancellation (Escape / turn abort): stop pulling new items
|
|
89
|
+
// rather than grind the whole batch. Completed entries are returned.
|
|
90
|
+
if (signal?.aborted)
|
|
91
|
+
return;
|
|
85
92
|
const index = nextIndex;
|
|
86
93
|
nextIndex += 1;
|
|
87
94
|
if (index >= items.length)
|
|
88
95
|
return;
|
|
89
96
|
results[index] = await mapper(items[index], index);
|
|
97
|
+
completed += 1;
|
|
98
|
+
onProgress?.(completed, items.length);
|
|
90
99
|
}
|
|
91
100
|
}));
|
|
92
101
|
return results;
|
|
@@ -148,6 +157,10 @@ export function createLspDiagnosticsTool() {
|
|
|
148
157
|
"Works on directories by auto-detecting file extensions and scanning all matching files.",
|
|
149
158
|
promptSnippet: "Get LSP diagnostics for a file or directory (use before builds)",
|
|
150
159
|
renderResult: compactRenderResult(({ details, args, isError, text }) => {
|
|
160
|
+
// Streaming progress partials render the live bar (see scanningSummaryLine).
|
|
161
|
+
const scanning = scanningSummaryLine(details, text);
|
|
162
|
+
if (scanning)
|
|
163
|
+
return scanning;
|
|
151
164
|
if (isError) {
|
|
152
165
|
return `lsp_diagnostics — ${text.split("\n")[0] ?? "error"}`;
|
|
153
166
|
}
|
|
@@ -182,7 +195,13 @@ export function createLspDiagnosticsTool() {
|
|
|
182
195
|
description: "Optional per-file LSP wait budget for batch diagnostics. Uses server defaults when omitted.",
|
|
183
196
|
})),
|
|
184
197
|
}),
|
|
185
|
-
async execute(_toolCallId, params, _signal,
|
|
198
|
+
async execute(_toolCallId, params, _signal, onUpdate, ctx) {
|
|
199
|
+
// Escape aborts the turn via ctx.signal; honor both it and the tool-call
|
|
200
|
+
// signal so a batch/directory scan cancels rather than grinding on.
|
|
201
|
+
const signal = combineAbortSignals(_signal, ctx.signal);
|
|
202
|
+
// Stream a throttled progress bar for batch/directory scans (opaque for
|
|
203
|
+
// seconds-to-minutes otherwise).
|
|
204
|
+
const onProgress = makeProgressReporter(onUpdate, "Scanning LSP diagnostics");
|
|
186
205
|
const typedParams = params;
|
|
187
206
|
const severity = (typedParams.severity ?? "all");
|
|
188
207
|
const cwd = ctx.cwd ?? process.cwd();
|
|
@@ -209,6 +228,8 @@ export function createLspDiagnosticsTool() {
|
|
|
209
228
|
return runBatchFileDiagnostics(absPaths, severity, lspService, {
|
|
210
229
|
concurrency,
|
|
211
230
|
waitMs,
|
|
231
|
+
signal,
|
|
232
|
+
onProgress,
|
|
212
233
|
});
|
|
213
234
|
}
|
|
214
235
|
const rawPath = typedParams.path;
|
|
@@ -244,6 +265,8 @@ export function createLspDiagnosticsTool() {
|
|
|
244
265
|
return runDirectoryDiagnostics(absPath, severity, lspService, {
|
|
245
266
|
concurrency,
|
|
246
267
|
waitMs,
|
|
268
|
+
signal,
|
|
269
|
+
onProgress,
|
|
247
270
|
});
|
|
248
271
|
}
|
|
249
272
|
return runFileDiagnostics(absPath, severity, lspService, waitMs);
|
|
@@ -354,7 +377,7 @@ async function runBatchFileDiagnostics(absPaths, severity, lspService, options)
|
|
|
354
377
|
details: { mode: "batch", severity, filesChecked: 0 },
|
|
355
378
|
};
|
|
356
379
|
}
|
|
357
|
-
const results = await mapWithConcurrency(absPaths, options.concurrency, (file) => collectFileDiagnosticResult(file, severity, lspService, options.waitMs));
|
|
380
|
+
const results = await mapWithConcurrency(absPaths, options.concurrency, (file) => collectFileDiagnosticResult(file, severity, lspService, options.waitMs), options.signal, options.onProgress);
|
|
358
381
|
const fileErrors = results.flatMap((result) => result.error ? [result.error] : []);
|
|
359
382
|
const lspHealthWarnings = results.flatMap((result) => result.unavailable ? [result.unavailable] : []);
|
|
360
383
|
const allDiags = results.flatMap((result) => result.diagnostics);
|
|
@@ -436,7 +459,7 @@ async function runDirectoryDiagnostics(absPath, severity, lspService, options) {
|
|
|
436
459
|
}
|
|
437
460
|
const wasCapped = collectedFiles.length > MAX_FILES;
|
|
438
461
|
const filesToProcess = collectedFiles.slice(0, MAX_FILES);
|
|
439
|
-
const results = await mapWithConcurrency(filesToProcess, options.concurrency, (file) => collectFileDiagnosticResult(file, severity, lspService, options.waitMs));
|
|
462
|
+
const results = await mapWithConcurrency(filesToProcess, options.concurrency, (file) => collectFileDiagnosticResult(file, severity, lspService, options.waitMs), options.signal, options.onProgress);
|
|
440
463
|
const fileErrors = results.flatMap((result) => result.error ? [result.error] : []);
|
|
441
464
|
const lspHealthWarnings = results.flatMap((result) => result.unavailable ? [result.unavailable] : []);
|
|
442
465
|
const allDiags = results.flatMap((result) => result.diagnostics);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared progress-streaming helper for the long-running full/batch/directory
|
|
3
|
+
* diagnostic scans (`lens_diagnostics mode=full`, `lsp_diagnostics`). Those runs
|
|
4
|
+
* are opaque for minutes; this streams a throttled progress bar to the tool's
|
|
5
|
+
* `onUpdate` callback so the agent/user sees movement.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* For a tool's `compactRenderResult` summarizer: when the result is a streaming
|
|
9
|
+
* progress partial (`details.phase === "scanning"`, emitted via `onUpdate` during
|
|
10
|
+
* a full scan), return the bar to display — the joined content `text` (which
|
|
11
|
+
* already holds `renderScanProgress` output), or a counts fallback. Returns
|
|
12
|
+
* `null` for a normal (non-progress) result so the caller falls through to its
|
|
13
|
+
* usual summary. Without this, the details-driven summarizer renders "0
|
|
14
|
+
* diagnostics" mid-scan and the bar never shows.
|
|
15
|
+
*/
|
|
16
|
+
export function scanningSummaryLine(details, text) {
|
|
17
|
+
const d = details;
|
|
18
|
+
if (d?.phase !== "scanning")
|
|
19
|
+
return null;
|
|
20
|
+
return text || `Scanning… ${d.completed ?? 0}/${d.total ?? 0}`;
|
|
21
|
+
}
|
|
22
|
+
/** A ≤20-char ASCII bar + counts, e.g. `Scanning… [████░░░░░░] 45/123 (37%)`. */
|
|
23
|
+
export function renderScanProgress(completed, total, label = "Scanning project diagnostics") {
|
|
24
|
+
const pct = total > 0 ? Math.min(100, Math.round((completed / total) * 100)) : 0;
|
|
25
|
+
const width = 20;
|
|
26
|
+
const filled = Math.round((pct / 100) * width);
|
|
27
|
+
const bar = "█".repeat(filled) + "░".repeat(Math.max(0, width - filled));
|
|
28
|
+
return `${label}… [${bar}] ${completed}/${total} (${pct}%)`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Build a throttled `(completed, total) => void` that streams the progress bar to
|
|
32
|
+
* `onUpdate` — at most once per `throttleMs` (default 250ms, ~4×/s) plus the
|
|
33
|
+
* final tick so the bar always lands on 100%. Returns `undefined` when the SDK
|
|
34
|
+
* gave no callback, so callers can pass it straight through as an optional.
|
|
35
|
+
*/
|
|
36
|
+
export function makeProgressReporter(onUpdate, label, throttleMs = 250) {
|
|
37
|
+
const emit = onUpdate;
|
|
38
|
+
if (typeof emit !== "function")
|
|
39
|
+
return undefined;
|
|
40
|
+
let lastEmit = 0;
|
|
41
|
+
return (completed, total) => {
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
if (completed < total && now - lastEmit < throttleMs)
|
|
44
|
+
return;
|
|
45
|
+
lastEmit = now;
|
|
46
|
+
emit({
|
|
47
|
+
content: [
|
|
48
|
+
{ type: "text", text: renderScanProgress(completed, total, label) },
|
|
49
|
+
],
|
|
50
|
+
details: { phase: "scanning", completed, total },
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-lens",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.65",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo",
|
|
6
6
|
"repository": {
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
},
|
|
10
10
|
"main": "./dist/index.js",
|
|
11
11
|
"bin": {
|
|
12
|
-
"pi-lens-mcp": "
|
|
13
|
-
"pi-lens-analyze": "
|
|
12
|
+
"pi-lens-mcp": "dist/mcp/server.js",
|
|
13
|
+
"pi-lens-analyze": "dist/mcp/analyze-cli.js"
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "tsc --project tsconfig.build.json",
|
|
17
17
|
"build:dist": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc --project tsconfig.dist.json --noCheck",
|
|
18
|
-
"prepare": "npm run build:dist",
|
|
18
|
+
"prepare": "npm run build:dist && node scripts/download-grammars.js --core --dest grammars",
|
|
19
19
|
"lint": "tsc --project tsconfig.json",
|
|
20
20
|
"watch": "tsc --watch",
|
|
21
21
|
"test": "vitest run",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"check": "node scripts/check-extensions.mjs",
|
|
26
26
|
"selftest:install": "node scripts/install-selftest.mjs",
|
|
27
27
|
"check:lockfile": "node scripts/check-lockfile-sync.mjs",
|
|
28
|
+
"check:grammars": "node scripts/check-grammar-provenance.mjs",
|
|
28
29
|
"audit:tree-sitter": "node scripts/audit-tree-sitter-rules.mjs",
|
|
29
30
|
"audit:rule-catalog": "node scripts/validate-rule-catalog.mjs",
|
|
30
31
|
"audit:playground": "node scripts/playground-verify-rule.mjs",
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
"changelog:release": "node scripts/changelog-release.mjs",
|
|
34
35
|
"changelog:extract": "node scripts/changelog-extract.mjs",
|
|
35
36
|
"release:backfill-notes": "node scripts/backfill-github-releases.mjs",
|
|
37
|
+
"release:backfill-thanks": "node scripts/backfill-release-thanks.mjs",
|
|
36
38
|
"docs:rule-catalogs": "node scripts/gen-rule-catalogs.mjs",
|
|
37
39
|
"banner": "npx -y sharp-cli --input banner.svg --output banner.png resize 2200 620",
|
|
38
40
|
"download-grammars": "node scripts/download-grammars.js",
|
|
@@ -40,8 +42,7 @@
|
|
|
40
42
|
"harness:python-poc": "node scripts/run-harness.mjs cases/python/pipeline-dispatch-poc --pi-bin \"C:\\Users\\R3LiC\\AppData\\Roaming\\npm\\pi.cmd\"",
|
|
41
43
|
"harness:ts-format-poc": "node scripts/run-harness.mjs cases/ts/format-default-poc --pi-bin \"C:\\Users\\R3LiC\\AppData\\Roaming\\npm\\pi.cmd\"",
|
|
42
44
|
"harness:python-autofix-poc": "node scripts/run-harness.mjs cases/python/autofix-default-poc --pi-bin \"C:\\Users\\R3LiC\\AppData\\Roaming\\npm\\pi.cmd\"",
|
|
43
|
-
"harness:go-poc": "node scripts/run-harness.mjs cases/go/pipeline-dispatch-poc --pi-bin \"C:\\Users\\R3LiC\\AppData\\Roaming\\npm\\pi.cmd\""
|
|
44
|
-
"postinstall": "node scripts/download-grammars.js"
|
|
45
|
+
"harness:go-poc": "node scripts/run-harness.mjs cases/go/pipeline-dispatch-poc --pi-bin \"C:\\Users\\R3LiC\\AppData\\Roaming\\npm\\pi.cmd\""
|
|
45
46
|
},
|
|
46
47
|
"keywords": [
|
|
47
48
|
"pi",
|
|
@@ -62,10 +63,12 @@
|
|
|
62
63
|
"license": "MIT",
|
|
63
64
|
"files": [
|
|
64
65
|
"dist/",
|
|
66
|
+
"grammars/",
|
|
65
67
|
"config/",
|
|
66
68
|
"rules/",
|
|
67
69
|
"skills/",
|
|
68
70
|
"scripts/download-grammars.js",
|
|
71
|
+
"scripts/grammars.lock.json",
|
|
69
72
|
"scripts/analyze-pi-lens-logs.mjs",
|
|
70
73
|
"scripts/install-selftest.mjs",
|
|
71
74
|
"scripts/rpc-load-check.mjs",
|
|
@@ -93,7 +96,6 @@
|
|
|
93
96
|
}
|
|
94
97
|
},
|
|
95
98
|
"optionalDependencies": {
|
|
96
|
-
"tree-sitter-wasms": "npm:null@^0.11.0",
|
|
97
99
|
"web-tree-sitter": "0.25.10"
|
|
98
100
|
},
|
|
99
101
|
"devDependencies": {
|
|
@@ -170,6 +170,16 @@ function createState(files) {
|
|
|
170
170
|
slowTotals: [],
|
|
171
171
|
toolResults: counter(),
|
|
172
172
|
phaseCounts: counter(),
|
|
173
|
+
phaseTimeouts: counter(),
|
|
174
|
+
workspace: {
|
|
175
|
+
started: 0,
|
|
176
|
+
completed: 0,
|
|
177
|
+
aborted: 0,
|
|
178
|
+
timedOutSweeps: 0,
|
|
179
|
+
timedOutFilesTotal: 0,
|
|
180
|
+
sweeps: [],
|
|
181
|
+
progress: [],
|
|
182
|
+
},
|
|
173
183
|
},
|
|
174
184
|
diagnostics: {
|
|
175
185
|
bySeverity: counter(),
|
|
@@ -293,6 +303,7 @@ async function analyzeLatency(files, state) {
|
|
|
293
303
|
} else if (entry.type === "phase") {
|
|
294
304
|
const phase = entry.phase ?? "unknown";
|
|
295
305
|
state.latency.phaseCounts.inc(phase);
|
|
306
|
+
if (/_timeout$/.test(phase)) state.latency.phaseTimeouts.inc(phase);
|
|
296
307
|
if (
|
|
297
308
|
phase === "total" &&
|
|
298
309
|
(entry.durationMs ?? 0) >= thresholds.totalSlowMs
|
|
@@ -304,6 +315,33 @@ async function analyzeLatency(files, state) {
|
|
|
304
315
|
limit * 3,
|
|
305
316
|
byDuration,
|
|
306
317
|
);
|
|
318
|
+
} else if (phase === "lsp_workspace_diagnostics_start") {
|
|
319
|
+
state.latency.workspace.started += 1;
|
|
320
|
+
} else if (phase === "lsp_workspace_diagnostics_progress") {
|
|
321
|
+
// Keep the heartbeats so an incomplete sweep can show how far it
|
|
322
|
+
// got (completed X/Y) before it went silent — the hang forensics.
|
|
323
|
+
pushTop(
|
|
324
|
+
state.latency.workspace.progress,
|
|
325
|
+
summarizeWorkspaceSweep(entry),
|
|
326
|
+
limit * 3,
|
|
327
|
+
byDuration,
|
|
328
|
+
);
|
|
329
|
+
} else if (phase === "lsp_workspace_diagnostics") {
|
|
330
|
+
const ws = state.latency.workspace;
|
|
331
|
+
ws.completed += 1;
|
|
332
|
+
if (entry.metadata?.aborted) ws.aborted += 1;
|
|
333
|
+
const timedOut = Number(entry.metadata?.timedOutFiles ?? 0);
|
|
334
|
+
if (timedOut > 0) {
|
|
335
|
+
ws.timedOutSweeps += 1;
|
|
336
|
+
ws.timedOutFilesTotal += timedOut;
|
|
337
|
+
state.smellTotals.inc("lsp-workspace-file-timeouts");
|
|
338
|
+
pushTop(
|
|
339
|
+
ws.sweeps,
|
|
340
|
+
summarizeWorkspaceSweep(entry),
|
|
341
|
+
limit * 3,
|
|
342
|
+
byDuration,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
307
345
|
}
|
|
308
346
|
} else if (entry.type === "tool_result") {
|
|
309
347
|
state.latency.toolResults.inc(entry.result ?? "unknown");
|
|
@@ -539,9 +577,12 @@ async function analyzeSessionStart(files, state) {
|
|
|
539
577
|
);
|
|
540
578
|
}
|
|
541
579
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
)
|
|
580
|
+
// Runtime logs "success runMs=<n> queuedMs=<n>"; older logs used
|
|
581
|
+
// "success (<n>ms)". Match both so a format drift can't silently zero
|
|
582
|
+
// this smell again (it did: the `(<n>ms)` regex matched 0 of ~2k rows).
|
|
583
|
+
const task =
|
|
584
|
+
/session_start task ([^:]+): success runMs=(\d+)/.exec(message) ??
|
|
585
|
+
/session_start task ([^:]+): success \((\d+)ms\)/.exec(message);
|
|
545
586
|
if (task && Number(task[2]) >= thresholds.backgroundSlowMs) {
|
|
546
587
|
state.smellTotals.inc("slow-background-tasks");
|
|
547
588
|
pushTop(
|
|
@@ -798,6 +839,24 @@ function summarizeLatency(entry) {
|
|
|
798
839
|
};
|
|
799
840
|
}
|
|
800
841
|
|
|
842
|
+
function summarizeWorkspaceSweep(entry) {
|
|
843
|
+
const md = entry.metadata ?? {};
|
|
844
|
+
const bits = [];
|
|
845
|
+
if (md.completed != null)
|
|
846
|
+
bits.push(`completed ${md.completed}/${md.total ?? md.fileCount ?? "?"}`);
|
|
847
|
+
else if (md.filesChecked != null) bits.push(`checked ${md.filesChecked}`);
|
|
848
|
+
if (md.timedOutFiles) bits.push(`timedOutFiles=${md.timedOutFiles}`);
|
|
849
|
+
if (md.aborted) bits.push("aborted");
|
|
850
|
+
if (md.perFileMs) bits.push(`perFileMs=${md.perFileMs}`);
|
|
851
|
+
return {
|
|
852
|
+
ts: entry.ts,
|
|
853
|
+
durationMs: entry.durationMs,
|
|
854
|
+
project: projectOf(entry.filePath),
|
|
855
|
+
filePath: shortPath(entry.filePath),
|
|
856
|
+
message: bits.join(", "),
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
801
860
|
function summarizeDiagnostic(entry) {
|
|
802
861
|
return {
|
|
803
862
|
ts: entry.timestamp ?? entry.ts,
|
|
@@ -1047,6 +1106,22 @@ function buildReport(state) {
|
|
|
1047
1106
|
"Actionable-warnings advisory pipeline logged an error",
|
|
1048
1107
|
state.actionable.errors.slice(0, limit),
|
|
1049
1108
|
);
|
|
1109
|
+
const ws = state.latency.workspace;
|
|
1110
|
+
const incompleteSweeps = Math.max(0, ws.started - ws.completed);
|
|
1111
|
+
addSmell(
|
|
1112
|
+
smells,
|
|
1113
|
+
"lsp-workspace-diagnostics-incomplete",
|
|
1114
|
+
incompleteSweeps,
|
|
1115
|
+
"Full LSP workspace sweeps that logged a start but never a completion (hang/kill signature — the 8h-hang class #383 hardened)",
|
|
1116
|
+
ws.progress.slice(0, limit),
|
|
1117
|
+
);
|
|
1118
|
+
addSmell(
|
|
1119
|
+
smells,
|
|
1120
|
+
"lsp-workspace-file-timeouts",
|
|
1121
|
+
smellCount("lsp-workspace-file-timeouts"),
|
|
1122
|
+
`Full LSP sweeps where files hit the per-file budget (${ws.timedOutFilesTotal} files across ${ws.timedOutSweeps} sweeps)`,
|
|
1123
|
+
ws.sweeps.slice(0, limit),
|
|
1124
|
+
);
|
|
1050
1125
|
|
|
1051
1126
|
return {
|
|
1052
1127
|
window: state.window,
|
|
@@ -1071,6 +1146,15 @@ function buildReport(state) {
|
|
|
1071
1146
|
runnerBlockingFindings: state.latency.runnerBlockingFindings.toJSON(),
|
|
1072
1147
|
toolResults: state.latency.toolResults.toJSON(),
|
|
1073
1148
|
phaseCounts: state.latency.phaseCounts.top(limit),
|
|
1149
|
+
phaseTimeouts: state.latency.phaseTimeouts.toJSON(),
|
|
1150
|
+
workspaceDiagnostics: {
|
|
1151
|
+
started: ws.started,
|
|
1152
|
+
completed: ws.completed,
|
|
1153
|
+
incomplete: incompleteSweeps,
|
|
1154
|
+
aborted: ws.aborted,
|
|
1155
|
+
timedOutSweeps: ws.timedOutSweeps,
|
|
1156
|
+
timedOutFilesTotal: ws.timedOutFilesTotal,
|
|
1157
|
+
},
|
|
1074
1158
|
},
|
|
1075
1159
|
cascade: {
|
|
1076
1160
|
phases: state.cascade.phases.toJSON(),
|
|
@@ -1252,6 +1336,20 @@ function printReport(report) {
|
|
|
1252
1336
|
if (Object.keys(ag.errorKinds).length)
|
|
1253
1337
|
console.log(` error kinds: ${JSON.stringify(ag.errorKinds)}`);
|
|
1254
1338
|
}
|
|
1339
|
+
|
|
1340
|
+
const ws = report.latency.workspaceDiagnostics;
|
|
1341
|
+
if (ws && (ws.started || ws.completed)) {
|
|
1342
|
+
console.log("\nLSP workspace sweeps (lens_diagnostics full)");
|
|
1343
|
+
console.log(
|
|
1344
|
+
` started=${ws.started} completed=${ws.completed} incomplete=${ws.incomplete} aborted=${ws.aborted} fileTimeouts=${ws.timedOutFilesTotal} (in ${ws.timedOutSweeps} sweeps)`,
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
const timeouts = Object.entries(report.latency.phaseTimeouts ?? {});
|
|
1348
|
+
if (timeouts.length) {
|
|
1349
|
+
console.log("\nPhase timeouts");
|
|
1350
|
+
for (const [k, v] of timeouts)
|
|
1351
|
+
console.log(` ${String(v).padStart(5)} ${k}`);
|
|
1352
|
+
}
|
|
1255
1353
|
}
|
|
1256
1354
|
|
|
1257
1355
|
function section(title, rows, formatter) {
|