pi-lens 3.8.39 → 3.8.41

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 (65) hide show
  1. package/CHANGELOG.md +84 -5
  2. package/README.md +37 -1
  3. package/clients/biome-client.ts +5 -4
  4. package/clients/cache/rule-cache.ts +1 -1
  5. package/clients/complexity-client.ts +1 -1
  6. package/clients/dependency-checker.ts +1 -1
  7. package/clients/dispatch/diagnostic-taxonomy.ts +13 -1
  8. package/clients/dispatch/dispatcher.ts +9 -0
  9. package/clients/dispatch/fact-scheduler.ts +1 -1
  10. package/clients/dispatch/integration.ts +58 -3
  11. package/clients/dispatch/runners/index.ts +2 -0
  12. package/clients/dispatch/runners/semgrep.ts +269 -0
  13. package/clients/dispatch/runners/shellcheck.ts +2 -8
  14. package/clients/dispatch/runners/tree-sitter.ts +32 -11
  15. package/clients/dispatch/tool-profile.ts +1 -0
  16. package/clients/format-service.ts +10 -0
  17. package/clients/formatters.ts +22 -8
  18. package/clients/installer/index.ts +3 -3
  19. package/clients/knip-client.ts +360 -362
  20. package/clients/lsp/aggregation.ts +91 -0
  21. package/clients/lsp/client.ts +91 -38
  22. package/clients/lsp/index.ts +88 -72
  23. package/clients/lsp/launch.ts +107 -34
  24. package/clients/lsp/server-strategies.ts +71 -0
  25. package/clients/lsp/server.ts +76 -57
  26. package/clients/path-utils.ts +17 -0
  27. package/clients/pipeline.ts +23 -5
  28. package/clients/production-readiness.ts +2 -2
  29. package/clients/read-guard-logger.ts +41 -1
  30. package/clients/read-guard-tool-lines.ts +17 -4
  31. package/clients/read-guard.ts +95 -46
  32. package/clients/runtime-agent-end.ts +3 -0
  33. package/clients/runtime-session.ts +5 -0
  34. package/clients/runtime-tool-result.ts +48 -1
  35. package/clients/runtime-turn.ts +48 -4
  36. package/clients/sanitize.ts +1 -1
  37. package/clients/semgrep-config.ts +213 -0
  38. package/clients/tool-policy.ts +1982 -1936
  39. package/clients/tree-sitter-client.ts +1 -1
  40. package/clients/widget-state.ts +283 -0
  41. package/commands/booboo.ts +34 -2
  42. package/index.ts +231 -17
  43. package/package.json +3 -2
  44. package/rules/rule-catalog.json +25 -1
  45. package/rules/tree-sitter-queries/cobol/lock-table-cobol.yml +35 -0
  46. package/rules/tree-sitter-queries/cpp/unnecessary-bit-ops.yml +58 -0
  47. package/rules/tree-sitter-queries/java/infinite-loop.yml +58 -0
  48. package/rules/tree-sitter-queries/java/infinite-recursion.yml +58 -0
  49. package/rules/tree-sitter-queries/java/mockito-initialized.yml +66 -0
  50. package/rules/tree-sitter-queries/java/name-capitalization-conflict.yml +54 -0
  51. package/rules/tree-sitter-queries/java/no-octal-values.yml +48 -0
  52. package/rules/tree-sitter-queries/java/resources-closed.yml +57 -0
  53. package/rules/tree-sitter-queries/java/short-circuit-logic.yml +57 -0
  54. package/rules/tree-sitter-queries/java/tests-include-assertions.yml +60 -0
  55. package/rules/tree-sitter-queries/java/unnecessary-bit-ops-java.yml +57 -0
  56. package/rules/tree-sitter-queries/javascript/switch-case-termination-js.yml +64 -0
  57. package/rules/tree-sitter-queries/plsql/lock-table.yml +42 -0
  58. package/rules/tree-sitter-queries/plsql/nchar-nvarchar2-bytes.yml +54 -0
  59. package/rules/tree-sitter-queries/python/no-super-torchscript.yml +52 -0
  60. package/rules/tree-sitter-queries/typescript/default-not-last.yml +54 -0
  61. package/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +51 -0
  62. package/rules/tree-sitter-queries/typescript/empty-switch-case.yml +54 -0
  63. package/rules/tree-sitter-queries/typescript/infinite-loop.yml +55 -0
  64. package/rules/tree-sitter-queries/typescript/self-assignment.yml +46 -0
  65. package/rules/tree-sitter-queries/typescript/switch-case-termination.yml +64 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,89 @@ All notable changes to pi-lens will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [3.8.41] - 2026-05-05
8
+
9
+ ### Fixed
10
+
11
+ - **tree-sitter wasm abort loop and memory leak (fixes #56)** — when the emscripten wasm runtime aborts (OOM or assertion failure on large workspaces), the module-level heap is permanently corrupted. pi-lens was re-invoking the dead runtime on every subsequent file write, printing `Aborted()` to stderr on each query and leaking memory on each retry. Added a module-level `_wasmAborted` flag: the first abort detected in the query catch loop poisons the singleton and prevents any further tree-sitter calls for the session. The runner skips cleanly with `reason: wasm_aborted_fatal` logged to `tree-sitter.log`.
12
+ - **`turn_end` phases now instrumented in latency log** — `handleTurnEnd` previously had no `logLatency` calls; all timing data was buried in plain-text `dbg()` lines in `sessionstart.log`. Added per-phase latency entries for `cascade_merge`, `jscpd`, `knip`, and `madge`, plus a `tool_result` total with `fileCount` and `blockerSections`. This gives a baseline for measuring the cost of future turn_end additions (e.g. LSP re-query).
13
+ - **Cascade ran graph build on non-code files** — markdown, YAML, JSON, and other files without a dispatchable kind were reaching `buildOrUpdateGraph`, causing cold graph builds that took up to 3–4 seconds per write with zero useful output. `computeCascadeForFile` now exits immediately with `cascade_skip / non_code_file` when `detectFileKind` returns `undefined`, consistent with the existing `shouldDispatch` gate used by the lint pipeline.
14
+
15
+ ### Added
16
+
17
+ - **Per-server LSP diagnostic strategies** — new `clients/lsp/server-strategies.ts` codifies known server behavior (TypeScript, rust-analyzer, pyright, ESLint) so timing decisions are automatic rather than one-size-fits-all. Strategies control first-push seeding, debounce window, pull retry budget, aggregate wait timeout, and whether a server benefits from a semantic second pull pass. Env var overrides (`PI_LENS_LSP_*`) take precedence. Unknown servers get a conservative default.
18
+ - **Result-aware diagnostic racing (`raceToCompletion`)** — new `clients/lsp/aggregation.ts` replaces the simple `Promise.race` + grace window pattern with a result-quality-aware aggregator. The grace window only triggers when at least one client has returned non-empty diagnostics, preventing premature resolution when the fastest client returns empty (e.g., TypeScript's syntactic pass). Document mode uses 0ms grace; full mode keeps the 400ms default.
19
+ - **`seedFirstPush` early-exit for clean files** — `raceToCompletion`'s completion predicate now also fires when a `seedFirstPush` server (TypeScript, ESLint) returns any result, even an empty one. These servers' first push is authoritative — waiting further yields nothing. Cuts clean-file diagnostic latency from ~1000ms to ~450ms in full mode and to near-zero in document mode (cascade neighbor touches).
20
+
21
+ - **`/lens-toggle` session switch** — added a single command to toggle pi-lens on/off at runtime without restarting pi. When off, write/edit analysis, read-guard, formatting, cascade, turn-end checks, and context injection are paused; running `/lens-toggle` again resumes them. `--no-lens` starts a session in the disabled state. Closes #49.
22
+ - **Experimental Semgrep CLI dispatch integration** — added a config-gated `semgrep` dispatch runner that normalizes Semgrep JSON findings into pi-lens diagnostics. The runner never auto-installs Semgrep and only runs when a local `.semgrep.yml`/`.semgrep.yaml`/`semgrep.yml`/`semgrep.yaml` is discovered or when explicitly configured with `--lens-semgrep --lens-semgrep-config <auto|p/pack|path>` / `/lens-semgrep enable --config <...>`. Dispatch scans pass `--metrics=off`; local rule scans do not require a Semgrep token, while Semgrep AppSec/Pro/managed configs may require `semgrep login` or `SEMGREP_APP_TOKEN`.
23
+ - **`/lens-semgrep` command** — new project command for managing Semgrep dispatch: `status` shows CLI/config/effective state, `init` writes a starter `.semgrep.yml` and enables dispatch, `enable [--config <auto|p/pack|path>]` persists activation in `.pi-lens/semgrep.json`, `disable` persists opt-out, and `clear` removes the pi-lens Semgrep config to return to local-config auto-discovery.
24
+ - **Semgrep severity policy metadata** — Semgrep rules can opt into pi-lens blocking semantics with metadata such as `metadata.pi-lens.semantic: blocking` and `metadata.pi-lens.defect_class: injection`. Otherwise, pi-lens promotes only high-signal Semgrep `ERROR` findings in security defect classes (`injection`, `secrets`, `safety`) to blockers and leaves other findings as warnings.
25
+ - **Experimental terminal dashboard** — `--lens-dashboard` / `PI_LENS_DASHBOARD=1` streams redacted session telemetry to a per-session JSONL file (`~/.pi-lens/dashboard-events/{sessionId}.jsonl`) and opens a live terminal dashboard. The dashboard shows the working folder, detected languages, formatter/linter activity, LSP servers spawned, diagnostics grouped by file with OSC-8 clickable links, and a session-start summary of languages, tools, configs, and autoinstalls. Each session gets its own event file; old files are pruned after 7 days (configurable via `PI_LENS_DASHBOARD_RETENTION_DAYS`). Use `PI_LENS_DASHBOARD_LOG_ONLY=1` to emit JSONL without opening a terminal. The viewer auto-scrolls to the latest content on each render.
26
+
27
+ ### Changed
28
+
29
+ - **LSP diagnostic pipeline latency optimization** — six targeted refactors reduce per-file diagnostic wait times by 50–900ms depending on the language server: first-push seeding skips the debounce timer for TypeScript and ESLint (~150–200ms saved); adaptive debounce computes remaining wait from `pushDiagnosticTimestamps` (50–140ms saved); per-server aggregate wait times (1000ms for TypeScript, 3000ms for rust-analyzer, 1500ms default); semantic settle pass gated to rust-analyzer only; pull retry budget zeroed for TypeScript/ESLint. Global constants `DIAGNOSTICS_DEBOUNCE_MS`, `PULL_DIAGNOSTICS_RETRY_BUDGET_MS`, and `DIAGNOSTICS_AGGREGATE_WAIT_MS` replaced by per-server strategy values from the new `server-strategies.ts`.
30
+
31
+ ### Fixed
32
+
33
+ - **Cascade neighbor touch cache ignores `writeSeq` on hit** — the A5 neighbor touch cache checked only `turnSeq` on cache hits, so a neighbor diagnosed at writeSeq=1 was served stale results when a second file write (writeSeq=2) cascaded to the same neighbor in the same turn. Fixed by requiring both `turnSeq` and `writeSeq` to match before using the cached entry.
34
+ - **Cascade fallback neighbors include other primary files** — `appendFallbackNeighbors` (the degraded-LSP path) excluded only the current primary file from the passive diagnostic snapshot sweep, but not other files edited as primary this turn. Those files could appear as cascade neighbors even though their own pipeline run is the authoritative diagnostic source. Fixed by adding a `primaryFilesThisTurn` check consistent with the B10 filter in the main neighbor path.
35
+
36
+ - **Semgrep dispatch plan regression** — kept the experimental Semgrep runner out of static `TOOL_PLANS` exposure and appends it only at runtime when Semgrep is actually configured. Fixes CI regressions in plan-shape tests while preserving config-gated Semgrep dispatch.
37
+ - **Widget theme method binding crash** — `renderWidget` now calls `theme.fg(...)` directly instead of destructuring `fg`, preserving the `this` binding required by pi's `Theme` class. Fixes the `Cannot read properties of undefined (reading 'fgColors')` widget render crash. Closes #53.
38
+ - **Read-guard follow-up edits after own writes** — tuned `file_modified` handling so a file changed by the agent's own prior allowed edit, immediate format, autofix, or deferred `agent_end` formatting does not force a redundant re-read when the next edit is still within already-read ranges. The guard still blocks zero-read and out-of-range edits, and external/stale changes outside the own-edit grace window remain protected. `PI_LENS_READ_GUARD_OWN_EDIT_GRACE_MS` controls the default 120s grace window.
39
+ - **Read-guard log noise and growth** — `~/.pi-lens/read-guard.log` now defaults to block/warn/anomaly events instead of logging every read and allowed edit. Verbose logging is available with `PI_LENS_READ_GUARD_VERBOSE=1` or `PI_LENS_READ_GUARD_LOG=verbose`; allowed-edit logging can be restored with `PI_LENS_READ_GUARD_LOG_ALLOWS=1`. The log now rotates at 1MB by default (`PI_LENS_READ_GUARD_MAX_BYTES`).
40
+ - **Pipelines skipped for external and vendor files** — agents reading dependency source (global npm packages, project-local `node_modules`) previously triggered LSP server spawns, tree-sitter read-range expansion, read-guard recording, and complexity baseline capture on those files — all noise with no diagnostic value. Added `isExternalOrVendorFile()` (built on the existing `isUnderDir` helper for correct Windows case handling) and gated all five pipeline paths: LSP auto-touch, tree-sitter expansion, read-guard recording, complexity baseline, and the full dispatch pipeline on write/edit.
41
+ - **Security: absolute paths for `cmd.exe` and `osascript` spawn calls** — dashboard terminal launch now resolves both executables via `process.env.SystemRoot` / absolute macOS path instead of relying on `PATH`, eliminating the SonarCloud S4036 PATH-injection finding.
42
+ - **Security: installed binary permissions tightened** — `chmod` calls on downloaded tool binaries changed from `0o755` to `0o750`, removing world-execute permission (SonarCloud S2612). GitHub Actions `contents: write` permission moved from workflow level to the `release` job only (S8233).
43
+ - **Agent messages: full-file-read options removed** — read-guard block messages no longer offer "read the full file" as an alternative. The out-of-range block now presents only the pre-computed targeted `offset`/`limit`; the zero-read block gives a single imperative directive. "Re-read the file" fallback text in ambiguous-edit messages replaced with "Re-read the relevant section" throughout.
44
+ - **Agent messages: indentation-mismatch RETRYABLE made explicitly directive** — the block now opens with "Retry the same edit call immediately with the corrected oldText shown below — copy it exactly as-is" and labels each corrected entry with "do not shorten, do not change newText", preventing agents from improvising instead of copying the corrected text verbatim.
45
+ - **SonarCloud reliability fixes** — five `.sort()` calls on string arrays given explicit `localeCompare` comparators (S2871); three identical-branch conditionals collapsed (S3923 in `knip-client.ts`, `shellcheck.ts`, `production-readiness.ts`); emoji character class converted to alternation to handle multi-codepoint variation-selector emojis (S5868); regex alternation precedence made explicit with non-capturing groups (S5850); `| 0` in hash function annotated as intentional 32-bit truncation (S7767).
46
+ - **CI: build step added before tests** — Vitest's native ESM resolver requires compiled `.js` output when `vi.resetModules()` is used; without a prior `tsc` build, imports of newly-added exports resolved as `undefined` in CI.
47
+ - **Widget: diagnostic rows exceeded terminal width** — the custom `truncate()` helper stripped ANSI sequences to measure length but sliced the raw string, losing OSC-8 hyperlinks and SGR sequences from the count. Replaced with pi-tui's `truncateToWidth()` / `visibleWidth()` which correctly account for all escape sequences. All widget lines (header, file rows, separators, diagnostic detail, LSP status) are now clamped. Closes #54.
48
+ - **Widget: file list capped at 5 entries, basename deduplication** — reduced max file rows from 6 to 5 to keep the widget compact. Added basename deduplication (last write wins) so that different files with the same name (e.g. `pi-lens/index.ts` and `pi-webaio/index.ts`) show as a single merged entry instead of flooding the widget with near-identical labels.
49
+
50
+ ## [3.8.40] - 2026-05-04
51
+
52
+ ### Added
53
+
54
+ - **60+ SonarCloud BLOCKER tree-sitter rules** — comprehensive BLOCKER severity rules across 13 languages:
55
+ - **Java (11 rules)**: no-exit-methods, no-threads-in-constructors, switch-fall-through, no-wait-notify-on-thread, no-double-checked-locking, no-future-keywords, no-field-shadowing, junit-call-super, no-octal-values, short-circuit-logic, infinite-loop, infinite-recursion, name-capitalization-conflict, mockito-initialized, resources-closed, unnecessary-bit-ops-java
56
+ - **TypeScript (5 rules)**: infinite-loop, self-assignment, duplicate-function-arg, empty-switch-case, default-not-last, switch-case-termination
57
+ - **JavaScript (1 rule)**: switch-case-termination-js (replaces switch-fall-through-js)
58
+ - **PL/SQL (7 rules)**: forallsave-exceptions, not-null-initialization, end-loop-semicolon, raise-application-error-codes, no-synchronize, lock-table, nchar-nvarchar2-bytes, delete-update-where, fetch-bulk-collect-limit
59
+ - **Python (8 rules)**: send-file-mimetype, no-super-torchscript, return-in-init, yield-return-outside-function, notimplemented-boolean-context, exit-signature-check, return-in-generator, iter-return-iterator, in-operator-unsupported
60
+ - **C++ (5 rules)**: unnecessary-bit-ops, noexcept-functions, no-auto-ptr, no-memset-sensitive-data, no-scoped-lock-without-args, no-confused-move-forward
61
+ - **PHP (2 rules)**: this-in-static-context, no-exit-die
62
+ - **C (3 rules)**: case-range-multiple-values, goto-label-order, goto-into-block
63
+ - **C# (5 rules)**: is-with-this, no-operator-eq-reference, no-dangerous-get-handle, no-thread-resume-suspend, async-await-identifiers
64
+ - **Kotlin (1 rule)**: prepared-statement-indices
65
+ - **ABAP (1 rule)**: delete-where
66
+ - **COBOL (2 rules)**: alter-statement, lock-table-cobol
67
+ - **CSS (1 rule)**: calc-spacing
68
+ - **rule-catalog.json** updated with all 60+ new rule registrations
69
+
70
+ ### Fixed
71
+
72
+ - **Read-guard: false `file_modified` blocks after own edits** — `ReadGuard` was blocking the second edit to a file because the model's first write changed the file's mtime, making `FileTime.hasChanged()` return `true` on the next `checkEdit`. Added `recordWritten(filePath)` to `ReadGuard` and wired it into the `tool_result` handler (post-write, file already on disk), so the FileTime stamp stays in sync with the model's own writes. Eliminates the spurious `file_modified` blocks that appeared on every multi-edit file in a session.
73
+
74
+ - **LSP: parallel-turn root-resolution timeouts** — `NearestRoot` performed a fresh `fs.stat` directory walk on every call with no caching. When Claude Code edited multiple files simultaneously (e.g. a 4-file turn), all pipelines raced `NearestRoot` concurrently, saturating Windows filesystem I/O and triggering the 750ms `lsp_client_wait_timeout` on all but the first. `NearestRoot` now maintains per-instance result and in-flight caches keyed by resolved directory: successful roots are cached for the session lifetime; concurrent calls for the same directory share one walk promise. Only successful roots are cached so a `package.json` created mid-session is still detected on the next call.
75
+
76
+ - **Memory: `lastAnalyzedStateByFile` cleared each turn** — module-level Map in `runtime-tool-result.ts` accumulated dead entries across turns (entries from previous turns can never match the new `turnIndex`). Now cleared at `turn_start` alongside `runtime.beginTurn()`, keeping the map bounded to files touched in the current turn only. (refs #50)
77
+ - **Memory: `recentTouches` stale entry eviction** — `LSPService.recentTouches` grew unboundedly across a session with one entry per unique file path. Entries older than `TOUCH_DEBOUNCE_MS` are already ignored by `shouldSkipTouch`; a threshold-based sweep (triggered when size > 200) now removes them. (refs #50)
78
+ - **Memory: orphaned LSP child processes on Windows** — `clientShutdown` only called `process.kill()` which on Windows terminates the direct child but leaves grandchildren (e.g. `tsserver.js`) as orphaned OS processes each holding 300–600MB. Both the normal shutdown and crash paths now go through a shared `killProcessTree` helper: on Windows it runs `taskkill /F /T` via absolute `SystemRoot` path and awaits completion before returning; on other platforms it sends `SIGTERM`. The SIGKILL fallback timer is also skipped on Windows since `taskkill /F` already force-terminates. (refs #50)
79
+ - **Memory: file-time session state not cleared on session reset** — `clearAllSessions()` from `file-time.ts` is now called during `handleSessionStart`, clearing stale file timestamp state that previously accumulated across session switches. (refs #50)
80
+ - **Memory: pending ast-grep warn timers not cancelled on session reset** — `resetDispatchBaselines()` left active `astGrepWarnDebounceTimers` running into a cleared session context. Now explicitly cancelled and cleared on reset. (refs #50)
81
+ - **Security: `taskkill` spawned via absolute path** — both the normal shutdown and crash paths now resolve `taskkill.exe` through `process.env.SystemRoot` instead of relying on PATH, eliminating the SonarCloud PATH-injection hotspot.
82
+ - **LSP: shutdown cannot hang indefinitely** — `client.shutdown()` now bounds the graceful `shutdown` request and proceeds to `exit`/process-tree kill if a server stops responding.
83
+ - **LSP: test cleanup stop helper hardened on Windows** — `stopLSP()` now uses the absolute `taskkill.exe` path, handles already-exited processes, and avoids orphaning grandchildren by killing the process tree before the direct child on Windows.
84
+
85
+ - **booboo project root detection** — `resolveProjectRoot` now walks up to the nearest ancestor with a root marker (`package.json`, `tsconfig.json`, `.git`, etc.), then falls back to walking down one level if exactly one immediate subdirectory has a root marker. Fixes scans running against the wrong directory in nested-project layouts (e.g. `pi-models/pi-models/`).
86
+
87
+ - **Switch-case false positives eliminated** — replaced naive `switch-fall-through` rules with `switch-case-termination` rules that properly recognize `return`, `throw`, and `continue` as valid case terminators. Reduced false positive hits from 174 to 0.
88
+ - **Self-assignment false positives fixed** — changed from `post_filter: same_identifier` to inline `#eq?` predicate so `wave = nextWave` is no longer flagged as self-assignment
89
+
7
90
  ## [3.8.39] - 2026-05-02
8
91
 
9
92
  ### Fixed
@@ -12,6 +95,7 @@ All notable changes to pi-lens will be documented in this file.
12
95
  - **jscpd no longer runs on YAML/JSON/Markdown files** — `getFilesForJscpd` now filters to source code extensions only, preventing multi-second delays at `turn_end` when editing rule YAMLs or config files.
13
96
  - **ReDoS S5852 final (gleam/zig parsers)** — rewrote `gleamRe` and `zigRe` as line-by-line parsers, eliminating the multiline flag that SonarCloud continued to flag despite `[ \t]*` substitution.
14
97
  - **SonarCloud MAJOR code smells (batch 1 & 2)** — `readonly` members, `void` operator removals, nested ternaries, nested template literals, optional chains, duplicate branches, and redundant type alias across 15+ files.
98
+ - **Type-narrow `severityMap` for `Diagnostic.severity` union** — properly satisfies the union type for diagnostic severity mapping.
15
99
  - **9 tree-sitter query bugs in new rule files** — predicate outside outermost parens (`cpp/no-auto-ptr`); false-positive `post_filter` gate added (`cpp/no-confused-move-forward`); leaf-node child match removed (`php/this-in-static-context`); invalid node name `class_hereditary` replaced (`java/no-field-shadowing`); field order corrected (`java/no-wait-notify-on-thread`); duplicate `modifiers` blocks merged (`java/spring-session-attributes-setcomplete`); invalid anonymous-node field label removed (`csharp/is-with-this`); inline alternation replaced with two patterns (`python/in-operator-unsupported`); adjacent sibling requirement removed, delegated to `post_filter` (`python/return-in-generator`).
16
100
 
17
101
  ## [3.8.38] - 2026-05-02
@@ -1133,7 +1217,6 @@ All runtime-applicable TypeScript ast-grep rules now have JavaScript equivalents
1133
1217
  - **Rust performance core (`pi-lens-core`)** — Optional Rust binary for CPU-intensive operations.
1134
1218
  All features fall back to TypeScript automatically if the binary is not available (it is **not**
1135
1219
  built automatically on `npm install` — run `npm run rust:build` once if you have Rust installed).
1136
-
1137
1220
  - **File scanning** — ripgrep’s `ignore` crate for `.gitignore`-aware project scanning
1138
1221
  - **Similarity detection** — parallel 57×72 state-matrix index, persisted to
1139
1222
  `.pi-lens/rust-index.json` between invocations (fixes in-memory cache that reset on every
@@ -1187,7 +1270,6 @@ All runtime-applicable TypeScript ast-grep rules now have JavaScript equivalents
1187
1270
  - Removed `clients/interviewer-templates.ts` (240 lines)
1188
1271
  - Removed initialization from `index.ts`
1189
1272
  - **Deleted deprecated commands** — All were superseded by `/lens-booboo`:
1190
-
1191
1273
  - `/lens-booboo-fix` command (fix-from-booboo.ts, 430 lines) — showed warning to use `/lens-booboo`
1192
1274
  - `/lens-fix-simplified` command (fix-simplified.ts, 770 lines) — never registered, unused
1193
1275
  - `/lens-rate` command (rate.ts, 340 lines) — showed warning to use `/lens-booboo`
@@ -1206,7 +1288,6 @@ All runtime-applicable TypeScript ast-grep rules now have JavaScript equivalents
1206
1288
  - Broken runner tests (7 files) — thin CLI wrappers with wrong imports
1207
1289
  - Trivial utility tests (5 files) — file extension parsing, string sanitization
1208
1290
  - **Added meaningful integration tests**:
1209
-
1210
1291
  - `tests/clients/dispatch/dispatcher-flow.test.ts` — Runner registration, execution, delta mode, conditional runners
1211
1292
  - `tests/extension-hooks.test.ts` — pi API: tool/command/flag registration, event handlers
1212
1293
  - `tests/mocks/runner-factory.ts` — Mock runners for testing without real CLI tools
@@ -1542,7 +1623,6 @@ Migrated 20 critical security rules to NAPI (fast native execution):
1542
1623
  Three new lint runners with full test coverage:
1543
1624
 
1544
1625
  - **Spellcheck runner** (`clients/dispatch/runners/spellcheck.ts`): Markdown spellchecking
1545
-
1546
1626
  - Uses `typos-cli` (Rust-based, fast, low false positives)
1547
1627
  - Checks `.md` and `.mdx` files
1548
1628
  - Priority 30, runs after code quality checks
@@ -1550,7 +1630,6 @@ Three new lint runners with full test coverage:
1550
1630
  - Install: `cargo install typos-cli`
1551
1631
 
1552
1632
  - **Oxlint runner** (`clients/dispatch/runners/oxlint.ts`): Fast JS/TS linting
1553
-
1554
1633
  - Uses `oxlint` from Oxc project (Rust-based, ~100x faster than ESLint)
1555
1634
  - Zero-config by default
1556
1635
  - JSON output with fix suggestions
package/README.md CHANGED
@@ -16,7 +16,7 @@ On every `write` and `edit`, pi-lens runs a fast, language-aware pipeline (check
16
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
17
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
18
18
  4. **LSP file sync** — opens/updates the file in active language servers
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
19
+ 5. **Dispatch lint** — parallel runner groups: LSP diagnostics, tree-sitter structural rules, ast-grep security/correctness rules, fact rules, language-specific linters, experimental Semgrep security scans, similarity detection
20
20
  6. **Cascade diagnostics** — review-graph impact cascade showing which other files were affected and how diagnostics propagated
21
21
 
22
22
  Results are inline and actionable:
@@ -153,6 +153,36 @@ Structural rules are organized by language in `rules/tree-sitter-queries/`:
153
153
  - **Style/smells** — nested-ternary, long-parameter-list, large-class, prefer-optional-chain, redundant-state, require-await
154
154
  - **Agent stubs** — no-unimplemented-stub, no-raise-not-implemented, no-ellipsis-body
155
155
 
156
+ ### Semgrep CLI Integration (Experimental)
157
+
158
+ pi-lens can run the locally installed `semgrep` CLI as an optional dispatch runner for security-focused findings. Semgrep diagnostics are normalized into the same pi-lens `Diagnostic` model as LSP, tree-sitter, ast-grep, and linters: high-signal security findings can become blocking, while other findings remain warnings for `/lens-booboo`/history.
159
+
160
+ Activation is intentionally gated:
161
+
162
+ - pi-lens **does not auto-install Semgrep**.
163
+ - A local `.semgrep.yml`, `.semgrep.yaml`, `semgrep.yml`, or `semgrep.yaml` enables the runner when the `semgrep` CLI is available.
164
+ - Without a local config, Semgrep stays skipped unless explicitly configured with `--lens-semgrep --lens-semgrep-config <auto|p/pack|path>` or `/lens-semgrep enable --config <auto|p/pack|path>`.
165
+ - Local `.semgrep.yml` scans do not require a Semgrep token. Semgrep AppSec/Pro/managed configurations may require `semgrep login` or `SEMGREP_APP_TOKEN`.
166
+ - pi-lens passes `--metrics=off` for dispatch scans.
167
+
168
+ Commands:
169
+
170
+ - `/lens-semgrep status` — show CLI availability, discovered local config, persisted pi-lens config, and effective dispatch state
171
+ - `/lens-semgrep init` — create a starter `.semgrep.yml` with a blocking `eval(...)` rule and enable Semgrep dispatch
172
+ - `/lens-semgrep enable [--config <auto|p/pack|path>]` — persist Semgrep dispatch activation in `.pi-lens/semgrep.json`
173
+ - `/lens-semgrep disable` — persistently disable Semgrep dispatch for this project
174
+ - `/lens-semgrep clear` — remove `.pi-lens/semgrep.json` and return to local-config auto-discovery
175
+
176
+ Local rules can opt into pi-lens blocking semantics with metadata:
177
+
178
+ ```yaml
179
+ metadata:
180
+ pi-lens:
181
+ semantic: blocking
182
+ defect_class: injection
183
+ confidence: high
184
+ ```
185
+
156
186
  ## Dependencies
157
187
 
158
188
  Auto-install behavior depends on gate type:
@@ -199,6 +229,7 @@ Auto-install behavior depends on gate type:
199
229
  | `vscode-html-languageserver-bin` | HTML LSP | Yes | Language-default |
200
230
  | `svelte-language-server` | Svelte LSP | Yes | Flow-gated |
201
231
  | `@vue/language-server` | Vue LSP | Yes | Flow-gated |
232
+ | `semgrep` | Experimental security dispatch | Manual | Local config / explicit opt-in |
202
233
  | `psscriptanalyzer` | PowerShell linting | Manual | — |
203
234
 
204
235
  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.
@@ -210,6 +241,7 @@ Additional language servers (gopls, ruby-lsp, solargraph, etc.) are auto-detecte
210
241
  pi
211
242
 
212
243
  # Optional switches
244
+ pi --no-lens # Start pi-lens disabled for this session; /lens-toggle can re-enable
213
245
  pi --no-lsp # Disable unified LSP diagnostics
214
246
  pi --no-autoformat # Skip auto-formatting entirely
215
247
  pi --immediate-format # Format immediately after each edit instead of deferring to agent_end
@@ -217,6 +249,8 @@ pi --no-autofix # Skip auto-fix (Biome, Ruff, ESLint, stylelint, sqlfl
217
249
  pi --no-tests # Skip test runner
218
250
  pi --no-delta # Disable delta mode (show all diagnostics, not just new ones)
219
251
  pi --lens-guard # Block git commit/push when unresolved blockers exist (experimental)
252
+ pi --lens-semgrep # Enable Semgrep dispatch when a local/configured Semgrep config exists
253
+ pi --lens-semgrep-config p/ci # Explicit Semgrep config for dispatch (requires --lens-semgrep)
220
254
  ```
221
255
 
222
256
  ## Environment Variables
@@ -233,10 +267,12 @@ pi --lens-guard # Block git commit/push when unresolved blockers exist
233
267
 
234
268
  ## Key Commands
235
269
 
270
+ - `/lens-toggle` — toggle pi-lens on/off for the current session without restarting
236
271
  - `/lens-booboo` — full quality report for current project state
237
272
  - `/lens-health` — runtime health, latency, and diagnostic telemetry
238
273
  - `/lens-tools` — tool installation status: globally installed, auto-installed, or npx fallback
239
274
  - `/lens-tdi` — Technical Debt Index (TDI) and project health trend
275
+ - `/lens-semgrep` — manage experimental Semgrep dispatch (`status`, `init`, `enable`, `disable`, `clear`)
240
276
 
241
277
  ## Language Coverage
242
278
 
@@ -263,9 +263,10 @@ export class BiomeClient {
263
263
  const content = fs.readFileSync(absolutePath, "utf-8");
264
264
 
265
265
  try {
266
- // Single invocation: check --write applies safe formatting + lint fixes.
267
- // No pre-flight checkFile() needed content diff tells us if anything changed.
268
- const result = this.spawnBiome(["check", "--write", absolutePath]);
266
+ // lint --write applies safe lint fixes only — no formatting.
267
+ // Formatting is deferred to agent_end to avoid mid-turn file modifications
268
+ // that trigger read-guard "file modified since read" blocks.
269
+ const result = this.spawnBiome(["lint", "--write", absolutePath]);
269
270
 
270
271
  if (result.error) {
271
272
  return {
@@ -325,7 +326,7 @@ export class BiomeClient {
325
326
  try {
326
327
  const before = await fs.promises.readFile(absolutePath, "utf-8");
327
328
  const result = await this.spawnBiomeAsync([
328
- "check",
329
+ "lint",
329
330
  "--write",
330
331
  absolutePath,
331
332
  ]);
@@ -50,7 +50,7 @@ export class RuleCache {
50
50
 
51
51
  private computeRuleHash(ruleFiles: string[]): string {
52
52
  const hash = crypto.createHash("sha256");
53
- for (const file of ruleFiles.sort()) {
53
+ for (const file of ruleFiles.sort((a, b) => a.localeCompare(b))) {
54
54
  if (fs.existsSync(file)) {
55
55
  const stat = fs.statSync(file);
56
56
  hash.update(`${file}:${stat.mtimeMs}:${stat.size}`);
@@ -386,7 +386,7 @@ export class ComplexityClient {
386
386
  let count = 0;
387
387
 
388
388
  const aiPatterns = [
389
- /[🔍✅📝🔧🐛⚠️🚀💡🎯📌🏷️🔑🏗️🧪🗑️🔄♻️📋🔖📊💬🔥💎⭐🌟🎯🎨🔧🛠️]/u,
389
+ /(?:🔍|✅|📝|🔧|🐛|⚠️|🚀|💡|🎯|📌|🏷️|🔑|🏗️|🧪|🗑️|🔄|♻️|📋|🔖|📊|💬|🔥|💎|⭐|🌟|🎨|🛠️)/u,
390
390
  /\/\/\s*(Initialize|Setup|Clean up|Create|Define|Check if|Handle|Process|Validate|Return|Get|Set|Add|Remove|Update|Fetch)\b/i,
391
391
  /\/\/\s*(This function|This method|This code|Here we|Now we)\b/i,
392
392
  /\/\*\*?\s*(Overview|Summary|Description|Example|Usage)\s*\*?\//i,
@@ -408,7 +408,7 @@ export class DependencyChecker {
408
408
  let output = `[Circular Deps] ${circular.length} cycle(s) found:\n`;
409
409
 
410
410
  for (const dep of circular) {
411
- const cycleKey = dep.path.sort().join("→");
411
+ const cycleKey = dep.path.sort((a, b) => a.localeCompare(b)).join("→");
412
412
  if (seen.has(cycleKey)) continue;
413
413
  seen.add(cycleKey);
414
414
 
@@ -12,6 +12,9 @@ const SILENT_ERROR_HINTS = [
12
12
 
13
13
  const INJECTION_HINTS = [
14
14
  "sql-injection",
15
+ "command-injection",
16
+ "template-injection",
17
+ "xss",
15
18
  "eval",
16
19
  "exec",
17
20
  "inner-html",
@@ -56,7 +59,16 @@ export function classifyDefect(
56
59
  return "correctness";
57
60
  }
58
61
 
59
- if (text.includes("unsafe") || text.includes("security")) return "safety";
62
+ if (
63
+ text.includes("unsafe") ||
64
+ text.includes("security") ||
65
+ text.includes("ssrf") ||
66
+ text.includes("path-traversal") ||
67
+ text.includes("deserial") ||
68
+ text.includes("auth-bypass") ||
69
+ text.includes("crypto")
70
+ )
71
+ return "safety";
60
72
  if (text.includes("style") || text.includes("format")) return "style";
61
73
 
62
74
  return "unknown";
@@ -16,6 +16,7 @@
16
16
 
17
17
  import * as path from "node:path";
18
18
  import type { FileKind } from "../file-kinds.js";
19
+ import { recordRunner } from "../widget-state.js";
19
20
  import { detectFileKind } from "../file-kinds.js";
20
21
  import { isTestFile } from "../file-utils.js";
21
22
  import { getPrimaryDispatchGroup } from "../language-policy.js";
@@ -389,6 +390,7 @@ function buildCoverageNotice(
389
390
  "similarity",
390
391
  "spellcheck",
391
392
  "fact-rules",
393
+ "semgrep",
392
394
  ]);
393
395
  const anyLinterHasCoverage = runnerLatencies.some(
394
396
  (r) =>
@@ -597,6 +599,13 @@ async function runGroup(
597
599
  diagnosticCount: result.diagnostics.length,
598
600
  semantic: result.semantic ?? semantic,
599
601
  });
602
+ recordRunner(
603
+ ctx.filePath,
604
+ runnerId,
605
+ result.status,
606
+ result.diagnostics.length,
607
+ duration,
608
+ );
600
609
 
601
610
  diagnostics.push(...result.diagnostics);
602
611
 
@@ -69,7 +69,7 @@ export function scheduleProviders(providers: FactProvider[]): FactProvider[] {
69
69
  const cycleParticipants = providers
70
70
  .filter((p) => !result.includes(p))
71
71
  .map((p) => p.id)
72
- .sort();
72
+ .sort((a, b) => a.localeCompare(b));
73
73
  throw new Error(
74
74
  `Cycle detected among FactProviders: ${cycleParticipants.join(", ")}`,
75
75
  );
@@ -16,6 +16,7 @@ import {
16
16
  formatSlopScoreSummary,
17
17
  type SlopScoreSummary,
18
18
  } from "../session-summary.js";
19
+ import { resolveSemgrepConfig } from "../semgrep-config.js";
19
20
  import {
20
21
  clearCoverageNoticeState,
21
22
  clearLatencyReports,
@@ -290,6 +291,52 @@ export function getDispatchSlopScoreLine(): string {
290
291
  return formatSlopScoreSummary(summary);
291
292
  }
292
293
 
294
+ const SEMGREP_SUPPORTED_KINDS = new Set<FileKind>([
295
+ "csharp",
296
+ "css",
297
+ "cxx",
298
+ "dart",
299
+ "docker",
300
+ "go",
301
+ "html",
302
+ "java",
303
+ "json",
304
+ "jsts",
305
+ "kotlin",
306
+ "lua",
307
+ "php",
308
+ "python",
309
+ "ruby",
310
+ "rust",
311
+ "shell",
312
+ "swift",
313
+ "terraform",
314
+ "yaml",
315
+ ]);
316
+
317
+ function withSemgrepGroup(
318
+ kind: FileKind,
319
+ groups: RunnerGroup[],
320
+ ctx: ReturnType<typeof createDispatchContext>,
321
+ ): RunnerGroup[] {
322
+ if (!SEMGREP_SUPPORTED_KINDS.has(kind)) return groups;
323
+ const config = resolveSemgrepConfig(ctx.cwd, {
324
+ enabled: Boolean(ctx.pi.getFlag("lens-semgrep")),
325
+ config: ctx.pi.getFlag("lens-semgrep-config"),
326
+ });
327
+ if (!config.enabled) return groups;
328
+ if (groups.some((group) => group.runnerIds.includes("semgrep"))) return groups;
329
+ return [
330
+ ...groups,
331
+ {
332
+ mode: "all",
333
+ runnerIds: ["semgrep"],
334
+ filterKinds: [kind],
335
+ semantic: "warning",
336
+ },
337
+ ];
338
+ }
339
+
293
340
  function withPrimaryPolicyGroup(
294
341
  kind: keyof typeof TOOL_PLANS,
295
342
  groups: RunnerGroup[],
@@ -358,6 +405,8 @@ export function resetDispatchBaselines(): void {
358
405
  primaryFilesThisTurn.clear();
359
406
  cascadeDiagnosticBaselines.clear();
360
407
  cascadeSessionStats = { runs: 0, diagnosticsSurfaced: 0, coldSnapshotTouches: 0 };
408
+ for (const timer of astGrepWarnDebounceTimers.values()) clearTimeout(timer);
409
+ astGrepWarnDebounceTimers.clear();
361
410
  }
362
411
 
363
412
  let cascadeSessionStats = { runs: 0, diagnosticsSurfaced: 0, coldSnapshotTouches: 0 };
@@ -428,6 +477,11 @@ export async function computeCascadeForFile(
428
477
  return undefined;
429
478
  }
430
479
 
480
+ if (!detectFileKind(filePath)) {
481
+ logCascade({ phase: "cascade_skip", filePath, reason: "non_code_file" });
482
+ return undefined;
483
+ }
484
+
431
485
  const normalizedFile = resolveRunnerPath(cwd, filePath);
432
486
  const normalizedFileKey = normalizeMapKey(normalizedFile);
433
487
 
@@ -584,7 +638,7 @@ export async function computeCascadeForFile(
584
638
  // write sequence. A new write (higher writeSeq) invalidates the cache entry.
585
639
  const cached =
586
640
  writeSeq != null ? neighborTouchCache.get(cacheKey) : undefined;
587
- if (cached?.turnSeq === turnSeq) {
641
+ if (cached?.turnSeq === turnSeq && cached?.writeSeq === writeSeq) {
588
642
  producedLspData = true;
589
643
  const durationMs = Date.now() - neighborStart;
590
644
  logCascade({
@@ -819,6 +873,7 @@ function appendFallbackNeighbors(
819
873
  for (const [diagPath, { diags, ts }] of allDiags) {
820
874
  const diagKey = normalizeMapKey(diagPath);
821
875
  if (diagKey === normalizedFileKey || seen.has(diagKey)) continue;
876
+ if (primaryFilesThisTurn.has(diagKey)) continue;
822
877
  if (!nodeFs.existsSync(diagPath)) continue;
823
878
  if (now - ts > CASCADE_TTL_MS) continue;
824
879
  const errors = convertLspDiagnostics(
@@ -917,7 +972,7 @@ export async function dispatchLint(
917
972
  const kind = ctx.kind;
918
973
  if (!kind) return "";
919
974
 
920
- const groups = getDispatchGroupsForKind(kind, pi);
975
+ const groups = withSemgrepGroup(kind, getDispatchGroupsForKind(kind, pi), ctx);
921
976
  if (groups.length === 0) return "";
922
977
 
923
978
  await runProviders(ctx);
@@ -960,7 +1015,7 @@ export async function dispatchLintWithResult(
960
1015
  };
961
1016
  }
962
1017
 
963
- const groups = getDispatchGroupsForKind(kind, pi);
1018
+ const groups = withSemgrepGroup(kind, getDispatchGroupsForKind(kind, pi), ctx);
964
1019
  if (groups.length === 0) {
965
1020
  return {
966
1021
  diagnostics: [],
@@ -34,6 +34,7 @@ import pythonSlopRunner from "./python-slop.js";
34
34
  import rubocopRunner from "./rubocop.js";
35
35
  import ruffRunner from "./ruff.js";
36
36
  import rustClippyRunner from "./rust-clippy.js";
37
+ import semgrepRunner from "./semgrep.js";
37
38
  import shellcheckRunner from "./shellcheck.js";
38
39
  import shfmtRunner from "./shfmt.js";
39
40
  // Import similarity runner
@@ -65,6 +66,7 @@ export function registerDefaultRunners(registry: RunnerRegistry): void {
65
66
  registry.register(pythonSlopRunner); // Python slop via CLI (priority 25)
66
67
  registry.register(typeSafetyRunner); // Type safety checks (priority 20)
67
68
  registry.register(shellcheckRunner); // Shell script linting (priority 20)
69
+ registry.register(semgrepRunner); // Semgrep security/deep static analysis (config/flag-gated, priority 50)
68
70
  // DISABLED: registerRunner(astGrepRunner); // Replaced by ast-grep-napi for dispatch
69
71
  // CLI ast-grep kept for ast_grep_search/ast_grep_replace tools only
70
72
  registry.register(similarityRunner); // Semantic reuse detection (priority 35)