pi-lens 3.8.27 → 3.8.29

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 (114) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/README.md +21 -3
  3. package/clients/architect-client.ts +2 -2
  4. package/clients/ast-grep-client.ts +1 -1
  5. package/clients/ast-grep-parser.ts +1 -1
  6. package/clients/ast-grep-rule-manager.ts +2 -2
  7. package/clients/biome-client.ts +17 -3
  8. package/clients/complexity-client.ts +5 -4
  9. package/clients/dependency-checker.ts +2 -2
  10. package/clients/dispatch/diagnostic-taxonomy.ts +30 -16
  11. package/clients/dispatch/dispatcher.ts +48 -20
  12. package/clients/dispatch/facts/try-catch-facts.ts +4 -3
  13. package/clients/dispatch/integration.ts +59 -34
  14. package/clients/dispatch/rules/error-swallowing.ts +1 -1
  15. package/clients/dispatch/rules/unsafe-boundary.ts +13 -7
  16. package/clients/dispatch/runners/ast-grep-napi.ts +14 -12
  17. package/clients/dispatch/runners/biome-check.ts +65 -31
  18. package/clients/dispatch/runners/eslint.ts +4 -29
  19. package/clients/dispatch/runners/lsp.ts +20 -22
  20. package/clients/dispatch/runners/pyright.ts +6 -3
  21. package/clients/dispatch/runners/shellcheck.ts +3 -13
  22. package/clients/dispatch/runners/tree-sitter.ts +144 -91
  23. package/clients/dispatch/runners/ts-lsp.ts +4 -6
  24. package/clients/dispatch/runners/utils/runner-helpers.ts +53 -11
  25. package/clients/dispatch/types.ts +10 -1
  26. package/clients/file-role.ts +74 -35
  27. package/clients/fix-worklog.ts +5 -2
  28. package/clients/formatters.ts +7 -2
  29. package/clients/installer/index.ts +473 -166
  30. package/clients/jscpd-client.ts +2 -2
  31. package/clients/knip-client.ts +12 -5
  32. package/clients/language-policy.ts +56 -33
  33. package/clients/language-profile.ts +38 -12
  34. package/clients/latency-logger.ts +1 -1
  35. package/clients/log-cleanup.ts +253 -0
  36. package/clients/lsp/client.ts +47 -5
  37. package/clients/lsp/index.ts +109 -66
  38. package/clients/lsp/interactive-install.ts +2 -2
  39. package/clients/lsp/launch.ts +170 -63
  40. package/clients/lsp/server.ts +124 -69
  41. package/clients/native-rust-client.ts +32 -17
  42. package/clients/pipeline.ts +33 -20
  43. package/clients/production-readiness.ts +0 -4
  44. package/clients/project-index.ts +11 -2
  45. package/clients/ruff-client.ts +1 -1
  46. package/clients/runtime-session.ts +69 -171
  47. package/clients/runtime-tool-result.ts +1 -2
  48. package/clients/runtime-turn.ts +51 -11
  49. package/clients/rust-client.ts +2 -2
  50. package/clients/secrets-scanner.ts +33 -2
  51. package/clients/sg-runner.ts +46 -22
  52. package/clients/tree-sitter-client.ts +17 -0
  53. package/clients/tree-sitter-symbol-extractor.ts +12 -3
  54. package/commands/booboo.ts +138 -65
  55. package/config/biome/core.jsonc +1 -3
  56. package/index.ts +502 -487
  57. package/package.json +1 -1
  58. package/rules/ast-grep-rules/rules/unchecked-sync-fs.yml +27 -22
  59. package/rules/tree-sitter-queries/go/go-command-injection.yml +5 -5
  60. package/rules/tree-sitter-queries/go/go-direct-panic.yml +2 -2
  61. package/rules/tree-sitter-queries/go/go-empty-if-err.yml +2 -2
  62. package/rules/tree-sitter-queries/go/go-ignored-call-result.yml +4 -3
  63. package/rules/tree-sitter-queries/go/go-insecure-random.yml +3 -3
  64. package/rules/tree-sitter-queries/go/go-log-fatal.yml +3 -3
  65. package/rules/tree-sitter-queries/go/go-path-traversal.yml +3 -3
  66. package/rules/tree-sitter-queries/go/go-sql-injection.yml +4 -4
  67. package/rules/tree-sitter-queries/go/go-weak-hash.yml +3 -3
  68. package/rules/tree-sitter-queries/python/python-command-injection.yml +7 -7
  69. package/rules/tree-sitter-queries/python/python-cross-language-method.yml +2 -2
  70. package/rules/tree-sitter-queries/python/python-insecure-deserialization.yml +3 -3
  71. package/rules/tree-sitter-queries/python/python-insecure-random.yml +3 -3
  72. package/rules/tree-sitter-queries/python/python-sql-injection.yml +2 -2
  73. package/rules/tree-sitter-queries/python/python-ssrf.yml +3 -3
  74. package/rules/tree-sitter-queries/python/python-subprocess-shell.yml +4 -4
  75. package/rules/tree-sitter-queries/python/python-weak-hash.yml +3 -3
  76. package/rules/tree-sitter-queries/ruby/ruby-insecure-deserialization.yml +3 -3
  77. package/rules/tree-sitter-queries/typescript/console-statement.yml +1 -2
  78. package/rules/tree-sitter-queries/typescript/deep-promise-chain.yml +5 -5
  79. package/rules/tree-sitter-queries/typescript/ts-command-injection.yml +6 -4
  80. package/rules/tree-sitter-queries/typescript/ts-detached-async-call.yml +2 -2
  81. package/rules/tree-sitter-queries/typescript/ts-insecure-random.yml +4 -4
  82. package/rules/tree-sitter-queries/typescript/ts-react-antipatterns.yml +6 -4
  83. package/rules/tree-sitter-queries/typescript/ts-ssrf.yml +4 -3
  84. package/rules/tree-sitter-queries/typescript/ts-weak-hash.yml +3 -3
  85. package/skills/ast-grep/SKILL.md +38 -251
  86. package/clients/auto-loop.ts +0 -200
  87. package/clients/config-validator.ts +0 -558
  88. package/clients/fix-scanners.ts +0 -303
  89. package/clients/scan-architectural-debt.ts +0 -203
  90. package/config/eslint/core.mjs +0 -28
  91. package/rules/ast-grep-rules/rules/empty-catch-js.yml +0 -48
  92. package/rules/ast-grep-rules/rules/empty-catch.yml +0 -48
  93. package/rules/ast-grep-rules/rules/getter-return-js.yml +0 -59
  94. package/rules/ast-grep-rules/rules/getter-return.yml +0 -59
  95. package/rules/ast-grep-rules/rules/in-correct-optional-input-type.yml +0 -63
  96. package/rules/ast-grep-rules/rules/long-method.yml +0 -15
  97. package/rules/ast-grep-rules/rules/missed-concurrency-js.yml +0 -25
  98. package/rules/ast-grep-rules/rules/missed-concurrency.yml +0 -25
  99. package/rules/ast-grep-rules/rules/missing-component-decorator.yml +0 -30
  100. package/rules/ast-grep-rules/rules/no-array-sort-without-comparator-js.yml +0 -8
  101. package/rules/ast-grep-rules/rules/no-array-sort-without-comparator.yml +0 -8
  102. package/rules/ast-grep-rules/rules/no-await-in-loop-js.yml +0 -30
  103. package/rules/ast-grep-rules/rules/no-await-in-loop.yml +0 -46
  104. package/rules/ast-grep-rules/rules/no-constructor-return-js.yml +0 -28
  105. package/rules/ast-grep-rules/rules/no-constructor-return.yml +0 -28
  106. package/rules/ast-grep-rules/rules/no-delete-operator.yml +0 -9
  107. package/rules/ast-grep-rules/rules/no-dupe-args-js.yml +0 -15
  108. package/rules/ast-grep-rules/rules/no-dupe-args.yml +0 -15
  109. package/rules/ast-grep-rules/rules/no-single-char-var.yml +0 -12
  110. package/rules/ast-grep-rules/rules/prefer-async-await-js.yml +0 -13
  111. package/rules/ast-grep-rules/rules/prefer-async-await.yml +0 -13
  112. package/rules/ast-grep-rules/rules/toctou-js.yml +0 -112
  113. package/rules/ast-grep-rules/rules/toctou.yml +0 -112
  114. package/rules/ast-grep-rules/rules/unchecked-throwing-call-ruby.yml +0 -47
package/CHANGELOG.md CHANGED
@@ -2,6 +2,109 @@
2
2
 
3
3
  All notable changes to pi-lens will be documented in this file.
4
4
 
5
+ ## [Unreleased]
6
+
7
+ ## [3.8.29] - 2026-04-21
8
+
9
+ ### Added
10
+ - **New diagnostic commands** — added `/lens-tools` and `/lens-health` for system visibility:
11
+ - `/lens-tools` — shows tool installation status: globally installed, pi-lens auto-installed, or npx fallback
12
+ - `/lens-health` — shows runtime health: pipeline crashes, slow runners, diagnostic stats
13
+ - Both provide actionable visibility into the pi-lens toolchain
14
+ - **Streamlined ast-grep skill** — reduced skill from 7,759 bytes to 2,313 bytes (~70% reduction):
15
+ - Removed verbose CLI tips and YAML rule authoring sections (agent uses tools, not CLI)
16
+ - Removed redundant testing documentation
17
+ - Kept essential: Golden Rules, Quick Reference, Common Gotchas
18
+ - **Configurable log cleanup** — automatic retention and rotation for `~/.pi-lens/*.log` files:
19
+ - Environment variable `PI_LENS_LOG_RETENTION_DAYS` (default: 7) — days to keep log files
20
+ - Environment variable `PI_LENS_MAX_LOG_SIZE_MB` (default: 10) — max size before rotation
21
+ - Runs automatically on session start, notifies when cleanup occurs
22
+ - Rotated backups (`.log.*`) cleaned after retention period
23
+ - Project-level logs (`{cwd}/.pi-lens/*`) intentionally excluded from cleanup
24
+
25
+ ### Changed
26
+ - **`/lens-tools` output improved** — added explanatory note when GitHub-release tools are shown as missing: "GitHub-release tools auto-install when you open files of those languages"
27
+ - **Simplified agent prompts** — removed verbose prompt sections to reduce token burn:
28
+ - Removed startup notes about project rules count (now just logged, not shown)
29
+ - Removed tooling hints for missing language tools (Go/Rust/Ruby install suggestions)
30
+ - Removed project rules section from system prompt (no longer injects `## Project Rules` block)
31
+ - Updated core guidance to clarify: automated checks run on edits/writes, blocking errors shown inline must be fixed
32
+ - **Simplified CLI flags** — removed 16 flags to reduce surface area and cognitive load:
33
+ - Removed per-tool disable flags: `--no-biome`, `--no-ast-grep`, `--no-shellcheck`, `--no-madge`, `--no-oxlint`, `--no-ruff`, `--no-go`, `--no-rust`
34
+ - Removed per-tool autofix flags: `--no-autofix-biome`, `--no-autofix-ruff`
35
+ - Removed feature flags: `--lens-verbose`, `--error-debt`, `--auto-install`, `--lens-eslint-core`
36
+ - Removed redundant `--lens-lsp` flag (LSP is default-on; use `--no-lsp` to disable)
37
+ - Removed internal dead flag: `--lens-blocking-only`
38
+ - **Removed `--no-lsp-install` flag** — LSP servers now always auto-install when needed (no manual opt-out)
39
+ - New minimal flag set: `--no-lsp`, `--no-autoformat`, `--no-autofix`, `--no-tests`, `--no-delta`, `--lens-guard`
40
+ - **Cross-platform line ending handling** — all `.split("\n")` changed to `.split(/\r?\n/)` for Windows CRLF compatibility (11 files updated)
41
+
42
+ ### Fixed
43
+ - **Biome VCS/ignore file errors eliminated** — disabled VCS integration in biome config to prevent "ignore file not found" errors:
44
+ - Changed `vcs.enabled: true` → `vcs.enabled: false` in `config/biome/core.jsonc`
45
+ - Biome was searching for `.gitignore` files that don't exist when running on arbitrary projects via pi-lens
46
+ - Eliminates biome:parse-error spam in logs when biome runs outside its config directory
47
+ - **LSP server thrashing eliminated** — added 240s idle timeout to prevent repeated LSP shutdown/startup cycles:
48
+ - New `scheduleLSPIdleReset()` in `runtime-turn.ts` defers server reset when no files modified
49
+ - Cancel pending reset when active editing resumes (avoids interrupting workflows)
50
+ - Eliminates ~1-2s cold-start penalty during active development sessions
51
+ - Debug logging added for scheduling and cancellation events
52
+ - **Biome check runner JSON parsing** — fixed error where biome's stderr warnings broke JSON parsing:
53
+ - Changed from parsing `stdout || stderr` to parsing `stdout` only
54
+ - Biome outputs text warnings (e.g., "couldn't find ignore file") to stderr which broke the JSON parser
55
+ - Fixes biome-check-json runner failing with parse errors instead of providing lint diagnostics
56
+ - **Auto-install verification gap** — `getToolPath()` now verifies tool binaries actually work before using them:
57
+ - Runs `--version` check on local npm tools (not just file existence)
58
+ - Detects broken/corrupted installations (e.g., wrapper exists but package missing)
59
+ - Triggers automatic reinstall when binary verification fails
60
+ - Fixes case where `@biomejs/biome` package deleted but `.cmd` wrapper remained
61
+ - **Error swallowing in tool availability checks** — `runtime-session.ts` now logs errors when biome/ast-grep/ruff/knip/dep/jscpd availability checks fail (was silently returning `false`)
62
+ - **Biome check runner reliability** — fixed path resolution and configuration issues causing "skipped" status and parse errors:
63
+ - Fixed biome flag: `--output-format=json` → `--reporter=json`
64
+ - Fixed `findBiome()` to check `~/.pi-lens/tools/` directory (was falling back to bare "biome" not in PATH)
65
+ - Fixed `findBiome()` to return `{cmd, argsPrefix}` object for proper npx fallback with `@biomejs/biome` prefix
66
+ - Added `vcs.root: "."` to `config/biome/core.jsonc` to respect project `.gitignore`
67
+ - **LSP error messaging** — improved error messages for Windows .cmd shim failures to distinguish "npm .cmd shim failed (underlying binary not installed)" from "may be missing or corrupted"
68
+ - **Windows installer improvements** — multiple fixes for Windows tool discovery and LSP stability:
69
+ - Prefer `.cmd` over extensionless in local TOOLS_DIR path lookup on Windows
70
+ - Bypass PS1 hangs in LSP initialization with hard-kill on timeout
71
+ - Remove `.ps1` from pyright managed candidates and ast-grep discovery on Windows
72
+ - Use `SYSTEMDRIVE` env var instead of hardcoded `C:` for cargo fallback path
73
+ - **Rust LSP** — exponential backoff circuit breaker for failing LSP connections
74
+ - **Installer reliability** — remove `console.error` verbosity, route all events to `sessionstart.log`
75
+ - **Circular dependencies** — fixed circular dependencies identified in code review
76
+ - **Knip race condition** — fixed race condition in knip tool discovery
77
+ - **Non-blocking tool availability checks** — changed all `ensureAvailable()` methods to use async `safeSpawnAsync` instead of sync `safeSpawn`, completing the startup unblocking work:
78
+ - `ruff-client.ts`, `biome-client.ts`, `sg-runner.ts` (first batch)
79
+ - `knip-client.ts`, `dependency-checker.ts`, `jscpd-client.ts` (second batch)
80
+ - `sg-runner.ts` — added missing `safeSpawnAsync` import
81
+ - **Secrets scanner false positives** — fixed incorrect flagging of environment variable name references (e.g., `"FIREWORKS_API_KEY"`, `"AWS_ACCESS_KEY_ID"`) as hardcoded secrets:
82
+ - Added word boundaries to `hardcoded-secret` regex pattern
83
+ - Added `looksLikeEnvVarName()` filter to skip UPPERCASE_SNAKE_CASE values
84
+ - Prevents false positives when env var names are used as placeholder strings
85
+
86
+ ### Changed
87
+ - **Biome check performance** — reduced lint latency from ~1.4s to ~100ms per file (92% improvement):
88
+ - Removed redundant `--version` pre-check spawn (~200ms saved)
89
+ - Switched from `biome check` to `biome lint` command (skip format validation)
90
+ - Added binary path caching per cwd to avoid repeated fs checks
91
+ - Benchmark: 107ms average vs 1400ms baseline
92
+ - **Tree-sitter performance** — reduced structural analysis latency by 30-50%:
93
+ - Execute queries in parallel with concurrency limit of 6 (was sequential)
94
+ - Skip entity snapshot extraction for changes under 5 lines (~500-800ms saved for trivial edits)
95
+ - Reduces tree-sitter latency from ~3s to ~1-2s for typical files
96
+
97
+ ## [3.8.28] - 2026-04-19
98
+
99
+ ### Fixed
100
+ - **Session startup no longer blocks the Node event loop** — tool availability probes (biome, ast-grep, ruff, knip, jscpd, madge) now run via async `ensureAvailable()` in a fire-and-forget IIFE instead of `setImmediate` + `spawnSync`, eliminating ~8–10 s of main-thread freeze on startup.
101
+ - **Biome binary lookup extended** — `getBiomeBinary()` now checks `~/.pi-lens/tools/node_modules/.bin/biome` so the async probe finds the pre-installed binary without falling back to `npx`.
102
+ - **CSS roots and Windows LSP shims tightened** — improved root resolution for CSS language server on Windows.
103
+ - **Zig compile coverage kept active** — LSP availability check no longer incorrectly disables Zig compile diagnostics.
104
+ - **Ruby LSP startup budgets relaxed** — reduced false-negative LSP attach failures on slower machines.
105
+ - **Kotlin and Zig LSP availability improved** — more reliable server detection across platforms.
106
+ - **Standalone Python and Ruby LSP roots fixed** — correct workspace root used when opening files outside a project directory.
107
+
5
108
  ## [3.8.27] - 2026-04-19
6
109
 
7
110
  ### Added
package/README.md CHANGED
@@ -42,6 +42,7 @@ At `turn_end`, pi-lens:
42
42
  - persists turn findings for next context injection
43
43
  - updates debt/diagnostic tracking and cleans transient state
44
44
  - renders a review-graph impact cascade showing affected files and diagnostic propagation
45
+ - manages LSP server lifecycle with a 240s idle timeout (resets when editing resumes)
45
46
 
46
47
  ## Install
47
48
 
@@ -62,17 +63,22 @@ pi install git:github.com/apmantza/pi-lens
62
63
  pi
63
64
 
64
65
  # Optional switches
65
- pi --no-lsp # Disable unified LSP, use language-specific fallbacks
66
+ pi --no-lsp # Disable unified LSP diagnostics
66
67
  pi --no-autoformat # Skip auto-formatting
67
68
  pi --no-autofix # Skip auto-fix (Biome, Ruff, ESLint, stylelint, sqlfluff, RuboCop)
68
69
  pi --no-tests # Skip test runner
69
- pi --no-shellcheck # Disable shellcheck runner
70
+ pi --no-delta # Disable delta mode (show all diagnostics, not just new ones)
71
+ pi --lens-guard # Block git commit/push when unresolved blockers exist (experimental)
70
72
  ```
71
73
 
74
+ LSP is enabled by default. Use `--no-lsp` to use language-specific fallbacks (ts-lsp, pyright) instead of the unified LSP service.
75
+
72
76
  ## Key Commands
73
77
 
74
78
  - `/lens-booboo` — full quality report for current project state
75
79
  - `/lens-health` — runtime health, latency, and diagnostic telemetry
80
+ - `/lens-tools` — tool installation status: globally installed, auto-installed, or npx fallback
81
+ - `/lens-tdi` — Technical Debt Index (TDI) and project health trend
76
82
 
77
83
  ## Language Coverage
78
84
 
@@ -147,6 +153,8 @@ pi-lens builds a review graph (`file → symbol → dependency`) during session
147
153
 
148
154
  pi-lens includes **37 language server definitions**. LSP is **enabled by default** (`--lsp` or no flag). Servers are auto-discovered from PATH, project `node_modules`, and managed installs. When a server is not installed, pi-lens offers an interactive install prompt.
149
155
 
156
+ **LSP Idle Management:** LSP servers shut down after 240 seconds of inactivity (no files modified) to free resources. The timer resets when you resume editing, preventing cold-start penalties during active development.
157
+
150
158
  LSP servers for: TypeScript, Deno, Python (pyright + pylsp), Go, Rust, Ruby (ruby-lsp + solargraph), PHP, C# (omnisharp), F#, Java, Kotlin, Swift, Dart, Lua, C/C++, Zig, Haskell, Elixir, Gleam, OCaml, Clojure, Terraform, Nix, Bash, Docker, YAML, JSON, HTML, TOML, Prisma, Vue, Svelte, ESLint, CSS.
151
159
 
152
160
  ## Runners
@@ -251,4 +259,14 @@ Additional language servers (gopls, ruby-lsp, solargraph, etc.) are auto-detecte
251
259
 
252
260
  - Not every auto-install runs in every project: gate type decides when install is attempted.
253
261
  - Rule packs are customizable via project-level rule directories.
254
- - Inline suppression: `// pi-lens-ignore` or `# pi-lens-ignore` comments suppress diagnostic output for that line.
262
+ - Inline suppression: `// pi-lens-ignore` or `# pi-lens-ignore` comments suppress diagnostic output for that line.
263
+
264
+ ## Environment Variables
265
+
266
+ | Variable | Default | Description |
267
+ |----------|---------|-------------|
268
+ | `PI_LENS_STARTUP_MODE` | auto | `full`, `minimal`, or `quick` — override session startup behavior |
269
+ | `PI_LENS_LOG_RETENTION_DAYS` | 7 | Days to retain log files before automatic cleanup |
270
+ | `PI_LENS_MAX_LOG_SIZE_MB` | 10 | Max size in MB before rotating active log files |
271
+
272
+ Logs are stored in `~/.pi-lens/` and automatically cleaned up at session start based on these settings.
@@ -187,7 +187,7 @@ export class ArchitectClient {
187
187
  // biome-ignore lint/suspicious/noAssignInExpressions: RegExp.exec iteration
188
188
  while ((match = regex.exec(content)) !== null) {
189
189
  // Convert index to line number
190
- const lineNum = content.slice(0, match.index).split("\n").length;
190
+ const lineNum = content.slice(0, match.index).split(/\r?\n/).length;
191
191
  violations.push({
192
192
  pattern: rule.pattern,
193
193
  message: check.message,
@@ -263,7 +263,7 @@ export class ArchitectClient {
263
263
  const ruleBlocks = content.split(/(?=^ {2}- pattern:)/m);
264
264
 
265
265
  for (const block of ruleBlocks) {
266
- const lines = block.split("\n");
266
+ const lines = block.split(/\r?\n/);
267
267
  let rule: ArchitectRule | null = null;
268
268
  let section: "must_not" | "must" | null = null;
269
269
  let violation: {
@@ -345,7 +345,7 @@ message: found
345
345
  output += ` ${ruleInfo} (${loc})${fix}\n`;
346
346
 
347
347
  if (d.ruleDescription?.note) {
348
- const shortNote = d.ruleDescription.note.split("\n")[0];
348
+ const shortNote = d.ruleDescription.note.split(/\r?\n/)[0];
349
349
  output += ` → ${shortNote}\n`;
350
350
  }
351
351
  }
@@ -51,7 +51,7 @@ export class AstGrepParser {
51
51
  }
52
52
 
53
53
  return output
54
- .split("\n")
54
+ .split(/\r?\n/)
55
55
  .filter((l) => l.trim())
56
56
  .map((line) => {
57
57
  try {
@@ -67,7 +67,7 @@ export class AstGrepRuleManager {
67
67
  );
68
68
  if (noteMatch) {
69
69
  result.note = noteMatch[1]
70
- .split("\n")
70
+ .split(/\r?\n/)
71
71
  .map((line) => line.trim())
72
72
  .filter((line) => line.length > 0)
73
73
  .join(" ");
@@ -82,7 +82,7 @@ export class AstGrepRuleManager {
82
82
  const fixMatch = content.match(/^fix:\s*\|?([\s\S]*?)(?=^\w|^rule:|Z)/m);
83
83
  if (fixMatch) {
84
84
  result.fix = fixMatch[1]
85
- .split("\n")
85
+ .split(/\r?\n/)
86
86
  .map((line) => line.replace(/^\s*\|?\s*/, ""))
87
87
  .filter((line) => line.length > 0)
88
88
  .join("\n");
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import * as fs from "node:fs";
12
+ import * as os from "node:os";
12
13
  import * as path from "node:path";
13
14
  import { isFileKind } from "./file-kinds.js";
14
15
  import { safeSpawn, safeSpawnAsync } from "./safe-spawn.js";
@@ -60,16 +61,29 @@ export class BiomeClient {
60
61
  if (this.localBinaryPath) return { cmd: this.localBinaryPath, args: [] };
61
62
 
62
63
  // Walk up from cwd looking for node_modules/.bin/biome.
64
+ // Also check ~/.pi-lens/tools (where ensureTool("biome") auto-installs),
65
+ // so we avoid the ~1.5s `npx @biomejs/biome --version` fallback when
66
+ // the tool is already installed but not in the project's node_modules.
63
67
  // On Windows prefer .cmd (native batch) over the sh wrapper — 2x faster.
64
68
  const isWin = process.platform === "win32";
69
+ const piLensBin = path.join(
70
+ os.homedir(),
71
+ ".pi-lens",
72
+ "tools",
73
+ "node_modules",
74
+ ".bin",
75
+ );
65
76
  const candidates = isWin
66
77
  ? [
67
78
  path.join(process.cwd(), "node_modules", ".bin", "biome.cmd"),
68
79
  path.join(process.cwd(), "node_modules", ".bin", "biome"),
80
+ path.join(piLensBin, "biome.cmd"),
81
+ path.join(piLensBin, "biome"),
69
82
  ]
70
83
  : [
71
84
  path.join(process.cwd(), "node_modules", ".bin", "biome"),
72
85
  path.join(process.cwd(), "node_modules", ".bin", "biome.cmd"),
86
+ path.join(piLensBin, "biome"),
73
87
  ];
74
88
  for (const p of candidates) {
75
89
  if (fs.existsSync(p)) {
@@ -123,7 +137,7 @@ export class BiomeClient {
123
137
  if (this.biomeAvailable !== null) return this.biomeAvailable;
124
138
 
125
139
  // Check if already available
126
- const result = this.spawnBiome(["--version"], 10000);
140
+ const result = await this.spawnBiomeAsync(["--version"], 10000);
127
141
  if (!result.error && result.status === 0) {
128
142
  this.biomeAvailable = true;
129
143
  return true;
@@ -554,8 +568,8 @@ export class BiomeClient {
554
568
  }
555
569
 
556
570
  private computeDiff(original: string, formatted: string): string {
557
- const origLines = original.split("\n");
558
- const formLines = formatted.split("\n");
571
+ const origLines = original.split(/\r?\n/);
572
+ const formLines = formatted.split(/\r?\n/);
559
573
 
560
574
  let changedLines = 0;
561
575
  const changes: string[] = [];
@@ -213,7 +213,7 @@ export class ComplexityClient {
213
213
  sourceFile: ts.SourceFile;
214
214
  }): FileComplexity {
215
215
  const { absolutePath, content, sourceFile } = parsed;
216
- const lines = content.split("\n");
216
+ const lines = content.split(/\r?\n/);
217
217
 
218
218
  // Line counts and function collection
219
219
  const { codeLines, commentLines } = this.countLines(sourceFile, lines);
@@ -393,7 +393,7 @@ export class ComplexityClient {
393
393
  /\/\*\*?\s*(Overview|Summary|Description|Example|Usage)\s*\*?\//i,
394
394
  ];
395
395
 
396
- const lines = sourceText.split("\n");
396
+ const lines = sourceText.split(/\r?\n/);
397
397
  for (const line of lines) {
398
398
  // Only check comment lines
399
399
  const trimmed = line.trim();
@@ -583,9 +583,10 @@ export class ComplexityClient {
583
583
  let match;
584
584
  while ((match = commentRegex.exec(text)) !== null) {
585
585
  const lineStart = text.lastIndexOf("\n", match.index) + 1;
586
- const startLine = text.substring(0, lineStart).split("\n").length - 1;
586
+ const startLine = text.substring(0, lineStart).split(/\r?\n/).length - 1;
587
587
  const endLine =
588
- text.substring(0, match.index + match[0].length).split("\n").length - 1;
588
+ text.substring(0, match.index + match[0].length).split(/\r?\n/).length -
589
+ 1;
589
590
  for (let i = startLine; i <= endLine; i++) {
590
591
  commentPositions.add(i);
591
592
  }
@@ -11,7 +11,7 @@
11
11
 
12
12
  import * as fs from "node:fs";
13
13
  import * as path from "node:path";
14
- import { safeSpawn } from "./safe-spawn.js";
14
+ import { safeSpawn, safeSpawnAsync } from "./safe-spawn.js";
15
15
 
16
16
  // --- Types ---
17
17
 
@@ -63,7 +63,7 @@ export class DependencyChecker {
63
63
  if (this.available !== null) return this.available;
64
64
 
65
65
  // Check if available in PATH
66
- const result = safeSpawn("madge", ["--version"], {
66
+ const result = await safeSpawnAsync("madge", ["--version"], {
67
67
  timeout: 5000,
68
68
  });
69
69
  this.available = !result.error && result.status === 0;
@@ -1,14 +1,4 @@
1
- import type { Diagnostic } from "./types.js";
2
-
3
- export type DefectClass =
4
- | "silent-error"
5
- | "injection"
6
- | "secrets"
7
- | "async-misuse"
8
- | "correctness"
9
- | "safety"
10
- | "style"
11
- | "unknown";
1
+ import type { DefectClass, Diagnostic } from "./types.js";
12
2
 
13
3
  const SILENT_ERROR_HINTS = [
14
4
  "empty-catch",
@@ -20,9 +10,27 @@ const SILENT_ERROR_HINTS = [
20
10
  "silent",
21
11
  ];
22
12
 
23
- const INJECTION_HINTS = ["sql-injection", "eval", "exec", "inner-html", "javascript-url"];
24
- const SECRET_HINTS = ["secret", "token", "password", "api-key", "hardcoded-secrets"];
25
- const ASYNC_HINTS = ["await-in-loop", "promise", "concurrency", "async", "then-catch"];
13
+ const INJECTION_HINTS = [
14
+ "sql-injection",
15
+ "eval",
16
+ "exec",
17
+ "inner-html",
18
+ "javascript-url",
19
+ ];
20
+ const SECRET_HINTS = [
21
+ "secret",
22
+ "token",
23
+ "password",
24
+ "api-key",
25
+ "hardcoded-secrets",
26
+ ];
27
+ const ASYNC_HINTS = [
28
+ "await-in-loop",
29
+ "promise",
30
+ "concurrency",
31
+ "async",
32
+ "then-catch",
33
+ ];
26
34
 
27
35
  function hasAny(haystack: string, hints: string[]): boolean {
28
36
  return hints.some((h) => haystack.includes(h));
@@ -40,7 +48,11 @@ export function classifyDefect(
40
48
  if (hasAny(text, SECRET_HINTS)) return "secrets";
41
49
  if (hasAny(text, ASYNC_HINTS)) return "async-misuse";
42
50
 
43
- if (text.includes("no-") || text.includes("return") || text.includes("constructor")) {
51
+ if (
52
+ text.includes("no-") ||
53
+ text.includes("return") ||
54
+ text.includes("constructor")
55
+ ) {
44
56
  return "correctness";
45
57
  }
46
58
 
@@ -50,6 +62,8 @@ export function classifyDefect(
50
62
  return "unknown";
51
63
  }
52
64
 
53
- export function classifyDiagnostic(d: Pick<Diagnostic, "rule" | "tool" | "message">): DefectClass {
65
+ export function classifyDiagnostic(
66
+ d: Pick<Diagnostic, "rule" | "tool" | "message">,
67
+ ): DefectClass {
54
68
  return classifyDefect(d.rule, d.tool, d.message);
55
69
  }
@@ -17,15 +17,15 @@
17
17
  import * as path from "node:path";
18
18
  import type { FileKind } from "../file-kinds.js";
19
19
  import { detectFileKind } from "../file-kinds.js";
20
+ import { isTestFile } from "../file-utils.js";
20
21
  import { getPrimaryDispatchGroup } from "../language-policy.js";
21
22
  import { resolveLanguageRootForFile } from "../language-profile.js";
22
- import { isTestFile } from "../file-utils.js";
23
23
  import { logLatency } from "../latency-logger.js";
24
24
  import { normalizeMapKey } from "../path-utils.js";
25
25
  import { RUNTIME_CONFIG } from "../runtime-config.js";
26
26
  import { safeSpawnAsync } from "../safe-spawn.js";
27
27
  import { classifyDiagnostic } from "./diagnostic-taxonomy.js";
28
- import { FactStore } from "./fact-store.js";
28
+ import type { FactStore } from "./fact-store.js";
29
29
  import { getToolPlan } from "./plan.js";
30
30
  import { resolveRunnerPath } from "./runner-context.js";
31
31
  import { getToolProfile } from "./tool-profile.js";
@@ -135,7 +135,7 @@ export function createDispatchContext(
135
135
  cwd: normalizedCwd,
136
136
  kind,
137
137
  pi,
138
- autofix: !!(pi.getFlag("autofix-biome") || pi.getFlag("autofix-ruff")),
138
+ autofix: false,
139
139
  deltaMode: !pi.getFlag("no-delta"),
140
140
  facts,
141
141
  blockingOnly,
@@ -215,7 +215,10 @@ function dedupeOverlappingDiagnostics(diagnostics: Diagnostic[]): Diagnostic[] {
215
215
  * Syntax: `// pi-lens-ignore: rule-id` (JS/TS) or `# pi-lens-ignore: rule-id` (Python/Ruby/etc.)
216
216
  * Place on the same line as the diagnostic or the line immediately above it.
217
217
  */
218
- function applyInlineSuppressions(diagnostics: Diagnostic[], content: string): Diagnostic[] {
218
+ function applyInlineSuppressions(
219
+ diagnostics: Diagnostic[],
220
+ content: string,
221
+ ): Diagnostic[] {
219
222
  if (!content || !diagnostics.length) return diagnostics;
220
223
 
221
224
  // Build a set of (line, ruleId) pairs that are suppressed.
@@ -226,9 +229,12 @@ function applyInlineSuppressions(diagnostics: Diagnostic[], content: string): Di
226
229
  for (let i = 0; i < lines.length; i++) {
227
230
  const m = SUPPRESS_RE.exec(lines[i]);
228
231
  if (!m) continue;
229
- const rules = m[1].split(",").map((r) => r.trim()).filter(Boolean);
232
+ const rules = m[1]
233
+ .split(",")
234
+ .map((r) => r.trim())
235
+ .filter(Boolean);
230
236
  const suppressedLine = i + 1; // same line (1-based)
231
- const nextLine = i + 2; // next line (1-based)
237
+ const nextLine = i + 2; // next line (1-based)
232
238
  for (const ruleId of rules) {
233
239
  suppressed.add(`${suppressedLine}:${ruleId}`);
234
240
  suppressed.add(`${nextLine}:${ruleId}`);
@@ -342,7 +348,7 @@ function buildCoverageNotice(
342
348
  runnerLatencies: RunnerLatency[],
343
349
  ): Diagnostic | undefined {
344
350
  if (!ctx.kind) return undefined;
345
- const lspEnabled = !!ctx.pi.getFlag("lens-lsp") && !ctx.pi.getFlag("no-lsp");
351
+ const lspEnabled = !ctx.pi.getFlag("no-lsp");
346
352
  const primary = getPrimaryDispatchGroup(ctx.kind, lspEnabled);
347
353
  if (!primary || primary.runnerIds.length === 0) return undefined;
348
354
 
@@ -367,7 +373,9 @@ function buildCoverageNotice(
367
373
  (plan?.groups ?? [])
368
374
  .filter(
369
375
  (group) =>
370
- !group.runnerIds.every((runnerId) => primary.runnerIds.includes(runnerId)),
376
+ !group.runnerIds.every((runnerId) =>
377
+ primary.runnerIds.includes(runnerId),
378
+ ),
371
379
  )
372
380
  .flatMap((group) => group.runnerIds)
373
381
  .filter((runnerId) => !primary.runnerIds.includes(runnerId)),
@@ -376,7 +384,12 @@ function buildCoverageNotice(
376
384
  // Structural-only runners (tree-sitter, ast-grep, similarity) are not
377
385
  // substitutes for real linters — don't suppress the notice if only they ran.
378
386
  const STRUCTURAL_RUNNERS = new Set([
379
- "tree-sitter", "ast-grep-napi", "similarity", "spellcheck", "architect", "fact-rules",
387
+ "tree-sitter",
388
+ "ast-grep-napi",
389
+ "similarity",
390
+ "spellcheck",
391
+ "architect",
392
+ "fact-rules",
380
393
  ]);
381
394
  const anyLinterHasCoverage = runnerLatencies.some(
382
395
  (r) =>
@@ -499,7 +512,7 @@ async function runGroup(
499
512
  ? group.runnerIds.filter((id) => {
500
513
  const runner = registry.get(id);
501
514
  return runner && ctx.kind && group.filterKinds?.includes(ctx.kind);
502
- })
515
+ })
503
516
  : group.runnerIds;
504
517
 
505
518
  const semantic = group.semantic ?? "warning";
@@ -668,12 +681,20 @@ export async function dispatchForFile(
668
681
  // This avoids partial-baseline corruption when processing multiple groups.
669
682
  const dedupedDiagnostics = dedupeOverlappingDiagnostics(allDiagnostics);
670
683
  const overlapSuppressed = suppressLintOverlapsWithLsp(dedupedDiagnostics);
671
- const fileContent = ctx.facts.getFileFact<string>(ctx.filePath, "file.content") ?? "";
672
- const inlineSuppressed = applyInlineSuppressions(overlapSuppressed, fileContent);
684
+ const fileContent =
685
+ ctx.facts.getFileFact<string>(ctx.filePath, "file.content") ?? "";
686
+ const inlineSuppressed = applyInlineSuppressions(
687
+ overlapSuppressed,
688
+ fileContent,
689
+ );
673
690
  let visibleDiagnostics = inlineSuppressed;
674
691
  let resolvedCount = 0;
675
692
  if (ctx.deltaMode && previousBaseline) {
676
- const filtered = filterDelta(visibleDiagnostics, previousBaseline, (d) => d.id);
693
+ const filtered = filterDelta(
694
+ visibleDiagnostics,
695
+ previousBaseline,
696
+ (d) => d.id,
697
+ );
677
698
  visibleDiagnostics = promoteDeltaUnusedToBlockers(filtered.new);
678
699
  resolvedCount = filtered.fixed.length;
679
700
  }
@@ -693,15 +714,19 @@ export async function dispatchForFile(
693
714
 
694
715
  // Append fixed and fixable diagnostics to the persistent worklog
695
716
  if (fixedItems.length > 0) {
696
- import("../fix-worklog.js").then(({ appendToWorklog }) => {
697
- appendToWorklog(ctx.cwd, fixedItems, true);
698
- }).catch(() => {});
717
+ import("../fix-worklog.js")
718
+ .then(({ appendToWorklog }) => {
719
+ appendToWorklog(ctx.cwd, fixedItems, true);
720
+ })
721
+ .catch(() => {});
699
722
  }
700
723
  const fixableWarnings = warnings.filter((d) => d.fixable);
701
724
  if (fixableWarnings.length > 0) {
702
- import("../fix-worklog.js").then(({ appendToWorklog }) => {
703
- appendToWorklog(ctx.cwd, fixableWarnings, false);
704
- }).catch(() => {});
725
+ import("../fix-worklog.js")
726
+ .then(({ appendToWorklog }) => {
727
+ appendToWorklog(ctx.cwd, fixableWarnings, false);
728
+ })
729
+ .catch(() => {});
705
730
  }
706
731
 
707
732
  const inlineBlockers = blockers.filter((d) => d.tool !== "similarity");
@@ -804,7 +829,10 @@ function looksLikeDiagnosticCodePath(value: string): boolean {
804
829
  return false;
805
830
  }
806
831
 
807
- function normalizeDiagnosticFilePath(ctx: DispatchContext, rawPath?: string): string {
832
+ function normalizeDiagnosticFilePath(
833
+ ctx: DispatchContext,
834
+ rawPath?: string,
835
+ ): string {
808
836
  if (typeof rawPath === "string" && looksLikeDiagnosticCodePath(rawPath)) {
809
837
  ctx.log(
810
838
  `runner path normalization: ignored diagnostic code-like path '${rawPath}', using current file`,
@@ -39,15 +39,16 @@ const DEFAULT_VALUE_PATTERN =
39
39
  const STRUCTURED_ERROR_PATTERN =
40
40
  /\breturn\s+\{[^}]*(?:success\s*:\s*false|error\s*:)/;
41
41
 
42
- const EXPLAINING_COMMENT_PATTERN =
43
- /\/\/[^\n]*(intentional|expected|ignore|ok|safe|fallback|known|silent|no-op)/i;
42
+ // Any non-trivial comment (≥ 4 non-space chars) counts as documented intent.
43
+ // This covers patterns like: // continue, /* not found */, // best-effort, etc.
44
+ const EXPLAINING_COMMENT_PATTERN = /(?:\/\/\s*\S.{3,}|\/\*\s*\S[\s\S]{3,}?\*\/)/;
44
45
 
45
46
  const FS_PROBE_PATTERN =
46
47
  /\b(?:existsSync|statSync|lstatSync|readFileSync|accessSync)\b/;
47
48
 
48
49
  const DB_PATTERN = /\b(?:query|execute|findOne|findMany|findById|insert|update|delete|select|prisma\.|knex\.|sequelize\.)/;
49
50
  const NETWORK_PATTERN = /\b(?:fetch|axios|http\.|https\.|request\.|got\.|undici\.)/;
50
- const FS_PATTERN = /\b(?:readFile|writeFile|appendFile|readdir|mkdir|stat|unlink|existsSync|readFileSync)\b/;
51
+ const FS_PATTERN = /\b(?:readFileSync?|writeFileSync?|appendFileSync?|readdirSync?|mkdirSync?|statSync?|unlinkSync?|existsSync|accessSync?|copyFileSync?|renameSync?)\b/;
51
52
  const PROCESS_PATTERN = /\b(?:spawn|exec|execSync|spawnSync|child_process\.)\b/;
52
53
 
53
54
  function detectBoundaryCategory(