pi-lens 3.8.46 → 3.8.47

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 (71) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +72 -107
  3. package/clients/actionable-warnings-logger.ts +4 -6
  4. package/clients/actionable-warnings.ts +3 -4
  5. package/clients/biome-client.ts +82 -30
  6. package/clients/cache/rule-cache.ts +4 -1
  7. package/clients/cascade-logger.ts +4 -6
  8. package/clients/cascade-types.ts +19 -0
  9. package/clients/diagnostic-logger.ts +2 -4
  10. package/clients/dispatch/dispatcher.ts +14 -8
  11. package/clients/dispatch/integration.ts +14 -14
  12. package/clients/dispatch/plan.ts +0 -2
  13. package/clients/dispatch/rules/quality-rules.ts +1 -1
  14. package/clients/dispatch/rules/sonar-rules.ts +45 -71
  15. package/clients/dispatch/runners/cpp-check.ts +44 -17
  16. package/clients/dispatch/runners/dart-analyze.ts +101 -8
  17. package/clients/dispatch/runners/detekt.ts +82 -4
  18. package/clients/dispatch/runners/dotnet-build.ts +1 -0
  19. package/clients/dispatch/runners/go-vet.ts +1 -0
  20. package/clients/dispatch/runners/golangci-lint.ts +49 -0
  21. package/clients/dispatch/runners/lsp.ts +8 -0
  22. package/clients/dispatch/runners/markdownlint.ts +44 -0
  23. package/clients/dispatch/runners/mypy.ts +1 -0
  24. package/clients/dispatch/runners/oxlint.ts +84 -20
  25. package/clients/dispatch/runners/pyright.ts +1 -0
  26. package/clients/dispatch/runners/rust-clippy.ts +52 -16
  27. package/clients/dispatch/runners/similarity.ts +5 -10
  28. package/clients/dispatch/runners/sqlfluff.ts +66 -0
  29. package/clients/dispatch/runners/stylelint.ts +44 -0
  30. package/clients/dispatch/runners/swiftlint.ts +71 -1
  31. package/clients/dispatch/runners/tree-sitter.ts +2 -1
  32. package/clients/dispatch/runners/type-safety.ts +3 -0
  33. package/clients/dispatch/runners/utils/diagnostic-parsers.ts +10 -1
  34. package/clients/dispatch/runners/utils/runner-helpers.ts +2 -2
  35. package/clients/dispatch/types.ts +2 -0
  36. package/clients/env-utils.ts +45 -0
  37. package/clients/file-utils.ts +16 -0
  38. package/clients/installer/index.ts +6 -7
  39. package/clients/latency-logger.ts +4 -6
  40. package/clients/lens-config.ts +24 -0
  41. package/clients/log-cleanup.ts +2 -2
  42. package/clients/lsp/index.ts +114 -26
  43. package/clients/lsp/launch.ts +6 -8
  44. package/clients/lsp/server.ts +92 -24
  45. package/clients/path-utils.ts +41 -0
  46. package/clients/pipeline.ts +11 -27
  47. package/clients/project-conventions.ts +215 -0
  48. package/clients/project-snapshot.ts +18 -0
  49. package/clients/read-guard-logger.ts +4 -7
  50. package/clients/read-guard-tool-lines.ts +50 -3
  51. package/clients/read-guard.ts +10 -0
  52. package/clients/review-graph/builder.ts +5 -4
  53. package/clients/ruff-client.ts +17 -2
  54. package/clients/runtime-agent-end.ts +4 -2
  55. package/clients/runtime-config.ts +44 -0
  56. package/clients/runtime-coordinator.ts +19 -14
  57. package/clients/runtime-tool-result.ts +131 -2
  58. package/clients/runtime-turn.ts +13 -2
  59. package/clients/semgrep-config.ts +9 -19
  60. package/clients/sg-runner.ts +18 -2
  61. package/clients/tool-policy.ts +7 -18
  62. package/clients/tree-sitter-logger.ts +4 -6
  63. package/clients/widget-state.ts +257 -63
  64. package/index.ts +8 -0
  65. package/package.json +1 -1
  66. package/rules/ast-grep-rules/rule-schema.json +130 -0
  67. package/rules/tree-sitter-queries/rule-schema.json +136 -0
  68. package/skills/ast-grep/SKILL.md +35 -24
  69. package/skills/lsp-navigation/SKILL.md +52 -68
  70. package/skills/write-ast-grep-rule/SKILL.md +68 -0
  71. package/skills/write-tree-sitter-rule/SKILL.md +86 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,44 @@ All notable changes to pi-lens will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [3.8.47] - 2026-06-01
8
+
9
+ ### Added
10
+
11
+ - **Actionable-warnings ecosystem expansion (closes #112)** — six dispatch runners now propagate `fixable` + a `fixSuggestion` so the actionable-warnings advisory can surface them instead of dropping them into code-quality. rust-clippy and golangci-lint read the structured replacement metadata each tool already publishes (`suggested_replacement` / `Replacement`); sqlfluff, detekt, swiftlint, and dart-analyze use curated allowlists of rules their respective `--fix` / `--auto-correct` / `dart fix --apply` commands rewrite deterministically. oxlint, stylelint, and markdownlint received the same treatment earlier in the cycle. Each slice ships parser-level unit tests against the runner's real output shape.
12
+ - **Framework / convention detector foundation (#118 Phases 1 + 2)** — new `clients/project-conventions.ts` exports `detectProjectConventions(cwd)` returning detected `frameworks` (react / next / vite / vitest in the first cut, each with confidence + signals), `testRunners`, `buildTools`, and `agentDocs`. Detection is purely deterministic — no LLM, no spawn — from `package.json` deps, canonical config files, and directory shape. `ProjectSnapshot` gained an optional `conventions` field with explicit-arg → previously-saved → fresh-detect precedence so a snapshot rewrite without conventions inherits rather than blanks.
13
+ - **Per-runner timeoutMs overrides the global 30 s default (#107)** — each `RunnerDefinition` may now declare its own `timeoutMs`; the dispatch harness honours it instead of the shared `RUNNER_TIMEOUT_FLOOR_MS`. The floor is also configurable via `pi-lens.runnerTimeoutFloorMs` config and `PI_LENS_RUNNER_TIMEOUT_FLOOR_MS` env, guarded against NaN, and lazy-resolved so tests can reset it.
14
+ - **LSP diagnostics-wait cap with env override (#117)** — dispatch LSP wait is now capped at 2.5 s by default to prevent slow language servers from holding edit feedback; tunable via `PI_LENS_LSP_DIAGNOSTICS_MAX_WAIT_MS`. A new `lsp_diagnostics_timeout` phase event and a `diagnosticsTimedOut` flag in the success log surface when the cap fires.
15
+ - **Tool-result debounce window (#115)** — `PI_LENS_TOOL_RESULT_DEBOUNCE_MS` (default 0, max 1 s) coalesces sequential tool_results for the same file so burst edits no longer rerun the full pipeline on every keystroke. Off by default; opt-in via env.
16
+ - **Custom rules guide + JSON schemas** — new docs and JSON schemas for tree-sitter and ast-grep custom rule authoring, plus tightened agent skill docs for write-ast-grep-rule, write-tree-sitter-rule, ast-grep, and lsp-navigation.
17
+ - **Read-guard `oldtext_duplicate` disambiguation** — the first `oldtext_not_found` and every `oldtext_duplicate` now include surrounding line context so the agent can pick the right occurrence without rereading the whole file.
18
+
19
+ ### Performance
20
+
21
+ - **In-flight dedupe on RuffClient and BiomeClient (#120)** — concurrent first-time callers to `ensureAvailable()` now share a single probe + auto-install promise via `ensureInFlight`, mirroring the pattern that closed #113 for SgRunner. Previously two parallel session-start tasks (one Python, one JS/TS) could each race the `ensureTool()` auto-install branch and produce partial state in `~/.pi-lens/tools`.
22
+ - **SgRunner in-flight dedupe (#113)** — concurrent ast-grep `ensureAvailable()` callers now share one probe; the auto-install branch runs at most once across a session.
23
+ - **Centralized `~/.pi-lens` and `walkUpDirs` helpers** — every `~/.pi-lens` computation now routes through `getGlobalPiLensDir()` (#122), and the parent-dir walk is consolidated as a `walkUpDirs` generator + `findNearestContaining` helper in `path-utils.ts`. Same behaviour, fewer ad-hoc walks.
24
+
25
+ ### Fixed
26
+
27
+ - **Cascade reverse-dependency neighbors now use the in-memory index** — the cascade builder was building a reverse-dep index from the review graph, saving it to the project snapshot, then immediately reloading it from disk to compute affected-file neighbors. The reload almost always returned `null` during active editing because the project sequence had advanced past the snapshot sequence, silently discarding the freshly computed data every time. Affected-file queries now run directly against the in-memory index built from the just-completed graph.
28
+ - **Tree-sitter rule cache preserves `has_fix` across the roundtrip** — `has_fix` was set on first load but dropped on the cache rehydration path, so cached runs never marked tree-sitter findings as fixable. Restored — the cache now roundtrips the flag end to end.
29
+ - **TypeScript LSP starts for pi-extension files when only `~/.pi/agent/package.json` exists (#123)** — root detection now performs a bounded walk to the extension boundary; if no marker is found inside that scope, it falls back to `FileDirRoot` provided the agent-level `package.json` exists, instead of silently giving up.
30
+ - **BiomeClient resolves binaries per project cwd, not `process.cwd()` (#121)** — `getBiomeBinary` now accepts a per-call cwd and caches resolved binaries keyed by cwd, so monorepos with sub-package biome installs reach the right binary even when pi-lens was invoked from a different directory.
31
+ - **Skip redundant `notify.open` on `touchFile` when content was already pushed within the debounce window (#116)** — split `shouldSkipTouch` from `shouldSkipNotify`; the latter avoids re-opening but still waits for diagnostics so cache invalidation isn't lost. A `notifySkipped` flag in the latency log records when the optimization fires.
32
+ - **dispatch runner `--version` probes flow through `createAvailabilityChecker`** — cpp-check is now cwd-keyed and dedupes concurrent first-time callers, eliminating one of the hottest uncached spawn paths in the audit.
33
+ - **PILENS_DATA_DIR compliance in actionable-warnings, review-graph, semgrep-config** — these paths now route through `getProjectDataDir(cwd)` instead of hardcoding `.pi-lens/` under cwd, so the data dir override is respected end to end.
34
+ - **Read-guard tracks session writes in an explicit Set** — unreliable mtime checks could let a Write→Edit sequence be blocked by a zero-read violation; an explicit per-session write set is the new authoritative signal.
35
+ - **Read-guard partial apply routes through post-edit analysis** — when only some `oldText` edits resolve, partial application performs exact replacements and then invokes the normal `handleToolResult` pipeline so staleness stamps, modified ranges, deferred formatting, dispatch diagnostics, cascade, and warning collection stay in sync with disk.
36
+ - **Read-guard staleness escalation fires across inter-turn gaps** — `REPEAT_FAILURE_TTL_MS` raised from 30 s to 300 s so repeated stale `oldText` attempts 2–3 minutes apart are still counted as the same streak; at ≥ 2 failures the preflight error is upgraded from `🔄 RETRYABLE` to `🛑 RE-READ REQUIRED`.
37
+ - **dart-analyze / detekt drop the dead sync `isAvailable` fallback** — both runners now use only the async availability check, eliminating a dead code path that masked test-mock mismatches.
38
+ - **ReDoS hotspots in oxlint rule extraction and cors-wildcard patterns** — bounded the affected regexes; the oxlint fix also backfills `defectClass` on five runners that had been missing it.
39
+ - **5 runners that were missing `defectClass`** — backfilled correctness/style/etc. classifications so downstream taxonomy + advisory routing work consistently.
40
+
41
+ ### Widget
42
+
43
+ - **Quieter widget glyphs and tighter horizontal layout** — warning glyph swapped from triangle to exclamation mark, dispatch findings pack into a single horizontal row at normal widths, the red dot now reflects blocking semantics (not just severity), and the divider/filename header / non-blocking fillers in horizontal mode were dropped.
44
+
7
45
  ## [3.8.46] - 2026-05-27
8
46
 
9
47
  ### Added
package/README.md CHANGED
@@ -8,6 +8,22 @@ pi-lens focuses on real-time inline code feedback for AI agents.
8
8
 
9
9
  ## What It Does
10
10
 
11
+ ### At Session Start
12
+
13
+ At `session_start`, pi-lens:
14
+
15
+ - resets runtime state and diagnostic telemetry
16
+ - detects project root, language profile, and active tools
17
+ - applies language-aware startup defaults for tool preinstall
18
+ - warms caches and optional indexes (with overlap/session guardrails)
19
+ - emits missing-tool install hints for detected languages when relevant
20
+ - prepends session guidance before the user's prompt so provider bridges keep the real prompt active
21
+ - opens `warmFiles` (if configured in `.pi-lens/lsp.json`) to seed lazy-indexing language servers like clangd before the first symbol query
22
+
23
+ Startup scan context and language profile are cached in the project snapshot and reused on subsequent `/new` invocations when the project has not changed, avoiding repeated full filesystem walks (~2.5 s saved on medium-to-large projects). Background startup scans are deferred past the interactive session-start path so they do not inflate visible `/new` latency.
24
+
25
+ For one-shot print sessions (for example `pi --print ...`), pi-lens auto-uses a quick startup path that skips heavy bootstrap work to reduce startup latency. Override with `PI_LENS_STARTUP_MODE=full|minimal|quick`.
26
+
11
27
  ### On Write/Edit
12
28
 
13
29
  On every `write` and `edit`, pi-lens runs a fast, language-aware pipeline (checks depend on file language, project config, and installed tools):
@@ -34,22 +50,6 @@ At `agent_end` (once per user prompt, after all agent tool calls complete):
34
50
  - **Conservative LSP warning autofix** — when `actionableWarnings.autoFix.enabled` is set, applies up to 5 preferred LSP quickfixes for warnings flagged in the turn's actionable warnings report. Each fix is re-validated against the live LSP server at apply time, checked for ambiguity (skipped if multiple eligible actions exist), and gated by a safety check before any write occurs. Changed files are registered with the read-guard and cache manager
35
51
  - **Summary notification** — concise status: how many files were formatted, which changed, and whether any formatter failed
36
52
 
37
- ### Session Start
38
-
39
- At `session_start`, pi-lens:
40
-
41
- - resets runtime state and diagnostic telemetry
42
- - detects project root, language profile, and active tools
43
- - applies language-aware startup defaults for tool preinstall
44
- - warms caches and optional indexes (with overlap/session guardrails)
45
- - emits missing-tool install hints for detected languages when relevant
46
- - prepends session guidance before the user's prompt so provider bridges keep the real prompt active
47
- - opens `warmFiles` (if configured in `.pi-lens/lsp.json`) to seed lazy-indexing language servers like clangd before the first symbol query
48
-
49
- Startup scan context and language profile are cached in the project snapshot and reused on subsequent `/new` invocations when the project has not changed, avoiding repeated full filesystem walks (~2.5 s saved on medium-to-large projects). Background startup scans are deferred past the interactive session-start path so they do not inflate visible `/new` latency.
50
-
51
- For one-shot print sessions (for example `pi --print ...`), pi-lens auto-uses a quick startup path that skips heavy bootstrap work to reduce startup latency. Override with `PI_LENS_STARTUP_MODE=full|minimal|quick`.
52
-
53
53
  ### Turn End
54
54
 
55
55
  At `turn_end`, pi-lens:
@@ -159,54 +159,19 @@ Supported: TypeScript, TSX, JavaScript, JSX, Python, Go, Rust, Ruby.
159
159
 
160
160
  Covers JavaScript/TypeScript, Python, Go, Rust, Ruby, Shell, and CMake. A TypeScript AST-based fact-rule engine extracts function-level metrics and evaluates quality and security rules inline. Blocking rules surface immediately at write time; advisory rules are available via `/lens-booboo`.
161
161
 
162
- **Blocking (surface inline at write time):**
163
-
164
- - **cors-wildcard** — `Access-Control-Allow-Origin: *` in server-side code
165
- - **error-swallowing** — empty catch block (skips documented local fallbacks and fs-boundary catches)
166
- - **no-commented-credentials** — password/token/secret in commented-out code
167
- - **high-entropy-string** — string literals with suspiciously high Shannon entropy (possible hardcoded secret)
168
-
169
- **Advisory (accessible via `/lens-booboo`):**
170
-
171
- - **high-complexity** / **no-complex-conditionals** — cyclomatic complexity and deeply nested conditions
172
- - **high-fan-out** — function calls too many distinct functions (coordination smell)
173
- - **unsafe-boundary** — dangerous `any` casts at API boundaries
174
- - **async-noise** / **async-unnecessary-wrapper** — async functions with no await; wrappers that add no value
175
- - **pass-through-wrappers** — trivial wrapper functions
176
- - **dynamic-regexp** — `new RegExp(variable)` (potential ReDoS; complements tree-sitter `unsafe-regex`)
177
- - **jwt-without-verify** — `jwt.sign()` without `jwt.verify()` in the same file
178
- - **missing-error-propagation** — catch blocks that log but don't rethrow
179
- - **error-obscuring** — catch blocks that wrap errors in a different type
180
- - **duplicate-string-literal** / **no-boolean-params** / **high-import-coupling** — code-quality signals
181
-
182
162
  ### Tree-sitter Rules
183
163
 
184
- Structural rules organized by language in `rules/tree-sitter-queries/`. Rules marked **🔴** block the agent inline at write time (only for lines in the current edit); others are advisory.
185
-
186
- **TypeScript (23 rules):**
187
- 🔴 `eval`, `sql-injection`, `ts-command-injection`, `ts-ssrf`, `ts-xss-dom-sink`, `ts-dynamic-require`, `ts-open-redirect`, `ts-nosql-injection`, `ts-weak-hash`, `ts-hallucinated-react-import`, `unsafe-regex`, `debugger`, `default-not-last`, `duplicate-function-arg`, `empty-switch-case`, `infinite-loop`, `self-assignment`, `switch-case-termination`
188
- ⚠️ `console-statement`, `deep-promise-chain`, `mixed-async-styles`, `ts-insecure-random`, `ts-detached-async-call`, `ts-react-antipatterns`, `ts-weak-hash`, `variable-shadowing`
189
-
190
- **Python:** 🔴 `python-command-injection`, `python-sql-injection`, `python-insecure-deserialization`, `python-weak-hash`, `python-hallucinated-import` + 20 advisory rules
191
-
192
- **Go:** 🔴 `go-command-injection`, `go-sql-injection`, `go-shared-map-write-goroutine`, `go-weak-hash` + 13 advisory rules
193
-
194
- **Rust:** 🔴 `rust-lock-held-across-await` + 3 advisory rules (`rust-unsafe-block`, `rust-expect`, `rust-clone-in-loop`)
195
-
196
- **Ruby:** 🔴 `ruby-weak-hash` + 14 advisory rules
164
+ Structural rules organized by language in `rules/tree-sitter-queries/<language>/`. Rules marked **🔴** block the agent inline at write time (only for lines in the current edit); others are advisory.
197
165
 
198
166
  **Suppressing a finding:** add `// pi-lens-ignore: rule-id` on the flagged line or the line above (JS/TS), or `# pi-lens-ignore: rule-id` for Python/Ruby/Shell. This suppresses that specific rule at that location only.
199
167
 
200
- **Project-wide disabling** is not currently supported through config there is no `.pi-lens/disabled-rules` file. Use inline suppression for per-occurrence overrides. When editing pi-lens itself, move a rule file to the `<language>-disabled/` directory to prevent it from running.
168
+ **Bring your own rules:** drop YAML query files into `rules/tree-sitter-queries/<language>/` in your project — pi-lens merges them with the built-ins on session start. The schema, predicates (`eq`, `match`, `any-of`), and `inline_tier` (`blocking` | `warning` | `review`) are documented in [`docs/custom-rules.md`](docs/custom-rules.md). A `rules/tree-sitter-queries/rule-schema.json` JSON Schema is bundled for editor autocomplete via `.vscode/settings.json`.
201
169
 
202
170
  ### Ast-Grep Rules
203
171
 
204
- **180+ rules** in `rules/ast-grep-rules/` across JS, TS, and Python:
172
+ Pattern-based structural rules in `rules/ast-grep-rules/` across JS, TS, and Python — covers security (eval, hardcoded secrets, insecure randomness, dangerous DOM sinks), correctness (strict equality, constant conditions, duplicate keys), code smells (nested ternaries, long parameter lists, redundant state), and agent stubs (unimplemented bodies, raise NotImplementedError).
205
173
 
206
- - **Security**no-eval, jwt-no-verify, no-hardcoded-secrets, no-insecure-randomness, no-inner-html, no-javascript-url, weak-rsa-key
207
- - **Correctness** — strict-equality, no-cond-assign, no-constant-condition, no-dupe-keys, no-nan-comparison, array-callback-return, constructor-super
208
- - **Style/smells** — nested-ternary, long-parameter-list, large-class, prefer-optional-chain, redundant-state, require-await
209
- - **Agent stubs** — no-unimplemented-stub, no-raise-not-implemented, no-ellipsis-body
174
+ **Bring your own rules:** drop YAML rule files into `rules/ast-grep-rules/rules/<id>.yml` in your project pi-lens merges them with the built-ins; same `id` as a built-in overrides it. The supported subset of ast-grep's rule schema (the NAPI runner does not support `inside` / `follows` / `precedes` / `stopBy` / `field` / `nthChild` / `constraints` — use a tree-sitter rule when you need relational context) is documented in [`docs/custom-rules.md`](docs/custom-rules.md), with a `rules/ast-grep-rules/rule-schema.json` JSON Schema for editor autocomplete.
210
175
 
211
176
  ### Semgrep CLI Integration (Experimental)
212
177
 
@@ -238,58 +203,6 @@ metadata:
238
203
  confidence: high
239
204
  ```
240
205
 
241
- ## Dependencies
242
-
243
- Auto-install behavior depends on gate type:
244
-
245
- - **Config-gated**: installs only when project config/deps indicate usage
246
- - **Flow/language-gated**: installs when the runtime path needs it for the current file/session flow
247
- - **Operational prewarm**: installs during session warm scans / turn-end analysis paths
248
- - **GitHub release**: platform-specific binary downloaded from GitHub releases to `~/.pi-lens/bin/`
249
-
250
- | Tool | Purpose | Auto-installed | Gate |
251
- | ----------------------------------- | -------------------------------- | -------------- | ---------------------------------- |
252
- | `@biomejs/biome` | JS/TS lint/format/autofix | Yes | Config-gated |
253
- | `prettier` | Formatting fallback | Yes | Config-gated |
254
- | `yamllint` | YAML linting | Yes | Config-gated |
255
- | `actionlint` | GitHub Actions workflow linting | Yes | GitHub release |
256
- | `sqlfluff` | SQL linting/formatting | Yes | Config-gated |
257
- | `ruff` | Python lint/format/autofix | Yes | Language-default + flow-gated |
258
- | `typescript-language-server` | Unified LSP diagnostics | Yes | Language-default |
259
- | `typescript` | TypeScript compiler | Yes | Language-default |
260
- | `pyright` | Python type diagnostics fallback | Yes | Flow/language-gated |
261
- | `@ast-grep/cli` (sg) | AST scans/search/replace | Yes | Operational prewarm |
262
- | `knip` | Dead code analysis | Yes | Operational prewarm + config-gated |
263
- | `jscpd` | Duplicate code detection | Yes | Operational prewarm + config-gated |
264
- | `madge` | Circular dependency analysis | Yes | Turn-end analysis flow |
265
- | `mypy` | Python type checking | Yes | Flow-gated |
266
- | `stylelint` | CSS/SCSS/Less linting | Yes | Config-gated |
267
- | `markdownlint-cli2` | Markdown linting | Yes | Config-gated |
268
- | `shellcheck` | Shell script linting | Yes | GitHub release |
269
- | `shfmt` | Shell script formatting | Yes | GitHub release |
270
- | `rust-analyzer` | Rust LSP | Yes | GitHub release |
271
- | `golangci-lint` | Go linting | Yes | GitHub release |
272
- | `hadolint` | Dockerfile linting | Yes | GitHub release |
273
- | `ktlint` | Kotlin linting | Yes | GitHub release |
274
- | `tflint` | Terraform linting | Yes | GitHub release |
275
- | `taplo` | TOML linting/formatting | Yes | GitHub release |
276
- | `terraform-ls` | Terraform LSP | Yes | GitHub release |
277
- | `htmlhint` | HTML linting | Yes | Config-gated |
278
- | `@prisma/language-server` | Prisma LSP | Yes | Flow-gated |
279
- | `dockerfile-language-server-nodejs` | Dockerfile LSP | Yes | Flow-gated |
280
- | `intelephense` | PHP LSP | Yes | Flow-gated |
281
- | `bash-language-server` | Bash LSP | Yes | Language-default |
282
- | `yaml-language-server` | YAML LSP | Yes | Language-default |
283
- | `vscode-langservers-extracted` | JSON/ESLint/CSS/HTML LSP | Yes | Language-default |
284
- | `vscode-css-languageserver` | CSS LSP | Yes | Language-default |
285
- | `vscode-html-languageserver-bin` | HTML LSP | Yes | Language-default |
286
- | `svelte-language-server` | Svelte LSP | Yes | Flow-gated |
287
- | `@vue/language-server` | Vue LSP | Yes | Flow-gated |
288
- | `semgrep` | Experimental security dispatch | Manual | Local config / explicit opt-in |
289
- | `psscriptanalyzer` | PowerShell linting | Manual | — |
290
-
291
- Additional language servers (gopls, ruby-lsp, solargraph, etc.) are auto-detected from PATH or installed via native package managers (`go install`, `gem install`) when their language is detected.
292
-
293
206
  ## Run
294
207
 
295
208
  ```bash
@@ -410,3 +323,55 @@ Dispatch is diagnostics-oriented: automatic formatting and safe autofix happen i
410
323
  | Nix | ✓ | lsp | nixfmt |
411
324
  | TOML | ✓ | lsp, taplo | taplo |
412
325
  | CMake | ✓ | lsp | cmake-format |
326
+
327
+ ## Dependencies
328
+
329
+ Auto-install behavior depends on gate type:
330
+
331
+ - **Config-gated**: installs only when project config/deps indicate usage
332
+ - **Flow/language-gated**: installs when the runtime path needs it for the current file/session flow
333
+ - **Operational prewarm**: installs during session warm scans / turn-end analysis paths
334
+ - **GitHub release**: platform-specific binary downloaded from GitHub releases to `~/.pi-lens/bin/`
335
+
336
+ | Tool | Purpose | Auto-installed | Gate |
337
+ | ----------------------------------- | -------------------------------- | -------------- | ---------------------------------- |
338
+ | `@biomejs/biome` | JS/TS lint/format/autofix | Yes | Config-gated |
339
+ | `prettier` | Formatting fallback | Yes | Config-gated |
340
+ | `yamllint` | YAML linting | Yes | Config-gated |
341
+ | `actionlint` | GitHub Actions workflow linting | Yes | GitHub release |
342
+ | `sqlfluff` | SQL linting/formatting | Yes | Config-gated |
343
+ | `ruff` | Python lint/format/autofix | Yes | Language-default + flow-gated |
344
+ | `typescript-language-server` | Unified LSP diagnostics | Yes | Language-default |
345
+ | `typescript` | TypeScript compiler | Yes | Language-default |
346
+ | `pyright` | Python type diagnostics fallback | Yes | Flow/language-gated |
347
+ | `@ast-grep/cli` (sg) | AST scans/search/replace | Yes | Operational prewarm |
348
+ | `knip` | Dead code analysis | Yes | Operational prewarm + config-gated |
349
+ | `jscpd` | Duplicate code detection | Yes | Operational prewarm + config-gated |
350
+ | `madge` | Circular dependency analysis | Yes | Turn-end analysis flow |
351
+ | `mypy` | Python type checking | Yes | Flow-gated |
352
+ | `stylelint` | CSS/SCSS/Less linting | Yes | Config-gated |
353
+ | `markdownlint-cli2` | Markdown linting | Yes | Config-gated |
354
+ | `shellcheck` | Shell script linting | Yes | GitHub release |
355
+ | `shfmt` | Shell script formatting | Yes | GitHub release |
356
+ | `rust-analyzer` | Rust LSP | Yes | GitHub release |
357
+ | `golangci-lint` | Go linting | Yes | GitHub release |
358
+ | `hadolint` | Dockerfile linting | Yes | GitHub release |
359
+ | `ktlint` | Kotlin linting | Yes | GitHub release |
360
+ | `tflint` | Terraform linting | Yes | GitHub release |
361
+ | `taplo` | TOML linting/formatting | Yes | GitHub release |
362
+ | `terraform-ls` | Terraform LSP | Yes | GitHub release |
363
+ | `htmlhint` | HTML linting | Yes | Config-gated |
364
+ | `@prisma/language-server` | Prisma LSP | Yes | Flow-gated |
365
+ | `dockerfile-language-server-nodejs` | Dockerfile LSP | Yes | Flow-gated |
366
+ | `intelephense` | PHP LSP | Yes | Flow-gated |
367
+ | `bash-language-server` | Bash LSP | Yes | Language-default |
368
+ | `yaml-language-server` | YAML LSP | Yes | Language-default |
369
+ | `vscode-langservers-extracted` | JSON/ESLint/CSS/HTML LSP | Yes | Language-default |
370
+ | `vscode-css-languageserver` | CSS LSP | Yes | Language-default |
371
+ | `vscode-html-languageserver-bin` | HTML LSP | Yes | Language-default |
372
+ | `svelte-language-server` | Svelte LSP | Yes | Flow-gated |
373
+ | `@vue/language-server` | Vue LSP | Yes | Flow-gated |
374
+ | `semgrep` | Experimental security dispatch | Manual | Local config / explicit opt-in |
375
+ | `psscriptanalyzer` | PowerShell linting | Manual | — |
376
+
377
+ Additional language servers (gopls, ruby-lsp, solargraph, etc.) are auto-detected from PATH or installed via native package managers (`go install`, `gem install`) when their language is detected.
@@ -1,8 +1,9 @@
1
1
  import * as fs from "node:fs";
2
- import * as os from "node:os";
3
2
  import * as path from "node:path";
3
+ import { isTestMode } from "./env-utils.js";
4
+ import { getGlobalPiLensDir } from "./file-utils.js";
4
5
 
5
- const AW_LOG_DIR = path.join(os.homedir(), ".pi-lens");
6
+ const AW_LOG_DIR = getGlobalPiLensDir();
6
7
  const AW_LOG_FILE = path.join(AW_LOG_DIR, "actionable-warnings.log");
7
8
  const AW_LOG_BACKUP_FILE = path.join(AW_LOG_DIR, "actionable-warnings.log.1");
8
9
  const MAX_LOG_BYTES = Math.max(
@@ -47,10 +48,7 @@ function rotateIfNeeded(): void {
47
48
  export function logActionableWarningsEvent(
48
49
  entry: ActionableWarningsLogEntry,
49
50
  ): void {
50
- if (
51
- process.env.PI_LENS_TEST_MODE === "1" ||
52
- (process.env.VITEST && process.env.PI_LENS_TEST_MODE !== "0")
53
- ) {
51
+ if (isTestMode()) {
54
52
  return;
55
53
  }
56
54
  const line = `${JSON.stringify({ ts: new Date().toISOString(), ...entry })}\n`;
@@ -9,6 +9,7 @@ import { getLSPService } from "./lsp/index.js";
9
9
  import { normalizeMapKey } from "./path-utils.js";
10
10
  import { toRunnerDisplayPath } from "./dispatch/runner-context.js";
11
11
  import { logActionableWarningsEvent } from "./actionable-warnings-logger.js";
12
+ import { getProjectDataDir } from "./file-utils.js";
12
13
 
13
14
  export interface ActionableWarningAction {
14
15
  title: string;
@@ -142,8 +143,7 @@ function serializeAction(action: LSPCodeAction): ActionableWarningAction {
142
143
 
143
144
  function readSuppressionState(cwd: string): WarningStateFile {
144
145
  const statePath = path.join(
145
- cwd,
146
- ".pi-lens",
146
+ getProjectDataDir(cwd),
147
147
  "cache",
148
148
  "actionable-warning-state.json",
149
149
  );
@@ -162,8 +162,7 @@ function updateWarningState(
162
162
  warnings: ActionableWarningRecord[],
163
163
  ): void {
164
164
  const statePath = path.join(
165
- cwd,
166
- ".pi-lens",
165
+ getProjectDataDir(cwd),
167
166
  "cache",
168
167
  "actionable-warning-state.json",
169
168
  );
@@ -32,7 +32,16 @@ export interface BiomeDiagnostic {
32
32
 
33
33
  export class BiomeClient {
34
34
  private biomeAvailable: boolean | null = null;
35
- private localBinaryPath: string | null = null;
35
+ // Per-cwd cache of the resolved biome binary. Keying by cwd matters in
36
+ // monorepos where different sub-packages each ship their own biome
37
+ // installation; sharing one slot across the whole client would cause
38
+ // the first resolution to win and stale across other packages.
39
+ private localBinaryByCwd = new Map<string, string>();
40
+ // The binary path written by `ensureTool("biome")` — genuinely global
41
+ // (lives under ~/.pi-lens/tools), so it's stored separately from the
42
+ // per-cwd cache and used as a final fallback before npx.
43
+ private autoInstalledBinaryPath: string | null = null;
44
+ private ensureInFlight: Promise<boolean> | null = null;
36
45
  private log: (msg: string) => void;
37
46
 
38
47
  constructor(verbose = false) {
@@ -42,12 +51,22 @@ export class BiomeClient {
42
51
  }
43
52
 
44
53
  /**
45
- * Resolve the fastest available biome binary.
54
+ * Resolve the fastest available biome binary for `cwd`.
46
55
  * Prefers local node_modules/.bin/biome (skip npx overhead ~1s).
47
- * Falls back to global biome, then npx.
56
+ * Falls back to ~/.pi-lens/tools, then npx.
57
+ *
58
+ * In monorepos, callers should pass the project / sub-package root for the
59
+ * edited file (typically `path.dirname(absolutePath)`). Omitting `cwd`
60
+ * falls back to `process.cwd()`, which is wrong when pi is invoked from
61
+ * a different directory than the file being edited.
48
62
  */
49
- private getBiomeBinary(): { cmd: string; args: string[] } {
50
- if (this.localBinaryPath) return { cmd: this.localBinaryPath, args: [] };
63
+ private getBiomeBinary(cwd?: string): { cmd: string; args: string[] } {
64
+ const resolveCwd = cwd ?? process.cwd();
65
+ const cached = this.localBinaryByCwd.get(resolveCwd);
66
+ if (cached) return { cmd: cached, args: [] };
67
+ if (this.autoInstalledBinaryPath) {
68
+ return { cmd: this.autoInstalledBinaryPath, args: [] };
69
+ }
51
70
 
52
71
  // Walk up from cwd looking for node_modules/.bin/biome.
53
72
  // Also check ~/.pi-lens/tools (where ensureTool("biome") auto-installs),
@@ -64,19 +83,19 @@ export class BiomeClient {
64
83
  );
65
84
  const candidates = isWin
66
85
  ? [
67
- path.join(process.cwd(), "node_modules", ".bin", "biome.cmd"),
68
- path.join(process.cwd(), "node_modules", ".bin", "biome"),
86
+ path.join(resolveCwd, "node_modules", ".bin", "biome.cmd"),
87
+ path.join(resolveCwd, "node_modules", ".bin", "biome"),
69
88
  path.join(piLensBin, "biome.cmd"),
70
89
  path.join(piLensBin, "biome"),
71
90
  ]
72
91
  : [
73
- path.join(process.cwd(), "node_modules", ".bin", "biome"),
74
- path.join(process.cwd(), "node_modules", ".bin", "biome.cmd"),
92
+ path.join(resolveCwd, "node_modules", ".bin", "biome"),
93
+ path.join(resolveCwd, "node_modules", ".bin", "biome.cmd"),
75
94
  path.join(piLensBin, "biome"),
76
95
  ];
77
96
  for (const p of candidates) {
78
97
  if (fs.existsSync(p)) {
79
- this.localBinaryPath = p;
98
+ this.localBinaryByCwd.set(resolveCwd, p);
80
99
  return { cmd: p, args: [] };
81
100
  }
82
101
  }
@@ -85,15 +104,20 @@ export class BiomeClient {
85
104
  }
86
105
 
87
106
  /**
88
- * Spawn biome with the fastest available binary.
107
+ * Spawn biome with the fastest available binary for `cwd`.
108
+ * Pass the file's project root in monorepos so the per-package binary wins.
89
109
  */
90
- private spawnBiome(args: string[], timeout = 15000) {
91
- const { cmd, args: prefix } = this.getBiomeBinary();
110
+ private spawnBiome(args: string[], timeout = 15000, cwd?: string) {
111
+ const { cmd, args: prefix } = this.getBiomeBinary(cwd);
92
112
  return safeSpawn(cmd, [...prefix, ...args], { timeout });
93
113
  }
94
114
 
95
- private async spawnBiomeAsync(args: string[], timeout = 15000) {
96
- const { cmd, args: prefix } = this.getBiomeBinary();
115
+ private async spawnBiomeAsync(
116
+ args: string[],
117
+ timeout = 15000,
118
+ cwd?: string,
119
+ ) {
120
+ const { cmd, args: prefix } = this.getBiomeBinary(cwd);
97
121
  return safeSpawnAsync(cmd, [...prefix, ...args], { timeout });
98
122
  }
99
123
 
@@ -121,10 +145,25 @@ export class BiomeClient {
121
145
  /**
122
146
  * Ensure Biome is available, auto-installing if necessary.
123
147
  * Prefer this over isAvailable() for auto-install behavior.
148
+ *
149
+ * Re-entrancy safe: concurrent first-time callers share a single
150
+ * `ensureInFlight` promise so probing/auto-install isn't duplicated.
151
+ * Mirrors the dedupe pattern in `SgRunner` / `KnipClient` /
152
+ * `DependencyChecker`.
124
153
  */
125
154
  async ensureAvailable(): Promise<boolean> {
126
155
  if (this.biomeAvailable !== null) return this.biomeAvailable;
156
+ if (this.ensureInFlight) return this.ensureInFlight;
157
+
158
+ this.ensureInFlight = this.doEnsureAvailable();
159
+ try {
160
+ return await this.ensureInFlight;
161
+ } finally {
162
+ this.ensureInFlight = null;
163
+ }
164
+ }
127
165
 
166
+ private async doEnsureAvailable(): Promise<boolean> {
128
167
  // Check if already available
129
168
  const result = await this.spawnBiomeAsync(["--version"], 10000);
130
169
  if (!result.error && result.status === 0) {
@@ -139,8 +178,9 @@ export class BiomeClient {
139
178
 
140
179
  if (installedPath) {
141
180
  this.log(`Biome auto-installed: ${installedPath}`);
142
- // Set the installed path as local binary to avoid npx overhead
143
- this.localBinaryPath = installedPath;
181
+ // Set the installed path as the global fallback so every cwd
182
+ // reaches it after its own per-package lookup misses.
183
+ this.autoInstalledBinaryPath = installedPath;
144
184
  this.biomeAvailable = true;
145
185
  return true;
146
186
  }
@@ -179,12 +219,16 @@ export class BiomeClient {
179
219
  if (!absolutePath) return [];
180
220
 
181
221
  try {
182
- const result = this.spawnBiome([
183
- "check",
184
- "--reporter=json",
185
- "--max-diagnostics=50",
186
- absolutePath,
187
- ]);
222
+ const result = this.spawnBiome(
223
+ [
224
+ "check",
225
+ "--reporter=json",
226
+ "--max-diagnostics=50",
227
+ absolutePath,
228
+ ],
229
+ 15000,
230
+ path.dirname(absolutePath),
231
+ );
188
232
 
189
233
  // Biome exits 0 on success, 1 on issues found
190
234
  const output = result.stdout || "";
@@ -218,7 +262,11 @@ export class BiomeClient {
218
262
  const content = fs.readFileSync(absolutePath, "utf-8");
219
263
 
220
264
  try {
221
- const result = this.spawnBiome(["format", "--write", absolutePath]);
265
+ const result = this.spawnBiome(
266
+ ["format", "--write", absolutePath],
267
+ 15000,
268
+ path.dirname(absolutePath),
269
+ );
222
270
 
223
271
  if (result.error) {
224
272
  return { success: false, changed: false, error: result.error.message };
@@ -266,7 +314,11 @@ export class BiomeClient {
266
314
  // lint --write applies safe lint fixes only — no formatting.
267
315
  // Formatting is deferred to agent_end to avoid mid-turn file modifications
268
316
  // that trigger read-guard "file modified since read" blocks.
269
- const result = this.spawnBiome(["lint", "--write", absolutePath]);
317
+ const result = this.spawnBiome(
318
+ ["lint", "--write", absolutePath],
319
+ 15000,
320
+ path.dirname(absolutePath),
321
+ );
270
322
 
271
323
  if (result.error) {
272
324
  return {
@@ -325,11 +377,11 @@ export class BiomeClient {
325
377
 
326
378
  try {
327
379
  const before = await fs.promises.readFile(absolutePath, "utf-8");
328
- const result = await this.spawnBiomeAsync([
329
- "lint",
330
- "--write",
331
- absolutePath,
332
- ]);
380
+ const result = await this.spawnBiomeAsync(
381
+ ["lint", "--write", absolutePath],
382
+ 15000,
383
+ path.dirname(absolutePath),
384
+ );
333
385
 
334
386
  if (result.error) {
335
387
  return {
@@ -10,7 +10,7 @@ import * as fs from "node:fs";
10
10
  import * as path from "node:path";
11
11
  import { getProjectDataDir } from "../file-utils.js";
12
12
 
13
- const CACHE_VERSION = "v2";
13
+ const CACHE_VERSION = "v3";
14
14
 
15
15
  export interface QueryCacheEntry {
16
16
  version: string;
@@ -27,6 +27,9 @@ export interface QueryCacheEntry {
27
27
  post_filter?: string;
28
28
  // biome-ignore lint/suspicious/noExplicitAny: Flexible filter params
29
29
  post_filter_params?: Record<string, any>;
30
+ defect_class?: string;
31
+ inline_tier?: "blocking" | "warning" | "review";
32
+ has_fix?: boolean;
30
33
  filePath?: string;
31
34
  }>;
32
35
  }
@@ -1,8 +1,9 @@
1
1
  import * as fs from "node:fs";
2
- import * as os from "node:os";
3
2
  import * as path from "node:path";
3
+ import { isTestMode } from "./env-utils.js";
4
+ import { getGlobalPiLensDir } from "./file-utils.js";
4
5
 
5
- const CASCADE_LOG_DIR = path.join(os.homedir(), ".pi-lens");
6
+ const CASCADE_LOG_DIR = getGlobalPiLensDir();
6
7
  const CASCADE_LOG_FILE = path.join(CASCADE_LOG_DIR, "cascade.log");
7
8
 
8
9
  try {
@@ -63,10 +64,7 @@ export interface CascadeLogEntry {
63
64
  }
64
65
 
65
66
  export function logCascade(entry: CascadeLogEntry): void {
66
- if (
67
- process.env.PI_LENS_TEST_MODE === "1" ||
68
- (process.env.VITEST && process.env.PI_LENS_TEST_MODE !== "0")
69
- ) {
67
+ if (isTestMode()) {
70
68
  return;
71
69
  }
72
70
  const line = `${JSON.stringify({ ts: new Date().toISOString(), ...entry })}\n`;
@@ -15,3 +15,22 @@ export interface CascadeResult {
15
15
  neighbors: CascadeNeighborResult[];
16
16
  formatted: string;
17
17
  }
18
+
19
+ /** Why a cascade run produced no formatted output. */
20
+ export type CascadeSkipReason =
21
+ | "blockers" // primary file had blocking diagnostics
22
+ | "non_code" // file kind not eligible for cascade
23
+ | "no_neighbors" // reverse-dep lookup found no importing files
24
+ | "clean"; // neighbors found but none had new diagnostics
25
+
26
+ /**
27
+ * Always-present result of one computeCascadeForFile invocation.
28
+ * result is defined only when formatted output was produced.
29
+ */
30
+ export interface CascadeRun {
31
+ filePath: string;
32
+ result: CascadeResult | undefined;
33
+ neighborCount: number;
34
+ diagnosticCount: number;
35
+ skipReason?: CascadeSkipReason;
36
+ }
@@ -7,6 +7,7 @@
7
7
  import * as fs from "node:fs";
8
8
  import * as os from "node:os";
9
9
  import * as path from "node:path";
10
+ import { isTestMode } from "./env-utils.js";
10
11
 
11
12
  export interface DiagnosticEntry {
12
13
  // When
@@ -108,10 +109,7 @@ export function createDiagnosticLogger(): DiagnosticLogger {
108
109
 
109
110
  return {
110
111
  log(entry: DiagnosticEntry) {
111
- if (
112
- process.env.PI_LENS_TEST_MODE === "1" ||
113
- (process.env.VITEST && process.env.PI_LENS_TEST_MODE !== "0")
114
- ) {
112
+ if (isTestMode()) {
115
113
  return;
116
114
  }
117
115
  pending.push(entry);