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.
Files changed (49) hide show
  1. package/CHANGELOG.md +32 -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 +49 -10
  20. package/dist/tools/lsp-diagnostics.js +27 -4
  21. package/dist/tools/scan-progress.js +53 -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 +9 -7
  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, 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, _onUpdate, ctx) {
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
- // 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).
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 note = "\n\n⚠ Scan cancelled before completion — results are partial. " +
456
- "Re-run with a smaller maxLspFiles to finish within budget.";
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
- { type: "text", text: result.content[0].text + note },
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, _onUpdate, ctx) {
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
@@ -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.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": "./dist/mcp/server.js",
13
- "pi-lens-analyze": "./dist/mcp/analyze-cli.js"
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
- 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) {