pi-lens 3.8.40 → 3.8.42

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 (72) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/README.md +73 -16
  3. package/clients/cache/rule-cache.ts +1 -1
  4. package/clients/cache-manager.ts +3 -0
  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 +228 -55
  11. package/clients/dispatch/plan.ts +13 -2
  12. package/clients/dispatch/rules/error-swallowing.ts +2 -2
  13. package/clients/dispatch/rules/quality-rules.ts +14 -4
  14. package/clients/dispatch/rules/sonar-rules.ts +36 -8
  15. package/clients/dispatch/runners/index.ts +2 -0
  16. package/clients/dispatch/runners/oxlint.ts +51 -10
  17. package/clients/dispatch/runners/semgrep.ts +269 -0
  18. package/clients/dispatch/runners/shellcheck.ts +2 -8
  19. package/clients/dispatch/runners/tree-sitter.ts +53 -21
  20. package/clients/dispatch/tool-profile.ts +1 -0
  21. package/clients/file-utils.ts +32 -0
  22. package/clients/format-service.ts +10 -0
  23. package/clients/formatters.ts +31 -8
  24. package/clients/installer/index.ts +126 -4
  25. package/clients/knip-client.ts +357 -362
  26. package/clients/lsp/aggregation.ts +91 -0
  27. package/clients/lsp/client.ts +29 -11
  28. package/clients/lsp/index.ts +121 -86
  29. package/clients/lsp/server-strategies.ts +71 -0
  30. package/clients/path-utils.ts +29 -0
  31. package/clients/pipeline.ts +7 -1
  32. package/clients/production-readiness.ts +2 -2
  33. package/clients/read-guard-logger.ts +41 -1
  34. package/clients/read-guard-tool-lines.ts +3 -3
  35. package/clients/read-guard.ts +40 -11
  36. package/clients/runtime-agent-end.ts +3 -0
  37. package/clients/runtime-config.ts +5 -0
  38. package/clients/runtime-context.ts +3 -3
  39. package/clients/runtime-session.ts +33 -10
  40. package/clients/runtime-tool-result.ts +26 -1
  41. package/clients/runtime-turn.ts +137 -102
  42. package/clients/sanitize.ts +1 -1
  43. package/clients/semgrep-config.ts +213 -0
  44. package/clients/source-filter.ts +3 -2
  45. package/clients/test-runner-client.ts +99 -6
  46. package/clients/tool-policy.ts +39 -1
  47. package/clients/tree-sitter-client.ts +26 -5
  48. package/clients/widget-state.ts +283 -0
  49. package/commands/booboo.ts +15 -3
  50. package/index.ts +262 -13
  51. package/package.json +3 -2
  52. package/rules/tree-sitter-queries/go/go-command-injection.yml +2 -2
  53. package/rules/tree-sitter-queries/go/go-shared-map-write-goroutine.yml +2 -2
  54. package/rules/tree-sitter-queries/go/go-sql-injection.yml +2 -2
  55. package/rules/tree-sitter-queries/go/go-weak-hash.yml +2 -2
  56. package/rules/tree-sitter-queries/python/python-command-injection.yml +2 -2
  57. package/rules/tree-sitter-queries/python/python-hallucinated-import.yml +1 -1
  58. package/rules/tree-sitter-queries/python/python-insecure-deserialization.yml +2 -2
  59. package/rules/tree-sitter-queries/python/python-sql-injection.yml +2 -2
  60. package/rules/tree-sitter-queries/python/python-weak-hash.yml +2 -2
  61. package/rules/tree-sitter-queries/ruby/ruby-weak-hash.yml +2 -2
  62. package/rules/tree-sitter-queries/rust/rust-lock-held-across-await.yml +2 -2
  63. package/rules/tree-sitter-queries/typescript/ts-command-injection.yml +2 -2
  64. package/rules/tree-sitter-queries/typescript/ts-dynamic-require.yml +55 -0
  65. package/rules/tree-sitter-queries/typescript/ts-hallucinated-react-import.yml +1 -1
  66. package/rules/tree-sitter-queries/typescript/ts-nosql-injection.yml +54 -0
  67. package/rules/tree-sitter-queries/typescript/ts-open-redirect.yml +111 -0
  68. package/rules/tree-sitter-queries/typescript/ts-react-antipatterns.yml +6 -3
  69. package/rules/tree-sitter-queries/typescript/ts-ssrf.yml +2 -2
  70. package/rules/tree-sitter-queries/typescript/ts-weak-hash.yml +2 -2
  71. package/rules/tree-sitter-queries/typescript/ts-xss-dom-sink.yml +100 -0
  72. package/rules/tree-sitter-queries/typescript/unsafe-regex.yml +3 -2
package/CHANGELOG.md CHANGED
@@ -4,6 +4,119 @@ All notable changes to pi-lens will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [3.8.42] - 2026-05-08
8
+
9
+ ### Added
10
+
11
+ - **Fact-rules wired into all language dispatch plans** — the `fact-rules` runner was registered but never listed in any `RunnerGroup`; 20 TypeScript FactRule instances (`corsWildcardRule`, `jwtWithoutVerifyRule`, `dynamicRegexpRule`, `errorObscuringRule`, `highComplexityRule`, etc.) were never executing. Added `mode:all fact-rules` group to jsts, python, go, rust, ruby, cmake, and shell write plans.
12
+ - **3 fact-rules promoted to blocking (inline at write time):** `cors-wildcard` (CORS `*` origin — no ast-grep/tree-sitter equivalent), `error-swallowing` (empty catch — smarter than the disabled tree-sitter `empty-catch`, skips fs-boundary and documented fallbacks), `no-commented-credentials` (credentials in commented code — complementary to ast-grep which covers live code). `high-entropy-string` was already blocking.
13
+ - **Fact-rule false-positive reductions:** `no-boolean-params` now exempts names with `*Only`/`*Enabled`/`*Disabled` suffixes, `allow*`/`skip*`/`needs*`/`auto*` prefixes, and `_`-prefixed params. `duplicate-string-literal` SKIP_STRINGS expanded with DSL discriminators (`types`, `fallback`, `direct`, `all`, `mode`, `source`) and infrastructure strings (`github`, `rubocop`, `arm64`). `high-import-coupling` threshold raised 10→15 and exempts `index.ts`/`integration.ts` registry/hub files. `no-commented-credentials` exempts scanner/fixture files.
14
+ - **Severity alignment for 3 existing TS tree-sitter blocking rules** — `ts-command-injection`, `ts-ssrf`, `unsafe-regex` had `inline_tier: blocking` but `severity: warning`, producing `semantic: "warning"` which is never shown inline. Fixed to `severity: error` → `semantic: "blocking"` → actually surfaces to the agent.
15
+ - **Fixed `inline_tier: error` typo** on `ts-hallucinated-react-import` and `python-hallucinated-import` (→ `blocking`).
16
+ - **13 new high-confidence blocking promotions across 5 languages** (all `severity: error`, `inline_tier: blocking`):
17
+ - *TypeScript:* `ts-weak-hash` (`createHash("md5"/"sha1")` — confidence: high)
18
+ - *Python:* `python-command-injection`, `python-sql-injection`, `python-insecure-deserialization`, `python-weak-hash`
19
+ - *Go:* `go-command-injection`, `go-sql-injection`, `go-shared-map-write-goroutine`, `go-weak-hash`
20
+ - *Ruby:* `ruby-weak-hash`
21
+ - *Rust:* `rust-lock-held-across-await`
22
+ - **4 new blocking tree-sitter rules (SonarCloud BLOCKER equivalents)**:
23
+ - `ts-xss-dom-sink` (S5696) — flags dynamic values assigned to `innerHTML`/`outerHTML` or passed to `document.write()` / `document.writeln()`
24
+ - `ts-dynamic-require` (S5335) — flags `require()` called with a non-string-literal argument (arbitrary module loading)
25
+ - `ts-open-redirect` (S6105) — flags `res.redirect(variable)` / `response.redirect` / `ctx.redirect` with dynamic URL, and `window.location.href = variable`
26
+ - `ts-nosql-injection` (S5147) — flags any MongoDB `$where` key (JS-execution sink, dangerous regardless of value)
27
+ - **2 existing security rules promoted to `inline_tier: blocking`** — `ts-command-injection` (maps to SonarCloud S2076) and `ts-ssrf` (maps to S5146) were previously `warning`; now block the agent turn on detection.
28
+
29
+ ### Fixed
30
+
31
+ - **`fact-rules` `RuleCache` blind to built-in rule changes** — the cache hash only covered project-local rule files; for any project with no local `rules/` directory the hash was a constant, so new pi-lens built-in rules were silently ignored after the first run. Fixed by including both project-local files and `resolvePackagePath()`-resolved built-in files in the hash, with a `Set` to deduplicate when pi-lens analyzes itself.
32
+
33
+ ### Changed
34
+
35
+ - **`max-switch-cases` threshold raised 30→40** — `applyPostFilter` dispatch table now has 31 cases and is expected to grow; the old threshold triggered a false positive on pi-lens itself.
36
+ - **Package scope migration** — all `@mariozechner/*` import references updated to `@earendil-works/*` following the repo move to `earendil-works/pi-mono`. `@earendil-works/pi-tui` dependency bumped to `^0.74.0`.
37
+ - **Startup: `lsp-config` phase is now fully fire-and-forget** — `loadLSPConfig` and `igniteWarmFiles` no longer block the interactive path, removing ~1s from session start on Windows (previously dominated by sequential ENOENT `readFile` calls walking the directory tree to find a config file).
38
+ - **Startup: persistent tool probe cache** — `ensureTool` now checks `~/.pi-lens/probe-cache.json` before falling back to the full `verifyToolBinary` process spawn. Cache entries are validated with `fs.access` + mtime check and expire after 24 h; stale or missing entries fall through to the full probe and update the cache on success.
39
+
40
+ ### Added
41
+
42
+ - **Startup observability** — `checkProbeCache` now logs the reason for each cache miss (`ttl expired`, `gone`, `mtime changed`); the lsp-config fire-and-forget callback logs how many warm files were configured once the config resolves asynchronously.
43
+
44
+
45
+
46
+ ### Added
47
+
48
+ - **Test runner: import-based fallback discovery** — when basename pattern lookup finds no test file for a modified source file (e.g. `cline.test.ts` for `cline-auth.ts`), the runner now scans `tests/`, `__tests__/`, and the source file's own directory for any `*.test.*` file whose content references the source basename in an import path. Fixes the silent `no test file found` for files whose test is named after a module rather than the source file.
49
+ - **Test runner: prefer local `node_modules/.bin` binary over `npx`** — `vitest` and `jest` now resolve the project-local binary (`node_modules/.bin/vitest.cmd` on Windows, `node_modules/.bin/vitest` on Unix) before falling back to `npx`, saving ~150ms of startup overhead per test run.
50
+ - **Turn-end test runner logging** — `turn_end` now logs the outcome of every test run: `turn_end: test vitest util.test.ts → PASS 8p/0f (412ms)` or `FAIL 2p/8f (930ms)`. Stale results (turn advanced while tests ran) are logged with a `[stale]` prefix instead of being silently discarded. All-pass turns are no longer silent.
51
+ - **Per-file test target logging** — `turn_end` now logs which test file was resolved for each modified source file, or `no test file found` when none matched. Previously silent; impossible to distinguish "runner disabled" from "no test found".
52
+ - **Session-scoped turn-end dedup** — `turn-end-findings-last` now stores the current session ID alongside the content signature. Identical findings from a previous session are no longer suppressed — each new session sees its blockers fresh. Same-session dedup continues to work as before.
53
+ - **Cross-session turn state eviction** — turn state (modified file ranges) now carries the session ID set at first edit. If `turn_end` reads a turn state written by a different session, it evicts it immediately and logs `turn_end: evicting stale turn state (session X ≠ current Y)`, preventing stale cross-session file lists from triggering jscpd, madge, or test runs.
54
+
55
+ ### Changed
56
+
57
+ - **Context injections framed as automated checks** — all three `consume*` injections (`turn-end findings`, `test findings`, `session guidance`) now prefix their content with `[pi-lens automated check — not a user request]` so the agent cannot mistake a hook-injected message for a direct user command. Advisory sections additionally carry `ℹ️ Advisory — no action required this turn:` before their content; blockers (🔴) continue to require action.
58
+
59
+ - **`/lens-widget-toggle` command** — toggles the pi-lens diagnostics widget below the editor on/off for the current session, so users can reclaim footer/editor space without disabling pi-lens analysis.
60
+
61
+ ### Changed
62
+
63
+ - **Removed per-turn jscpd scans** — jscpd remains in the session-start project scan, but no longer runs unconditionally at `turn_end`; inline structural-similarity checks cover the high-value duplicate-code signal during active edits without the repeated multi-second clone scan.
64
+ - **Cascade avoids low-value work** — unsupported graph kinds now skip review-graph construction and go straight to passive LSP fallback diagnostics, and neighbor files that recently returned clean can skip repeated active LSP touches for a few turns unless the passive snapshot already contains fresh errors.
65
+ - **Knip now surfaces unused-export regressions** — newly unused exports in modified files are shown as advisory end-of-turn findings when they were absent from the previous Knip cache.
66
+
67
+ ### Fixed
68
+
69
+ - **Knip latency log now includes result metadata** — the `turn_end` Knip phase previously logged only duration with empty `metadata: {}`, making it impossible to distinguish a clean run from a silent failure. It now logs `success`, `totalIssues`, `newIssues`, `blockerIssues`, and `skipped` when the startup scan is still in flight.
70
+
71
+ - **LSP timeout log now includes `serverIds`** — `lsp_client_wait_timeout` previously only recorded `maxWaitMs`, making it impossible to identify which server consistently failed to respond within the budget. The event now includes the array of server IDs that were being waited on.
72
+
73
+ - **Vendor/third-party files excluded from cascade neighbor analysis** — `isExternalOrVendorFile()` previously only checked `node_modules`; it now checks every path segment against `vendor`, `vendors`, `third_party`, and `third-party` as well. Cascade neighbor discovery and fallback neighbor injection both skip files inside these directories, preventing vendored dependency diagnostics from surfacing in cascade output.
74
+
75
+ - **`lens-booboo` hangs on repos with large vendored trees (fixes #57)** — `collectSourceFiles` and the `sg scan` runner in `lens-booboo` now exclude `vendor/`, `third_party/`, `third-party/`, and `vendors/` by default (added to `EXCLUDED_DIRS`). Additionally, `readGitignoreDirs()` reads the root `.gitignore` and extracts simple directory-name entries (bare names and `name/` patterns — no wildcards, negations, or internal slashes), merging them into the exclusion list for `collectSourceFiles` and the `sg scan` glob arguments. This covers project-specific large dirs (e.g. `my-upstream/`) without requiring full gitignore-spec compliance.
76
+
77
+ ## [3.8.41] - 2026-05-05
78
+
79
+ ### Fixed
80
+
81
+ - **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`.
82
+ - **`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).
83
+ - **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.
84
+
85
+ ### Added
86
+
87
+ - **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.
88
+ - **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.
89
+ - **`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).
90
+
91
+ - **`/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.
92
+ - **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`.
93
+ - **`/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.
94
+ - **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.
95
+ - **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.
96
+
97
+ ### Changed
98
+
99
+ - **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`.
100
+
101
+ ### Fixed
102
+
103
+ - **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.
104
+ - **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.
105
+
106
+ - **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.
107
+ - **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.
108
+ - **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.
109
+ - **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`).
110
+ - **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.
111
+ - **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.
112
+ - **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).
113
+ - **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.
114
+ - **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.
115
+ - **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).
116
+ - **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.
117
+ - **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.
118
+ - **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.
119
+
7
120
  ## [3.8.40] - 2026-05-04
8
121
 
9
122
  ### Added
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:
@@ -124,25 +124,45 @@ Supported: TypeScript, TSX, JavaScript, JSX, Python, Go, Rust, Ruby.
124
124
 
125
125
  ### Fact Rules Pipeline
126
126
 
127
- Covers JavaScript/TypeScript, Python, Go, Rust, Ruby, Shell, and CMake. Dispatch includes a fact-rule engine that extracts function-level metrics (cyclomatic complexity, nesting depth, outgoing calls) and evaluates quality rules inline:
128
-
129
- - **high-complexity** flags functions exceeding configurable CC thresholds
130
- - **unsafe-boundary** — detects dangerous boundary crossings (unvalidated user input → trusted context)
131
- - **high-fan-out** — flags excessive outgoing call count (default threshold: 20)
132
- - **comment-facts** — classifies comment quality (TODO density, doc coverage)
133
- - **try-catch-facts** — flags empty/obscuring catch blocks
134
- - **import-facts** — detects circular/star/unused imports
135
- - **file-role** classifies files as source/test/config/vendor and adjusts severity
127
+ 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`.
128
+
129
+ **Blocking (surface inline at write time):**
130
+ - **cors-wildcard** — `Access-Control-Allow-Origin: *` in server-side code
131
+ - **error-swallowing** — empty catch block (skips documented local fallbacks and fs-boundary catches)
132
+ - **no-commented-credentials** — password/token/secret in commented-out code
133
+ - **high-entropy-string** — string literals with suspiciously high Shannon entropy (possible hardcoded secret)
134
+
135
+ **Advisory (accessible via `/lens-booboo`):**
136
+ - **high-complexity** / **no-complex-conditionals** — cyclomatic complexity and deeply nested conditions
137
+ - **high-fan-out** — function calls too many distinct functions (coordination smell)
138
+ - **unsafe-boundary** — dangerous `any` casts at API boundaries
139
+ - **async-noise** / **async-unnecessary-wrapper** — async functions with no await; wrappers that add no value
140
+ - **pass-through-wrappers** — trivial wrapper functions
141
+ - **dynamic-regexp** — `new RegExp(variable)` (potential ReDoS; complements tree-sitter `unsafe-regex`)
142
+ - **jwt-without-verify** — `jwt.sign()` without `jwt.verify()` in the same file
143
+ - **missing-error-propagation** — catch blocks that log but don't rethrow
144
+ - **error-obscuring** — catch blocks that wrap errors in a different type
145
+ - **duplicate-string-literal** / **no-boolean-params** / **high-import-coupling** — code-quality signals
136
146
 
137
147
  ### Tree-sitter Rules
138
148
 
139
- Structural rules are organized by language in `rules/tree-sitter-queries/`:
149
+ 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.
150
+
151
+ **TypeScript (23 rules):**
152
+ 🔴 `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`
153
+ ⚠️ `console-statement`, `deep-promise-chain`, `mixed-async-styles`, `ts-insecure-random`, `ts-detached-async-call`, `ts-react-antipatterns`, `ts-weak-hash`, `variable-shadowing`
154
+
155
+ **Python:** 🔴 `python-command-injection`, `python-sql-injection`, `python-insecure-deserialization`, `python-weak-hash`, `python-hallucinated-import` + 20 advisory rules
156
+
157
+ **Go:** 🔴 `go-command-injection`, `go-sql-injection`, `go-shared-map-write-goroutine`, `go-weak-hash` + 13 advisory rules
140
158
 
141
- - **TypeScript** (18 rules): console-statement, debugger, deep-nesting, eval, sql-injection, ssrf, weak-hash, unsafe-regex, variable-shadowing, and more
142
- - **Python** (26 rules): debug statements, hardcoded secrets, mutable class attrs, unsafe regex, empty except, and more
143
- - **Go** (17 rules): defer-in-loop, hardcoded secrets, unchecked errors, and more
144
- - **Rust** (6 rules): unsafe blocks, unwrap outside tests, and more
145
- - **Ruby** (15 rules): empty rescue, rescue Exception, debugger, hardcoded secrets, and more
159
+ **Rust:** 🔴 `rust-lock-held-across-await` + 3 advisory rules (`rust-unsafe-block`, `rust-expect`, `rust-clone-in-loop`)
160
+
161
+ **Ruby:** 🔴 `ruby-weak-hash` + 14 advisory rules
162
+
163
+ **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.
164
+
165
+ **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.
146
166
 
147
167
  ### Ast-Grep Rules
148
168
 
@@ -153,6 +173,36 @@ Structural rules are organized by language in `rules/tree-sitter-queries/`:
153
173
  - **Style/smells** — nested-ternary, long-parameter-list, large-class, prefer-optional-chain, redundant-state, require-await
154
174
  - **Agent stubs** — no-unimplemented-stub, no-raise-not-implemented, no-ellipsis-body
155
175
 
176
+ ### Semgrep CLI Integration (Experimental)
177
+
178
+ 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.
179
+
180
+ Activation is intentionally gated:
181
+
182
+ - pi-lens **does not auto-install Semgrep**.
183
+ - A local `.semgrep.yml`, `.semgrep.yaml`, `semgrep.yml`, or `semgrep.yaml` enables the runner when the `semgrep` CLI is available.
184
+ - 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>`.
185
+ - Local `.semgrep.yml` scans do not require a Semgrep token. Semgrep AppSec/Pro/managed configurations may require `semgrep login` or `SEMGREP_APP_TOKEN`.
186
+ - pi-lens passes `--metrics=off` for dispatch scans.
187
+
188
+ Commands:
189
+
190
+ - `/lens-semgrep status` — show CLI availability, discovered local config, persisted pi-lens config, and effective dispatch state
191
+ - `/lens-semgrep init` — create a starter `.semgrep.yml` with a blocking `eval(...)` rule and enable Semgrep dispatch
192
+ - `/lens-semgrep enable [--config <auto|p/pack|path>]` — persist Semgrep dispatch activation in `.pi-lens/semgrep.json`
193
+ - `/lens-semgrep disable` — persistently disable Semgrep dispatch for this project
194
+ - `/lens-semgrep clear` — remove `.pi-lens/semgrep.json` and return to local-config auto-discovery
195
+
196
+ Local rules can opt into pi-lens blocking semantics with metadata:
197
+
198
+ ```yaml
199
+ metadata:
200
+ pi-lens:
201
+ semantic: blocking
202
+ defect_class: injection
203
+ confidence: high
204
+ ```
205
+
156
206
  ## Dependencies
157
207
 
158
208
  Auto-install behavior depends on gate type:
@@ -199,6 +249,7 @@ Auto-install behavior depends on gate type:
199
249
  | `vscode-html-languageserver-bin` | HTML LSP | Yes | Language-default |
200
250
  | `svelte-language-server` | Svelte LSP | Yes | Flow-gated |
201
251
  | `@vue/language-server` | Vue LSP | Yes | Flow-gated |
252
+ | `semgrep` | Experimental security dispatch | Manual | Local config / explicit opt-in |
202
253
  | `psscriptanalyzer` | PowerShell linting | Manual | — |
203
254
 
204
255
  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 +261,7 @@ Additional language servers (gopls, ruby-lsp, solargraph, etc.) are auto-detecte
210
261
  pi
211
262
 
212
263
  # Optional switches
264
+ pi --no-lens # Start pi-lens disabled for this session; /lens-toggle can re-enable
213
265
  pi --no-lsp # Disable unified LSP diagnostics
214
266
  pi --no-autoformat # Skip auto-formatting entirely
215
267
  pi --immediate-format # Format immediately after each edit instead of deferring to agent_end
@@ -217,6 +269,8 @@ pi --no-autofix # Skip auto-fix (Biome, Ruff, ESLint, stylelint, sqlfl
217
269
  pi --no-tests # Skip test runner
218
270
  pi --no-delta # Disable delta mode (show all diagnostics, not just new ones)
219
271
  pi --lens-guard # Block git commit/push when unresolved blockers exist (experimental)
272
+ pi --lens-semgrep # Enable Semgrep dispatch when a local/configured Semgrep config exists
273
+ pi --lens-semgrep-config p/ci # Explicit Semgrep config for dispatch (requires --lens-semgrep)
220
274
  ```
221
275
 
222
276
  ## Environment Variables
@@ -233,10 +287,13 @@ pi --lens-guard # Block git commit/push when unresolved blockers exist
233
287
 
234
288
  ## Key Commands
235
289
 
290
+ - `/lens-toggle` — toggle pi-lens on/off for the current session without restarting
291
+ - `/lens-widget-toggle` — show/hide the pi-lens diagnostics widget below the editor
236
292
  - `/lens-booboo` — full quality report for current project state
237
293
  - `/lens-health` — runtime health, latency, and diagnostic telemetry
238
294
  - `/lens-tools` — tool installation status: globally installed, auto-installed, or npx fallback
239
295
  - `/lens-tdi` — Technical Debt Index (TDI) and project health trend
296
+ - `/lens-semgrep` — manage experimental Semgrep dispatch (`status`, `init`, `enable`, `disable`, `clear`)
240
297
 
241
298
  ## Language Coverage
242
299
 
@@ -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}`);
@@ -43,6 +43,7 @@ export interface TurnState {
43
43
  turnCycles: number;
44
44
  maxCycles: number;
45
45
  lastUpdated: string;
46
+ sessionId?: string;
46
47
  }
47
48
 
48
49
  // --- Defaults ---
@@ -250,8 +251,10 @@ export class CacheManager {
250
251
  range: ModifiedRange,
251
252
  importsChanged: boolean,
252
253
  cwd: string,
254
+ sessionId?: string,
253
255
  ): TurnState {
254
256
  const state = this.readTurnState(cwd);
257
+ if (sessionId) state.sessionId = sessionId;
255
258
  const normalizedPath = this.toTurnStateKey(filePath, cwd);
256
259
 
257
260
  const existing = state.files[normalizedPath];
@@ -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
  );