pi-lens 3.8.63 → 3.8.64

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +12 -9
  3. package/dist/clients/deadline-utils.js +23 -0
  4. package/dist/clients/installer/index.js +18 -27
  5. package/dist/clients/log-cleanup.js +36 -13
  6. package/dist/clients/lsp/client.js +44 -0
  7. package/dist/clients/lsp/index.js +208 -33
  8. package/dist/clients/lsp/interactive-install.js +14 -4
  9. package/dist/clients/lsp/launch.js +29 -100
  10. package/dist/clients/mcp/review.js +7 -4
  11. package/dist/clients/package-manager.js +223 -0
  12. package/dist/clients/pipeline.js +55 -2
  13. package/dist/clients/project-metadata.js +3 -23
  14. package/dist/clients/safe-spawn.js +9 -0
  15. package/dist/clients/tree-sitter-client.js +72 -14
  16. package/dist/index.js +16 -0
  17. package/dist/mcp/server.js +4 -2
  18. package/dist/tools/ast-grep-search.js +10 -6
  19. package/dist/tools/lens-diagnostics.js +43 -10
  20. package/dist/tools/lsp-diagnostics.js +23 -4
  21. package/dist/tools/scan-progress.js +38 -0
  22. package/grammars/tree-sitter-bash.wasm +0 -0
  23. package/grammars/tree-sitter-bash.wasm.json +5 -0
  24. package/grammars/tree-sitter-css.wasm +0 -0
  25. package/grammars/tree-sitter-css.wasm.json +5 -0
  26. package/grammars/tree-sitter-go.wasm +0 -0
  27. package/grammars/tree-sitter-go.wasm.json +5 -0
  28. package/grammars/tree-sitter-html.wasm +0 -0
  29. package/grammars/tree-sitter-html.wasm.json +5 -0
  30. package/grammars/tree-sitter-java.wasm +0 -0
  31. package/grammars/tree-sitter-java.wasm.json +5 -0
  32. package/grammars/tree-sitter-javascript.wasm +0 -0
  33. package/grammars/tree-sitter-javascript.wasm.json +5 -0
  34. package/grammars/tree-sitter-json.wasm +0 -0
  35. package/grammars/tree-sitter-json.wasm.json +5 -0
  36. package/grammars/tree-sitter-python.wasm +0 -0
  37. package/grammars/tree-sitter-python.wasm.json +5 -0
  38. package/grammars/tree-sitter-rust.wasm +0 -0
  39. package/grammars/tree-sitter-rust.wasm.json +5 -0
  40. package/grammars/tree-sitter-tsx.wasm +0 -0
  41. package/grammars/tree-sitter-tsx.wasm.json +5 -0
  42. package/grammars/tree-sitter-typescript.wasm +0 -0
  43. package/grammars/tree-sitter-typescript.wasm.json +5 -0
  44. package/grammars/tree-sitter-yaml.wasm +0 -0
  45. package/grammars/tree-sitter-yaml.wasm.json +5 -0
  46. package/package.json +7 -5
  47. package/scripts/analyze-pi-lens-logs.mjs +101 -3
  48. package/scripts/download-grammars.js +171 -30
  49. 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 } 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
@@ -95,7 +106,7 @@ flushPending = async () => { }) {
95
106
  description: "Filter by severity (default: all).",
96
107
  })),
97
108
  }),
98
- async execute(_toolCallId, params, signal, _onUpdate, ctx) {
109
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
99
110
  const mode = params.mode ?? "delta";
100
111
  const severity = params.severity ?? "all";
101
112
  const refreshRunners = params.refreshRunners;
@@ -105,6 +116,11 @@ flushPending = async () => { }) {
105
116
  const maxProjectFiles = parsePositiveInt(params.maxProjectFiles);
106
117
  const maxLspFiles = parsePositiveInt(params.maxLspFiles);
107
118
  const cwd = ctx.cwd ?? getCwd();
119
+ // Escape aborts the agent *turn*, which fires ctx.signal (the turn-wired
120
+ // abort); the positional signal is the tool-call signal. A registered
121
+ // extension tool only reliably sees the turn abort via ctx.signal, so honor
122
+ // BOTH — else a long mode=full scan ignores Escape (the reported bug).
123
+ const abortSignal = combineAbortSignals(signal, ctx.signal);
108
124
  // Reflect the agent's just-made fixes before reporting: flush pending
109
125
  // per-edit dispatches (re-records fixed files), then drop entries whose
110
126
  // file changed on disk afterwards / was deleted (stale, e.g. external
@@ -115,11 +131,21 @@ flushPending = async () => { }) {
115
131
  return formatAllMode(cwd, severity, undefined, undefined, staleDropped);
116
132
  }
117
133
  if (mode === "full") {
134
+ // Fold a hard wall-clock ceiling into the abort signal so the scan
135
+ // always terminates (partial) even if a hung server would otherwise
136
+ // stall it forever. AbortSignal.timeout aborts with a TimeoutError.
137
+ const ceiling = AbortSignal.timeout(FULL_SCAN_WALL_CLOCK_MS);
138
+ const fullSignal = combineAbortSignals(abortSignal, ceiling);
139
+ // Stream a throttled progress bar: the full scan is opaque for minutes
140
+ // otherwise.
141
+ const onProgress = makeProgressReporter(onUpdate);
118
142
  return formatFullMode(cwd, severity, getLspService(), {
119
143
  refreshRunners,
120
144
  maxProjectFiles,
121
145
  maxLspFiles,
122
- signal,
146
+ signal: fullSignal,
147
+ wallClockMs: FULL_SCAN_WALL_CLOCK_MS,
148
+ onProgress,
123
149
  });
124
150
  }
125
151
  return formatDeltaMode(cacheManager, cwd, severity);
@@ -421,6 +447,7 @@ async function formatFullMode(cwd, severity, lspService, options = {}) {
421
447
  runWorkspaceDiagnostics.call(lspService, cwd, {
422
448
  maxFiles: options.maxLspFiles,
423
449
  signal,
450
+ onProgress: options.onProgress,
424
451
  }),
425
452
  getProjectDiagnosticsSnapshotForFullMode(cwd, options),
426
453
  ]);
@@ -449,16 +476,22 @@ async function formatFullMode(cwd, severity, lspService, options = {}) {
449
476
  turnIndex: projectDelta.turnIndex,
450
477
  },
451
478
  });
452
- // Cancelled mid-scan: the results above are whatever completed before the
453
- // abort. Tell the agent so it doesn't read a partial sweep as "clean" (#341).
479
+ // Stopped mid-scan: the results above are whatever completed before the abort.
480
+ // Tell the agent so it doesn't read a partial sweep as "clean" (#341). The
481
+ // abort is either a user/turn cancel (Escape) or the wall-clock ceiling firing
482
+ // (AbortSignal.timeout → TimeoutError), which guarantees the scan can't hang
483
+ // indefinitely — distinguish them so the agent knows whether to just re-run.
454
484
  if (aborted) {
455
- const note = "\n\n⚠ Scan cancelled before completion — results are partial. " +
456
- "Re-run with a smaller maxLspFiles to finish within budget.";
485
+ const timedOut = signal
486
+ ?.reason?.name === "TimeoutError";
487
+ const note = timedOut
488
+ ? `\n\n⚠ Scan exceeded its ${Math.round((options.wallClockMs ?? 0) / 1000)}s time budget and was stopped — results are partial. ` +
489
+ "Narrow it with maxLspFiles, or raise PI_LENS_LENS_DIAGNOSTICS_FULL_TIMEOUT_MS."
490
+ : "\n\n⚠ Scan cancelled before completion — results are partial. " +
491
+ "Re-run with a smaller maxLspFiles to finish within budget.";
457
492
  return {
458
- content: [
459
- { type: "text", text: result.content[0].text + note },
460
- ],
461
- details: result.details,
493
+ content: [{ type: "text", text: result.content[0].text + note }],
494
+ details: { ...result.details, timedOut },
462
495
  };
463
496
  }
464
497
  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 } 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;
@@ -182,7 +191,13 @@ export function createLspDiagnosticsTool() {
182
191
  description: "Optional per-file LSP wait budget for batch diagnostics. Uses server defaults when omitted.",
183
192
  })),
184
193
  }),
185
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
194
+ async execute(_toolCallId, params, _signal, onUpdate, ctx) {
195
+ // Escape aborts the turn via ctx.signal; honor both it and the tool-call
196
+ // signal so a batch/directory scan cancels rather than grinding on.
197
+ const signal = combineAbortSignals(_signal, ctx.signal);
198
+ // Stream a throttled progress bar for batch/directory scans (opaque for
199
+ // seconds-to-minutes otherwise).
200
+ const onProgress = makeProgressReporter(onUpdate, "Scanning LSP diagnostics");
186
201
  const typedParams = params;
187
202
  const severity = (typedParams.severity ?? "all");
188
203
  const cwd = ctx.cwd ?? process.cwd();
@@ -209,6 +224,8 @@ export function createLspDiagnosticsTool() {
209
224
  return runBatchFileDiagnostics(absPaths, severity, lspService, {
210
225
  concurrency,
211
226
  waitMs,
227
+ signal,
228
+ onProgress,
212
229
  });
213
230
  }
214
231
  const rawPath = typedParams.path;
@@ -244,6 +261,8 @@ export function createLspDiagnosticsTool() {
244
261
  return runDirectoryDiagnostics(absPath, severity, lspService, {
245
262
  concurrency,
246
263
  waitMs,
264
+ signal,
265
+ onProgress,
247
266
  });
248
267
  }
249
268
  return runFileDiagnostics(absPath, severity, lspService, waitMs);
@@ -354,7 +373,7 @@ async function runBatchFileDiagnostics(absPaths, severity, lspService, options)
354
373
  details: { mode: "batch", severity, filesChecked: 0 },
355
374
  };
356
375
  }
357
- const results = await mapWithConcurrency(absPaths, options.concurrency, (file) => collectFileDiagnosticResult(file, severity, lspService, options.waitMs));
376
+ const results = await mapWithConcurrency(absPaths, options.concurrency, (file) => collectFileDiagnosticResult(file, severity, lspService, options.waitMs), options.signal, options.onProgress);
358
377
  const fileErrors = results.flatMap((result) => result.error ? [result.error] : []);
359
378
  const lspHealthWarnings = results.flatMap((result) => result.unavailable ? [result.unavailable] : []);
360
379
  const allDiags = results.flatMap((result) => result.diagnostics);
@@ -436,7 +455,7 @@ async function runDirectoryDiagnostics(absPath, severity, lspService, options) {
436
455
  }
437
456
  const wasCapped = collectedFiles.length > MAX_FILES;
438
457
  const filesToProcess = collectedFiles.slice(0, MAX_FILES);
439
- const results = await mapWithConcurrency(filesToProcess, options.concurrency, (file) => collectFileDiagnosticResult(file, severity, lspService, options.waitMs));
458
+ const results = await mapWithConcurrency(filesToProcess, options.concurrency, (file) => collectFileDiagnosticResult(file, severity, lspService, options.waitMs), options.signal, options.onProgress);
440
459
  const fileErrors = results.flatMap((result) => result.error ? [result.error] : []);
441
460
  const lspHealthWarnings = results.flatMap((result) => result.unavailable ? [result.unavailable] : []);
442
461
  const allDiags = results.flatMap((result) => result.diagnostics);
@@ -0,0 +1,38 @@
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
+ /** A ≤20-char ASCII bar + counts, e.g. `Scanning… [████░░░░░░] 45/123 (37%)`. */
8
+ export function renderScanProgress(completed, total, label = "Scanning project diagnostics") {
9
+ const pct = total > 0 ? Math.min(100, Math.round((completed / total) * 100)) : 0;
10
+ const width = 20;
11
+ const filled = Math.round((pct / 100) * width);
12
+ const bar = "█".repeat(filled) + "░".repeat(Math.max(0, width - filled));
13
+ return `${label}… [${bar}] ${completed}/${total} (${pct}%)`;
14
+ }
15
+ /**
16
+ * Build a throttled `(completed, total) => void` that streams the progress bar to
17
+ * `onUpdate` — at most once per `throttleMs` (default 250ms, ~4×/s) plus the
18
+ * final tick so the bar always lands on 100%. Returns `undefined` when the SDK
19
+ * gave no callback, so callers can pass it straight through as an optional.
20
+ */
21
+ export function makeProgressReporter(onUpdate, label, throttleMs = 250) {
22
+ const emit = onUpdate;
23
+ if (typeof emit !== "function")
24
+ return undefined;
25
+ let lastEmit = 0;
26
+ return (completed, total) => {
27
+ const now = Date.now();
28
+ if (completed < total && now - lastEmit < throttleMs)
29
+ return;
30
+ lastEmit = now;
31
+ emit({
32
+ content: [
33
+ { type: "text", text: renderScanProgress(completed, total, label) },
34
+ ],
35
+ details: { phase: "scanning", completed, total },
36
+ });
37
+ };
38
+ }
Binary file
@@ -0,0 +1,5 @@
1
+ {
2
+ "npmPackage": "tree-sitter-wasms",
3
+ "version": "0.1.13",
4
+ "sha256": "sha256:807dcdb1380a59befb112ed8fbd3d3872c7fadaf5903a769282b50973b30696d"
5
+ }
Binary file
@@ -0,0 +1,5 @@
1
+ {
2
+ "npmPackage": "tree-sitter-wasms",
3
+ "version": "0.1.13",
4
+ "sha256": "sha256:5fc615467b1b98420ed7517e5bf9e1f88468132dd903d842dfb13714f6a1cb0c"
5
+ }
Binary file
@@ -0,0 +1,5 @@
1
+ {
2
+ "npmPackage": "tree-sitter-wasms",
3
+ "version": "0.1.13",
4
+ "sha256": "sha256:9963ca89b616eaf04b08a43bc1fb0f07b85395bec313330851f1f1ead2f755b6"
5
+ }
Binary file
@@ -0,0 +1,5 @@
1
+ {
2
+ "npmPackage": "tree-sitter-wasms",
3
+ "version": "0.1.13",
4
+ "sha256": "sha256:11b3405c1543fb012f5ed7f8ee73125076dce8b168301e1e787e4c717da6b456"
5
+ }
Binary file
@@ -0,0 +1,5 @@
1
+ {
2
+ "npmPackage": "tree-sitter-wasms",
3
+ "version": "0.1.13",
4
+ "sha256": "sha256:637aac4415fb39a211a4f4292d63c66b5ce9c32fa2cd35464af4f681d91b9a1f"
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "npmPackage": "tree-sitter-wasms",
3
+ "version": "0.1.13",
4
+ "sha256": "sha256:63812b9e275d26851264734868d27a1656bd44a2ef6eb3e85e6b03728c595ab5"
5
+ }
Binary file
@@ -0,0 +1,5 @@
1
+ {
2
+ "npmPackage": "tree-sitter-wasms",
3
+ "version": "0.1.13",
4
+ "sha256": "sha256:fdb5219abe058369e16897aaa11eecf47ef4f546752c3ddbac339cdd89e1e667"
5
+ }
Binary file
@@ -0,0 +1,5 @@
1
+ {
2
+ "npmPackage": "tree-sitter-wasms",
3
+ "version": "0.1.13",
4
+ "sha256": "sha256:9056d0fb0c337810d019fae350e8167786119da98f0f282aceae7ab89ee8253b"
5
+ }
Binary file
@@ -0,0 +1,5 @@
1
+ {
2
+ "npmPackage": "tree-sitter-wasms",
3
+ "version": "0.1.13",
4
+ "sha256": "sha256:4409921a70d0aa5bec7d1d7ce809a557a8ee1cf6ace901e3ac6a76e62cfea903"
5
+ }
Binary file
@@ -0,0 +1,5 @@
1
+ {
2
+ "npmPackage": "tree-sitter-wasms",
3
+ "version": "0.1.13",
4
+ "sha256": "sha256:6aa3b2c70e76f5d48eafef1093e9c4de383e13f2fdde2f4e9b98a378f6a8f1b6"
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "npmPackage": "tree-sitter-wasms",
3
+ "version": "0.1.13",
4
+ "sha256": "sha256:8515404dceed38e1ed86aa34b09fcf3379fff1b4ff9dd3967bcd6d1eb5ac3d8f"
5
+ }
Binary file
@@ -0,0 +1,5 @@
1
+ {
2
+ "npmPackage": "tree-sitter-wasms",
3
+ "version": "0.1.13",
4
+ "sha256": "sha256:5dea7cfff83d41d8f87fb8e434e1a5b292c0d670bfcdc42cb2af420ef490dde5"
5
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-lens",
3
- "version": "3.8.63",
3
+ "version": "3.8.64",
4
4
  "type": "module",
5
5
  "description": "Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo",
6
6
  "repository": {
@@ -15,7 +15,7 @@
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
- const task = /session_start task ([^:]+): success \((\d+)ms\)/.exec(
543
- message,
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) {