pi-lens 3.8.34 → 3.8.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,46 @@
2
2
 
3
3
  All notable changes to pi-lens will be documented in this file.
4
4
 
5
+ ## [Unreleased]
6
+
7
+ ## [3.8.36] - 2026-05-02
8
+
9
+ ### Changed
10
+
11
+ - **`agent_end` deferred format notification now lists filenames** — the notification now reads `pi-lens deferred format applied to N file(s): foo.ts, bar.ts` instead of just the count, making it immediately clear which files were reformatted without needing to check logs.
12
+
13
+ ### Added
14
+
15
+ - **Deferred formatting by default** — files touched by `write` and `edit` are now queued and formatted once at `agent_end` instead of immediately after each edit. This prevents mid-task formatting mutations from invalidating read-guard context and interrupting multi-edit flows. Formatting still runs in real time when `--immediate-format` is passed.
16
+ - **`agent_end` lifecycle handler** — new `clients/runtime-agent-end.ts` drains the deferred format queue at the end of each agent turn, runs the formatter once per file, syncs formatted content to LSP, and emits a concise notification.
17
+ - **`--immediate-format` flag** — opt-in flag to restore the legacy per-edit formatting behavior.
18
+ - **`/lens-health` session timestamp** — output now opens with `Session started: HH:MM (Xh Ym ago)` so all session-scoped counters have clear time context.
19
+ - **`/lens-health` LSP status section** — shows each currently running language server with a `✓`/`✗` connected indicator and workspace root. Makes dead servers immediately visible to the agent without needing to check logs. Also fixes `LSPService.getStatus()` which previously hardcoded `connected: true` instead of calling `isAlive()`.
20
+ - **`/lens-health` cascade summary** — shows session-total cascade runs, diagnostics surfaced, and cold-snapshot touches (the new active-touch fallback for TypeScript neighbors with no snapshot).
21
+ - **`/lens-health` i18n** — localizes status labels with English fallback; es, fr, and pt-BR strings included (PR #45 by @jerryfan).
22
+ - **`/lens-booboo` language gates** — Knip (dead code), Madge (circular deps), and type coverage now skip on non-JS/TS projects. Compiler checks extended with Java (mvn/gradle), C# (dotnet build), Dart, Gleam, Zig, and Elixir alongside the existing TypeScript, Go, Rust, Ruby, and Python checks.
23
+ - **`project-metadata` detects 8 new languages** — Java, Kotlin, C#, Dart, Gleam, Zig, Elixir, and C++ are now detected from their project markers (pom.xml, build.gradle.kts, \*.sln, pubspec.yaml, gleam.toml, build.zig, mix.exs, CMakeLists.txt). All runners and booboo language gates now work correctly for these languages.
24
+ - **4 new formatters** — `google-java-format` (config-gated via `.editorconfig` or `.google-java-format`), `cljfmt` (config-gated via `.cljfmt.edn`), `cmake-format` (config-gated via `.cmake-format`), and `PSScriptAnalyzer` formatter for PowerShell (smart-default when PSScriptAnalyzer module is available).
25
+ - **Startup pre-install defaults for shell, Ruby, Kotlin, TOML** — `shellcheck`, `rubocop`, `ktlint`, and `taplo` are now pre-installed fire-and-forget at session start for matching projects, consistent with the existing pattern for `typescript-language-server`, `biome`, `pyright`, `ruff`, `yamllint`, and `sqlfluff`. No latency impact — all installs are fire-and-forget and no-ops when already cached.
26
+
27
+ ### Fixed
28
+
29
+ - **Installer race condition** — coalesced the entire `ensureTool()` operation (not just the install phase) to prevent duplicate concurrent "auto-install ensure X: start" probes when multiple tools race to resolve the same binary.
30
+ - **Read-expansion union bug** — tree-sitter read expansion now returns the union of the requested range and the enclosing symbol range, instead of silently dropping originally requested prefix/suffix lines. Fixes false "Edit outside read range" blocks when an agent reads a partial range inside a large symbol.
31
+ - **Startup probe deduplication** — removed broad eager probes for biome, ast-grep, ruff, knip, jscpd, and madge at session start. Replaced with `scheduleDeferredToolProbes()` which only probes tools not already covered by preinstall or startup scans, scoped to the project's actual language profile.
32
+ - **ReDoS-safe compiler output parsers in `/lens-booboo`** — five regex patterns in the compiler checks (Maven, Gradle, .NET, Gleam, Elixir) flagged by SonarCloud as vulnerable to super-linear backtracking (S5852). Fixed: `mvnRe` and `gradleRe` replaced greedy `(.+)$` with `([^\n]+)` and dropped the end anchor; `csRe` replaced lazy `([^[]+?)` with greedy `([^[]+)`; `gleamRe` replaced `(.+?)` with `([^:]+)`; `elixirRe` replaced the multiline regex entirely with a line-by-line parser to eliminate the flagged pattern.
33
+ - **Cascade diagnostics now surface for TypeScript neighbors on cold sessions** — previously cascade silently returned zero diagnostics for TypeScript/Deno neighbors when no passive snapshot existed (i.e. the agent had not yet opened the file). Cold-snapshot neighbors now fall through into the parallel `touchFile` pool with a 1000ms budget (tighter than the 2000ms used for non-jsts neighbors, since the TypeScript server is expected to be warm). Valid snapshots still use the fast read path with no touch. New `coldSnapshot: true` field on `neighbor_touch` log entries tracks these in `cascade.log`.
34
+
35
+ ### Improved
36
+
37
+ - **`ast-grep` skill clarifies string literal behaviour** — exact string literals in patterns (e.g. `from "./utils"`) work correctly; only metavariables inside string literals (e.g. `from "$PATH"`) are not supported and should use grep instead. Previously the skill incorrectly implied import path matching was unsupported entirely, causing unnecessary grep fallbacks.
38
+
39
+ ## [3.8.35] - 2026-05-02
40
+
41
+ ### Fixed
42
+
43
+ - **Startup hang for all users fixed (issue #46)** — `igniteWarmFiles` was previously `await`ed unconditionally on the session-start path, causing every session to pay the cost of a full directory walk looking for `lsp.json` (checking 3 config paths at every ancestor up to the filesystem root) before returning. This caused the 20–30s startup delay reported in 3.8.34 regardless of whether `warmFiles` was configured. The `loadLSPConfig` call now runs with `await` at the call site; if `warmFiles` is absent or empty, `igniteWarmFiles` is skipped entirely. When warm files are configured, the per-file LSP `touchFile` loop runs fire-and-forget so it never blocks session completion.
44
+
5
45
  ## [3.8.34] - 2026-05-01
6
46
 
7
47
  ### Added
package/README.md CHANGED
@@ -1,3 +1,7 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/apmantza/pi-lens/master/banner.svg" alt="pi-lens" width="1100">
3
+ </p>
4
+
1
5
  # pi-lens
2
6
 
3
7
  pi-lens focuses on real-time inline code feedback for AI agents.
@@ -9,17 +13,25 @@ pi-lens focuses on real-time inline code feedback for AI agents.
9
13
  On every `write` and `edit`, pi-lens runs a fast, language-aware pipeline (checks depend on file language, project config, and installed tools):
10
14
 
11
15
  1. **Secrets scan** — blocking; aborts the write if credentials are detected
12
- 2. **Auto-format** — 26 language-specific formatters (Biome, Prettier, Ruff, gofmt, rustfmt, and 21 others)
16
+ 2. **Auto-format** — deferred to `agent_end` by default; queued files are formatted once after all agent tool calls complete. Use `--immediate-format` for per-edit formatting
13
17
  3. **Auto-fix** — safe autofixes from 6 tools (Biome `check --write`, Ruff `check --fix`, ESLint `--fix`, stylelint `--fix`, sqlfluff `fix`, RuboCop `-a`) applied before analysis
14
18
  4. **LSP file sync** — opens/updates the file in active language servers
15
19
  5. **Dispatch lint** — parallel runner groups: LSP diagnostics, tree-sitter structural rules, ast-grep security/correctness rules, fact rules, language-specific linters, similarity detection
16
20
  6. **Cascade diagnostics** — review-graph impact cascade showing which other files were affected and how diagnostics propagated
17
21
 
18
22
  Results are inline and actionable:
23
+
19
24
  - **Blocking issues** — stop progress until fixed
20
25
  - **Warnings** — summarized inline, detail in `/lens-booboo`
21
26
  - **Health/telemetry** — available in `/lens-health`
22
27
 
28
+ ### Agent End
29
+
30
+ At `agent_end` (once per user prompt, after all agent tool calls complete):
31
+
32
+ - **Deferred formatting** — any files queued during the turn are formatted once, synced to LSP, and tracked for read-guard coverage
33
+ - **Summary notification** — concise status: how many files were formatted, which changed, and whether any formatter failed
34
+
23
35
  ### Session Start
24
36
 
25
37
  At `session_start`, pi-lens:
@@ -80,6 +92,7 @@ pi-lens auto-detects and runs **26 formatters** based on project config:
80
92
  biome, prettier, ruff, black, sqlfluff, gofmt, rustfmt, zig fmt, dart format, shfmt, nixfmt, mix format, ocamlformat, clang-format, ktlint, rubocop, standardrb, gleam format, terraform fmt, php-cs-fixer, csharpier, fantomas, swiftformat, stylua, ormolu, taplo
81
93
 
82
94
  Detection rules:
95
+
83
96
  - **Config-gated**: only runs when project config indicates usage (e.g. `biome.json`, `.prettierrc`, `ruff.toml`)
84
97
  - **Nearest-wins**: when multiple formatter configs exist at different directory levels, the one closest to the edited file wins
85
98
  - **Biome-default**: for JS/TS files without Prettier or Biome config, Biome is used as the default formatter
@@ -149,44 +162,44 @@ Auto-install behavior depends on gate type:
149
162
  - **Operational prewarm**: installs during session warm scans / turn-end analysis paths
150
163
  - **GitHub release**: platform-specific binary downloaded from GitHub releases to `~/.pi-lens/bin/`
151
164
 
152
- | Tool | Purpose | Auto-installed | Gate |
153
- |---|---|---|---|
154
- | `@biomejs/biome` | JS/TS lint/format/autofix | Yes | Config-gated |
155
- | `prettier` | Formatting fallback | Yes | Config-gated |
156
- | `yamllint` | YAML linting | Yes | Config-gated |
157
- | `sqlfluff` | SQL linting/formatting | Yes | Config-gated |
158
- | `ruff` | Python lint/format/autofix | Yes | Language-default + flow-gated |
159
- | `typescript-language-server` | Unified LSP diagnostics | Yes | Language-default |
160
- | `typescript` | TypeScript compiler | Yes | Language-default |
161
- | `pyright` | Python type diagnostics fallback | Yes | Flow/language-gated |
162
- | `@ast-grep/cli` (sg) | AST scans/search/replace | Yes | Operational prewarm |
163
- | `knip` | Dead code analysis | Yes | Operational prewarm + config-gated |
164
- | `jscpd` | Duplicate code detection | Yes | Operational prewarm + config-gated |
165
- | `madge` | Circular dependency analysis | Yes | Turn-end analysis flow |
166
- | `mypy` | Python type checking | Yes | Flow-gated |
167
- | `stylelint` | CSS/SCSS/Less linting | Yes | Config-gated |
168
- | `markdownlint-cli2` | Markdown linting | Yes | Config-gated |
169
- | `shellcheck` | Shell script linting | Yes | GitHub release |
170
- | `shfmt` | Shell script formatting | Yes | GitHub release |
171
- | `rust-analyzer` | Rust LSP | Yes | GitHub release |
172
- | `golangci-lint` | Go linting | Yes | GitHub release |
173
- | `hadolint` | Dockerfile linting | Yes | GitHub release |
174
- | `ktlint` | Kotlin linting | Yes | GitHub release |
175
- | `tflint` | Terraform linting | Yes | GitHub release |
176
- | `taplo` | TOML linting/formatting | Yes | GitHub release |
177
- | `terraform-ls` | Terraform LSP | Yes | GitHub release |
178
- | `htmlhint` | HTML linting | Yes | Config-gated |
179
- | `@prisma/language-server` | Prisma LSP | Yes | Flow-gated |
180
- | `dockerfile-language-server-nodejs` | Dockerfile LSP | Yes | Flow-gated |
181
- | `intelephense` | PHP LSP | Yes | Flow-gated |
182
- | `bash-language-server` | Bash LSP | Yes | Language-default |
183
- | `yaml-language-server` | YAML LSP | Yes | Language-default |
184
- | `vscode-langservers-extracted` | JSON/ESLint/CSS/HTML LSP | Yes | Language-default |
185
- | `vscode-css-languageserver` | CSS LSP | Yes | Language-default |
186
- | `vscode-html-languageserver-bin` | HTML LSP | Yes | Language-default |
187
- | `svelte-language-server` | Svelte LSP | Yes | Flow-gated |
188
- | `@vue/language-server` | Vue LSP | Yes | Flow-gated |
189
- | `psscriptanalyzer` | PowerShell linting | Manual | — |
165
+ | Tool | Purpose | Auto-installed | Gate |
166
+ | ----------------------------------- | -------------------------------- | -------------- | ---------------------------------- |
167
+ | `@biomejs/biome` | JS/TS lint/format/autofix | Yes | Config-gated |
168
+ | `prettier` | Formatting fallback | Yes | Config-gated |
169
+ | `yamllint` | YAML linting | Yes | Config-gated |
170
+ | `sqlfluff` | SQL linting/formatting | Yes | Config-gated |
171
+ | `ruff` | Python lint/format/autofix | Yes | Language-default + flow-gated |
172
+ | `typescript-language-server` | Unified LSP diagnostics | Yes | Language-default |
173
+ | `typescript` | TypeScript compiler | Yes | Language-default |
174
+ | `pyright` | Python type diagnostics fallback | Yes | Flow/language-gated |
175
+ | `@ast-grep/cli` (sg) | AST scans/search/replace | Yes | Operational prewarm |
176
+ | `knip` | Dead code analysis | Yes | Operational prewarm + config-gated |
177
+ | `jscpd` | Duplicate code detection | Yes | Operational prewarm + config-gated |
178
+ | `madge` | Circular dependency analysis | Yes | Turn-end analysis flow |
179
+ | `mypy` | Python type checking | Yes | Flow-gated |
180
+ | `stylelint` | CSS/SCSS/Less linting | Yes | Config-gated |
181
+ | `markdownlint-cli2` | Markdown linting | Yes | Config-gated |
182
+ | `shellcheck` | Shell script linting | Yes | GitHub release |
183
+ | `shfmt` | Shell script formatting | Yes | GitHub release |
184
+ | `rust-analyzer` | Rust LSP | Yes | GitHub release |
185
+ | `golangci-lint` | Go linting | Yes | GitHub release |
186
+ | `hadolint` | Dockerfile linting | Yes | GitHub release |
187
+ | `ktlint` | Kotlin linting | Yes | GitHub release |
188
+ | `tflint` | Terraform linting | Yes | GitHub release |
189
+ | `taplo` | TOML linting/formatting | Yes | GitHub release |
190
+ | `terraform-ls` | Terraform LSP | Yes | GitHub release |
191
+ | `htmlhint` | HTML linting | Yes | Config-gated |
192
+ | `@prisma/language-server` | Prisma LSP | Yes | Flow-gated |
193
+ | `dockerfile-language-server-nodejs` | Dockerfile LSP | Yes | Flow-gated |
194
+ | `intelephense` | PHP LSP | Yes | Flow-gated |
195
+ | `bash-language-server` | Bash LSP | Yes | Language-default |
196
+ | `yaml-language-server` | YAML LSP | Yes | Language-default |
197
+ | `vscode-langservers-extracted` | JSON/ESLint/CSS/HTML LSP | Yes | Language-default |
198
+ | `vscode-css-languageserver` | CSS LSP | Yes | Language-default |
199
+ | `vscode-html-languageserver-bin` | HTML LSP | Yes | Language-default |
200
+ | `svelte-language-server` | Svelte LSP | Yes | Flow-gated |
201
+ | `@vue/language-server` | Vue LSP | Yes | Flow-gated |
202
+ | `psscriptanalyzer` | PowerShell linting | Manual | — |
190
203
 
191
204
  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.
192
205
 
@@ -198,7 +211,8 @@ pi
198
211
 
199
212
  # Optional switches
200
213
  pi --no-lsp # Disable unified LSP diagnostics
201
- pi --no-autoformat # Skip auto-formatting
214
+ pi --no-autoformat # Skip auto-formatting entirely
215
+ pi --immediate-format # Format immediately after each edit instead of deferring to agent_end
202
216
  pi --no-autofix # Skip auto-fix (Biome, Ruff, ESLint, stylelint, sqlfluff, RuboCop)
203
217
  pi --no-tests # Skip test runner
204
218
  pi --no-delta # Disable delta mode (show all diagnostics, not just new ones)
@@ -232,39 +246,39 @@ Formatting uses a single selected formatter per file: explicit project config wi
232
246
 
233
247
  Dispatch is diagnostics-oriented: automatic formatting and safe autofix happen in the post-write pipeline rather than through dispatch format-check runners.
234
248
 
235
- | Language | LSP | Dispatch Runners | Formatter |
236
- |---|---|---|---|
237
- | JavaScript/TypeScript | ✓ | lsp, ts-lsp, biome-check-json, tree-sitter, ast-grep-napi, type-safety, similarity, fact-rules, eslint, oxlint | biome, prettier |
238
- | Python | ✓ | lsp, pyright, ruff-lint, tree-sitter, python-slop | ruff, black |
239
- | Go | ✓ | lsp, go-vet, golangci-lint, tree-sitter | gofmt |
240
- | Rust | ✓ | lsp, rust-clippy, tree-sitter | rustfmt |
241
- | Ruby | ✓ | lsp, rubocop, tree-sitter | rubocop, standardrb |
242
- | C/C++ | ✓ | lsp, cpp-check | clang-format |
243
- | Shell | ✓ | lsp, shellcheck | shfmt |
244
- | CSS/SCSS/Less | ✓ | lsp, stylelint | biome, prettier |
245
- | HTML | ✓ | lsp, htmlhint | prettier |
246
- | YAML | ✓ | lsp, yamllint | prettier |
247
- | JSON | ✓ | lsp | biome, prettier |
248
- | SQL | — | sqlfluff | sqlfluff |
249
- | Markdown | — | spellcheck, markdownlint | prettier |
250
- | Docker | ✓ | lsp, hadolint | — |
251
- | PHP | ✓ | lsp, php-lint, phpstan | php-cs-fixer |
252
- | PowerShell | ✓ | lsp, psscriptanalyzer | — |
253
- | Prisma | ✓ | lsp, prisma-validate | — |
254
- | C# | ✓ | lsp, dotnet-build | csharpier |
255
- | F# | ✓ | lsp | fantomas |
256
- | Java | ✓ | lsp, javac | — |
257
- | Kotlin | ✓ | lsp, ktlint | ktlint |
258
- | Swift | ✓ | lsp | swiftformat |
259
- | Dart | ✓ | lsp, dart-analyze | dart format |
260
- | Lua | ✓ | lsp | stylua |
261
- | Zig | ✓ | lsp, zig-check | zig fmt |
262
- | Haskell | ✓ | lsp | ormolu |
263
- | Elixir | ✓ | lsp, elixir-check, credo | mix format |
264
- | Gleam | ✓ | lsp, gleam-check | gleam format |
265
- | OCaml | ✓ | lsp | ocamlformat |
266
- | Clojure | ✓ | lsp | — |
267
- | Terraform | ✓ | lsp, tflint | terraform fmt |
268
- | Nix | ✓ | lsp | nixfmt |
269
- | TOML | ✓ | lsp, taplo | taplo |
270
- | CMake | ✓ | lsp | — |
249
+ | Language | LSP | Dispatch Runners | Formatter |
250
+ | --------------------- | --- | -------------------------------------------------------------------------------------------------------------- | ------------------- |
251
+ | JavaScript/TypeScript | ✓ | lsp, ts-lsp, biome-check-json, tree-sitter, ast-grep-napi, type-safety, similarity, fact-rules, eslint, oxlint | biome, prettier |
252
+ | Python | ✓ | lsp, pyright, ruff-lint, tree-sitter, python-slop | ruff, black |
253
+ | Go | ✓ | lsp, go-vet, golangci-lint, tree-sitter | gofmt |
254
+ | Rust | ✓ | lsp, rust-clippy, tree-sitter | rustfmt |
255
+ | Ruby | ✓ | lsp, rubocop, tree-sitter | rubocop, standardrb |
256
+ | C/C++ | ✓ | lsp, cpp-check | clang-format |
257
+ | Shell | ✓ | lsp, shellcheck | shfmt |
258
+ | CSS/SCSS/Less | ✓ | lsp, stylelint | biome, prettier |
259
+ | HTML | ✓ | lsp, htmlhint | prettier |
260
+ | YAML | ✓ | lsp, yamllint | prettier |
261
+ | JSON | ✓ | lsp | biome, prettier |
262
+ | SQL | — | sqlfluff | sqlfluff |
263
+ | Markdown | — | spellcheck, markdownlint | prettier |
264
+ | Docker | ✓ | lsp, hadolint | — |
265
+ | PHP | ✓ | lsp, php-lint, phpstan | php-cs-fixer |
266
+ | PowerShell | ✓ | lsp, psscriptanalyzer | — |
267
+ | Prisma | ✓ | lsp, prisma-validate | — |
268
+ | C# | ✓ | lsp, dotnet-build | csharpier |
269
+ | F# | ✓ | lsp | fantomas |
270
+ | Java | ✓ | lsp, javac | — |
271
+ | Kotlin | ✓ | lsp, ktlint | ktlint |
272
+ | Swift | ✓ | lsp | swiftformat |
273
+ | Dart | ✓ | lsp, dart-analyze | dart format |
274
+ | Lua | ✓ | lsp | stylua |
275
+ | Zig | ✓ | lsp, zig-check | zig fmt |
276
+ | Haskell | ✓ | lsp | ormolu |
277
+ | Elixir | ✓ | lsp, elixir-check, credo | mix format |
278
+ | Gleam | ✓ | lsp, gleam-check | gleam format |
279
+ | OCaml | ✓ | lsp | ocamlformat |
280
+ | Clojure | ✓ | lsp | — |
281
+ | Terraform | ✓ | lsp, tflint | terraform fmt |
282
+ | Nix | ✓ | lsp | nixfmt |
283
+ | TOML | ✓ | lsp, taplo | taplo |
284
+ | CMake | ✓ | lsp | — |
package/banner.svg ADDED
@@ -0,0 +1,73 @@
1
+ <svg width="1100" height="280" viewBox="0 0 1100 280" xmlns="http://www.w3.org/2000/svg">
2
+ <defs>
3
+ <linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
4
+ <stop offset="0%" stop-color="#0d1117"/>
5
+ <stop offset="100%" stop-color="#161b22"/>
6
+ </linearGradient>
7
+ <linearGradient id="accent" x1="0%" y1="0%" x2="100%" y2="100%">
8
+ <stop offset="0%" stop-color="#2f81f7"/>
9
+ <stop offset="100%" stop-color="#58a6ff"/>
10
+ </linearGradient>
11
+ <linearGradient id="accentFade" x1="0%" y1="0%" x2="100%" y2="0%">
12
+ <stop offset="0%" stop-color="#2f81f7"/>
13
+ <stop offset="100%" stop-color="#2f81f700"/>
14
+ </linearGradient>
15
+ <clipPath id="bounds">
16
+ <rect width="1100" height="280" rx="12"/>
17
+ </clipPath>
18
+ </defs>
19
+
20
+ <!-- Background -->
21
+ <rect width="1100" height="280" fill="url(#bg)" rx="12"/>
22
+
23
+ <!-- Top accent line -->
24
+ <rect x="0" y="0" width="1100" height="3" fill="url(#accent)" rx="1.5" clip-path="url(#bounds)"/>
25
+
26
+ <!-- Decorative aperture rings (right side) -->
27
+ <g opacity="0.07" clip-path="url(#bounds)">
28
+ <circle cx="920" cy="140" r="180" fill="none" stroke="#2f81f7" stroke-width="1.5"/>
29
+ <circle cx="920" cy="140" r="145" fill="none" stroke="#2f81f7" stroke-width="1"/>
30
+ <circle cx="920" cy="140" r="110" fill="none" stroke="#2f81f7" stroke-width="1"/>
31
+ <circle cx="920" cy="140" r="75" fill="none" stroke="#2f81f7" stroke-width="1"/>
32
+ <line x1="740" y1="140" x2="1100" y2="140" stroke="#2f81f7" stroke-width="0.8"/>
33
+ <line x1="920" y1="-40" x2="920" y2="320" stroke="#2f81f7" stroke-width="0.8"/>
34
+ <line x1="793" y1="13" x2="1047" y2="267" stroke="#2f81f7" stroke-width="0.6"/>
35
+ <line x1="1047" y1="13" x2="793" y2="267" stroke="#2f81f7" stroke-width="0.6"/>
36
+ </g>
37
+
38
+ <!-- Lens icon -->
39
+ <!-- Outer ring glow -->
40
+ <circle cx="142" cy="130" r="70" fill="#2f81f708"/>
41
+ <!-- Lens circle -->
42
+ <circle cx="142" cy="130" r="63" fill="#2f81f710" stroke="url(#accent)" stroke-width="4.5"/>
43
+ <!-- Inner ring -->
44
+ <circle cx="142" cy="130" r="48" fill="none" stroke="#2f81f740" stroke-width="1.5"/>
45
+ <!-- Handle -->
46
+ <line x1="191" y1="179" x2="222" y2="214" stroke="url(#accent)" stroke-width="8" stroke-linecap="round"/>
47
+ <!-- Code brackets inside lens -->
48
+ <text x="110" y="145" font-family="'Courier New', Courier, monospace" font-size="34" font-weight="700" fill="#58a6ff">&lt;/&gt;</text>
49
+
50
+ <!-- pi-lens wordmark -->
51
+ <text x="258" y="122" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, sans-serif" font-size="74" font-weight="700" fill="#e6edf3" letter-spacing="-2">pi-lens</text>
52
+
53
+ <!-- Tagline -->
54
+ <text x="261" y="164" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, sans-serif" font-size="21" font-weight="400" fill="#8b949e" letter-spacing="0.3">Real-time code intelligence for AI agents</text>
55
+
56
+ <!-- Feature pills -->
57
+ <g font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, sans-serif" font-size="12" font-weight="500">
58
+ <rect x="261" y="196" width="56" height="24" rx="12" fill="#2f81f715" stroke="#2f81f740" stroke-width="1"/>
59
+ <text x="289" y="213" fill="#58a6ff" text-anchor="middle">LSP</text>
60
+
61
+ <rect x="327" y="196" width="68" height="24" rx="12" fill="#2f81f715" stroke="#2f81f740" stroke-width="1"/>
62
+ <text x="361" y="213" fill="#58a6ff" text-anchor="middle">Linters</text>
63
+
64
+ <rect x="405" y="196" width="90" height="24" rx="12" fill="#2f81f715" stroke="#2f81f740" stroke-width="1"/>
65
+ <text x="450" y="213" fill="#58a6ff" text-anchor="middle">Formatters</text>
66
+
67
+ <rect x="505" y="196" width="152" height="24" rx="12" fill="#2f81f715" stroke="#2f81f740" stroke-width="1"/>
68
+ <text x="581" y="213" fill="#58a6ff" text-anchor="middle">Structural Analysis</text>
69
+
70
+ <rect x="667" y="196" width="96" height="24" rx="12" fill="#2f81f715" stroke="#2f81f740" stroke-width="1"/>
71
+ <text x="715" y="213" fill="#58a6ff" text-anchor="middle">Read-Guard</text>
72
+ </g>
73
+ </svg>
@@ -49,6 +49,7 @@ export interface CascadeLogEntry {
49
49
  lspServerCount?: number; // number of LSP servers configured for this file type
50
50
  touchedCount?: number;
51
51
  snapshotCount?: number;
52
+ coldSnapshot?: boolean; // true when touch was triggered because autoPropagate snapshot was missing
52
53
 
53
54
  // shared
54
55
  fallbackUsed?: boolean;
@@ -357,6 +357,13 @@ export function resetDispatchBaselines(): void {
357
357
  neighborTouchCache.clear();
358
358
  primaryFilesThisTurn.clear();
359
359
  cascadeDiagnosticBaselines.clear();
360
+ cascadeSessionStats = { runs: 0, diagnosticsSurfaced: 0, coldSnapshotTouches: 0 };
361
+ }
362
+
363
+ let cascadeSessionStats = { runs: 0, diagnosticsSurfaced: 0, coldSnapshotTouches: 0 };
364
+
365
+ export function getCascadeSessionStats(): { runs: number; diagnosticsSurfaced: number; coldSnapshotTouches: number } {
366
+ return { ...cascadeSessionStats };
360
367
  }
361
368
 
362
369
  // A5: per-turn neighbor-touch cache keyed by normalized path.
@@ -496,6 +503,7 @@ export async function computeCascadeForFile(
496
503
 
497
504
  const neighbors: CascadeResult["neighbors"] = [];
498
505
  let producedLspData = false;
506
+ let coldSnapshotPaths: string[] = [];
499
507
 
500
508
  if (sortedNeighbors.length > 0) {
501
509
  const snapshotPaths = sortedNeighbors.filter(shouldReadCascadeFromSnapshot);
@@ -504,6 +512,10 @@ export async function computeCascadeForFile(
504
512
  );
505
513
 
506
514
  // Auto-propagating LSPs (TypeScript/Deno) — read passive snapshot with normalized key.
515
+ // When the snapshot is valid, use it immediately (no touch needed — server already has
516
+ // fresh data from auto-propagation). When missing or stale, fall through to the active
517
+ // touch pool below so we get real diagnostics instead of silently returning zero.
518
+ coldSnapshotPaths = [];
507
519
  for (const neighborPath of snapshotPaths) {
508
520
  const neighborStart = Date.now();
509
521
  const entry = allDiags.get(normalizeMapKey(neighborPath));
@@ -512,14 +524,30 @@ export async function computeCascadeForFile(
512
524
  : undefined;
513
525
  const snapshotValid =
514
526
  entry != null && Date.now() - entry.ts < CASCADE_TTL_MS;
515
- const diags = snapshotValid
516
- ? convertLspDiagnostics(
517
- entry.diags.filter((d) => d.severity === 1).slice(0, MAX_PER_FILE),
518
- neighborPath,
519
- { source: "cascade" },
520
- )
521
- : [];
522
- if (snapshotValid) producedLspData = true;
527
+
528
+ if (!snapshotValid) {
529
+ // No usable snapshot — queue for active touch alongside non-jsts neighbors.
530
+ logCascade({
531
+ phase: "neighbor_snapshot",
532
+ filePath,
533
+ neighborFile: neighborPath,
534
+ diagnosticCount: 0,
535
+ durationMs: Date.now() - neighborStart,
536
+ autoPropagate: true,
537
+ snapshotMissing: entry == null,
538
+ snapshotAgeSec,
539
+ coldSnapshot: true,
540
+ });
541
+ coldSnapshotPaths.push(neighborPath);
542
+ continue;
543
+ }
544
+
545
+ const diags = convertLspDiagnostics(
546
+ entry.diags.filter((d) => d.severity === 1).slice(0, MAX_PER_FILE),
547
+ neighborPath,
548
+ { source: "cascade" },
549
+ );
550
+ producedLspData = true;
523
551
  const durationMs = Date.now() - neighborStart;
524
552
 
525
553
  logCascade({
@@ -529,7 +557,7 @@ export async function computeCascadeForFile(
529
557
  diagnosticCount: diags.length,
530
558
  durationMs,
531
559
  autoPropagate: true,
532
- snapshotMissing: entry == null,
560
+ snapshotMissing: false,
533
561
  snapshotAgeSec,
534
562
  });
535
563
 
@@ -542,10 +570,13 @@ export async function computeCascadeForFile(
542
570
  });
543
571
  }
544
572
 
545
- // non-jsts: fan-out active touches in parallel (A3), single wait per neighbor.
546
- // touchFile("none") opens the doc; getDiagnostics waits once for fresh results.
573
+ // fan-out active touches in parallel (A3):
574
+ // - non-jsts neighbors (always touched)
575
+ // - autoPropagate neighbors whose snapshot was missing/stale (coldSnapshotPaths)
576
+ // use a tighter 1000ms budget since the server is expected to be warm already.
547
577
  const touchResults = await Promise.allSettled(
548
- activePaths.map(async (neighborPath) => {
578
+ [...activePaths, ...coldSnapshotPaths].map(async (neighborPath) => {
579
+ const isColdSnapshot = coldSnapshotPaths.includes(neighborPath);
549
580
  const neighborStart = Date.now();
550
581
  const cacheKey = normalizeMapKey(neighborPath);
551
582
 
@@ -595,10 +626,13 @@ export async function computeCascadeForFile(
595
626
  const content = await nodeFs.promises.readFile(neighborPath, "utf8");
596
627
  // Open with silent=true (suppresses didChangeWatchedFiles rechecks, C2)
597
628
  // and collect diagnostics from the same touched clients.
629
+ // Cold-snapshot neighbors (autoPropagate LSP, server warm) use a tighter
630
+ // 1000ms budget — they should respond quickly; we'd rather return zero
631
+ // than block cascade for 2s on a slow open.
598
632
  const rawDiags = await lspService.touchFile(neighborPath, content, {
599
633
  diagnostics: "document",
600
634
  collectDiagnostics: true,
601
- maxClientWaitMs: 2000,
635
+ maxClientWaitMs: isColdSnapshot ? 1000 : 2000,
602
636
  silent: true,
603
637
  source: "cascade",
604
638
  clientScope: "all",
@@ -629,6 +663,7 @@ export async function computeCascadeForFile(
629
663
  durationMs,
630
664
  lspTouched: true,
631
665
  lspServerCount: configuredServerCount,
666
+ coldSnapshot: isColdSnapshot,
632
667
  });
633
668
 
634
669
  return {
@@ -641,9 +676,10 @@ export async function computeCascadeForFile(
641
676
  }),
642
677
  );
643
678
 
679
+ const allTouchPaths = [...activePaths, ...coldSnapshotPaths];
644
680
  for (let i = 0; i < touchResults.length; i++) {
645
681
  const result = touchResults[i];
646
- const neighborPath = activePaths[i];
682
+ const neighborPath = allTouchPaths[i];
647
683
  if (result.status === "fulfilled") {
648
684
  if (result.value) neighbors.push(result.value);
649
685
  } else {
@@ -722,6 +758,13 @@ export async function computeCascadeForFile(
722
758
  },
723
759
  });
724
760
 
761
+ cascadeSessionStats.runs += 1;
762
+ cascadeSessionStats.diagnosticsSurfaced += visibleNeighbors.reduce(
763
+ (sum, n) => sum + n.diagnostics.length,
764
+ 0,
765
+ );
766
+ cascadeSessionStats.coldSnapshotTouches += coldSnapshotPaths.length;
767
+
725
768
  if (!formatted) return undefined;
726
769
 
727
770
  getDiagnosticTracker().trackShown(
@@ -20,6 +20,9 @@ import {
20
20
  hasBiomeConfig,
21
21
  hasBlackConfig,
22
22
  hasClangFormatConfig,
23
+ hasCljfmtConfig,
24
+ hasCmakeFormatConfig,
25
+ hasGoogleJavaFormatConfig,
23
26
  hasNearestPackageJsonDependency,
24
27
  hasNearestPackageJsonField,
25
28
  hasOcamlformatConfig,
@@ -296,6 +299,12 @@ function hasExplicitFormatterConfig(
296
299
  return hasStyluaConfig(cwd);
297
300
  case "ocamlformat":
298
301
  return hasOcamlformatConfig(cwd);
302
+ case "google-java-format":
303
+ return hasGoogleJavaFormatConfig(cwd);
304
+ case "cljfmt":
305
+ return hasCljfmtConfig(cwd);
306
+ case "cmake-format":
307
+ return hasCmakeFormatConfig(cwd);
299
308
  default:
300
309
  return false;
301
310
  }
@@ -737,6 +746,63 @@ export const taploFormatter: FormatterInfo = {
737
746
  },
738
747
  };
739
748
 
749
+ export const googleJavaFormatFormatter: FormatterInfo = {
750
+ name: "google-java-format",
751
+ command: ["google-java-format", "--replace", "$FILE"],
752
+ extensions: [".java"],
753
+ async detect(cwd: string) {
754
+ if ((await which("google-java-format")) === null) return false;
755
+ return hasGoogleJavaFormatConfig(cwd);
756
+ },
757
+ };
758
+
759
+ export const cljfmtFormatter: FormatterInfo = {
760
+ name: "cljfmt",
761
+ command: ["cljfmt", "fix", "$FILE"],
762
+ extensions: [".clj", ".cljc", ".cljs"],
763
+ async detect(cwd: string) {
764
+ if ((await which("cljfmt")) === null) return false;
765
+ return hasCljfmtConfig(cwd);
766
+ },
767
+ };
768
+
769
+ export const cmakeFormatFormatter: FormatterInfo = {
770
+ name: "cmake-format",
771
+ command: ["cmake-format", "-i", "$FILE"],
772
+ extensions: [".cmake"],
773
+ async detect(cwd: string) {
774
+ if ((await which("cmake-format")) === null) return false;
775
+ return hasCmakeFormatConfig(cwd);
776
+ },
777
+ };
778
+
779
+ export const psscriptanalyzerFormatFormatter: FormatterInfo = {
780
+ name: "psscriptanalyzer-format",
781
+ command: ["pwsh", "-Command", "Invoke-Formatter -ScriptDefinition (Get-Content -Raw '$FILE') | Set-Content '$FILE'"],
782
+ extensions: [".ps1", ".psm1", ".psd1"],
783
+ async resolveCommand(filePath, _cwd) {
784
+ const pwsh = (await which("pwsh")) ?? (await which("powershell"));
785
+ if (!pwsh) return null;
786
+ return [
787
+ pwsh,
788
+ "-NoProfile",
789
+ "-Command",
790
+ `$content = Get-Content -Raw '${filePath}'; $formatted = Invoke-Formatter -ScriptDefinition $content; Set-Content -Path '${filePath}' -Value $formatted`,
791
+ ];
792
+ },
793
+ async detect(_cwd: string) {
794
+ const pwsh = (await which("pwsh")) ?? (await which("powershell"));
795
+ if (!pwsh) return false;
796
+ // Check PSScriptAnalyzer module is available
797
+ const result = safeSpawn(pwsh, [
798
+ "-NoProfile",
799
+ "-Command",
800
+ "Get-Module -ListAvailable PSScriptAnalyzer | Select-Object -First 1 -ExpandProperty Name",
801
+ ], { timeout: 5_000 });
802
+ return (result.stdout ?? "").includes("PSScriptAnalyzer");
803
+ },
804
+ };
805
+
740
806
  // --- Registry ---
741
807
 
742
808
  const ALL_FORMATTERS: FormatterInfo[] = [
@@ -767,6 +833,10 @@ const ALL_FORMATTERS: FormatterInfo[] = [
767
833
  standardrbFormatter,
768
834
  gleamFormatter,
769
835
  taploFormatter,
836
+ googleJavaFormatFormatter,
837
+ cljfmtFormatter,
838
+ cmakeFormatFormatter,
839
+ psscriptanalyzerFormatFormatter,
770
840
  ];
771
841
 
772
842
  // Cache for detection results - stores array of enabled formatter names per cwd+ext