pi-lens 3.8.33 → 3.8.35

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 (68) hide show
  1. package/CHANGELOG.md +55 -1
  2. package/README.md +23 -2
  3. package/clients/cache-manager.ts +2 -1
  4. package/clients/cascade-format.ts +27 -0
  5. package/clients/cascade-logger.ts +78 -0
  6. package/clients/cascade-types.ts +17 -0
  7. package/clients/diagnostic-logger.ts +6 -0
  8. package/clients/dispatch/integration.ts +504 -4
  9. package/clients/dispatch/runners/detekt.ts +2 -2
  10. package/clients/dispatch/runners/elixir-check.ts +2 -1
  11. package/clients/dispatch/runners/lsp.ts +6 -22
  12. package/clients/dispatch/runners/similarity.ts +0 -121
  13. package/clients/dispatch/types.ts +5 -1
  14. package/clients/dispatch/utils/lsp-diagnostics.ts +48 -0
  15. package/clients/file-kinds.ts +194 -70
  16. package/clients/file-utils.ts +36 -0
  17. package/clients/fix-worklog.ts +3 -2
  18. package/clients/formatters.ts +36 -12
  19. package/clients/installer/index.ts +36 -16
  20. package/clients/latency-logger.ts +6 -0
  21. package/clients/log-cleanup.ts +9 -7
  22. package/clients/lsp/client.ts +16 -7
  23. package/clients/lsp/config.ts +2 -0
  24. package/clients/lsp/index.ts +66 -11
  25. package/clients/lsp/interactive-install.ts +2 -1
  26. package/clients/lsp/launch.ts +39 -17
  27. package/clients/lsp/server.ts +74 -39
  28. package/clients/metrics-history.ts +6 -5
  29. package/clients/pipeline.ts +16 -117
  30. package/clients/project-index.ts +4 -3
  31. package/clients/read-expansion.ts +213 -0
  32. package/clients/read-guard-logger.ts +6 -0
  33. package/clients/read-guard-tool-lines.ts +277 -18
  34. package/clients/read-guard.ts +6 -6
  35. package/clients/review-graph/builder.ts +109 -17
  36. package/clients/review-graph/format.ts +6 -6
  37. package/clients/review-graph/query.ts +9 -0
  38. package/clients/review-graph/service.ts +2 -1
  39. package/clients/rules-scanner.ts +1 -2
  40. package/clients/runtime-coordinator.ts +18 -35
  41. package/clients/runtime-session.ts +64 -0
  42. package/clients/runtime-tool-result.ts +26 -11
  43. package/clients/runtime-turn.ts +70 -36
  44. package/clients/safe-spawn.ts +22 -2
  45. package/clients/sg-runner.ts +4 -4
  46. package/clients/test-runner-client.ts +1 -1
  47. package/clients/tool-policy.ts +1837 -1774
  48. package/clients/tree-sitter-client.ts +1 -0
  49. package/clients/tree-sitter-logger.ts +6 -0
  50. package/index.ts +147 -164
  51. package/package.json +7 -5
  52. package/tools/ast-grep-replace.js +7 -2
  53. package/tools/ast-grep-replace.ts +10 -5
  54. package/tools/ast-grep-search.js +7 -2
  55. package/tools/ast-grep-search.ts +10 -5
  56. package/tools/lsp-navigation.js +26 -1
  57. package/tools/lsp-navigation.ts +35 -2
  58. package/tsconfig.json +9 -5
  59. package/clients/architect-client.ts +0 -386
  60. package/clients/dispatch/runners/architect.ts +0 -129
  61. package/clients/native-rust-client.ts +0 -546
  62. package/rust/Cargo.toml +0 -34
  63. package/rust/src/cache.rs +0 -127
  64. package/rust/src/index.rs +0 -407
  65. package/rust/src/lib.rs +0 -209
  66. package/rust/src/main.rs +0 -24
  67. package/rust/src/scan.rs +0 -116
  68. package/rust/src/similarity.rs +0 -387
package/CHANGELOG.md CHANGED
@@ -2,10 +2,59 @@
2
2
 
3
3
  All notable changes to pi-lens will be documented in this file.
4
4
 
5
- ## [Unreleased]
5
+ ## [3.8.35] - 2026-05-02
6
+
7
+ ### Fixed
8
+
9
+ - **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.
10
+
11
+ ## [3.8.34] - 2026-05-01
6
12
 
7
13
  ### Added
8
14
 
15
+ - **LSP config `warmFiles` option** — added `warmFiles` to the LSP config schema. Accepts an array of relative or absolute file paths that pi-lens opens at full session startup to seed language servers that perform lazy translation-unit indexing (e.g. clangd). Without this, a short-lived `workspaceSymbol` query may return empty results for symbols in TUs clangd has not yet built an AST for, and background indexing timing is unreliable at LLVM scale. Specify entry-point files that transitively cover most of the project. The feature is general — any LSP that indexes lazily benefits.
16
+ - **TypeScript tsconfig split into build and lint configs** — `tsconfig.build.json` now drives `npm run build` (emits, excludes tests), while `tsconfig.json` drives `npm run lint` (no-emit, includes tests, `allowImportingTsExtensions`, `noUnusedLocals`, `noUnusedParameters`). CI lint step consolidated to `npm run lint`. Surfaced and fixed several latent type errors: unused imports removed, `error: null → undefined` alignment, `_ctx` unused-param rename, `void resolveSlowWait` for intentional float.
17
+ - **`GITHUB_TOOLS` const array and `GitHubToolId` type exported from installer** — the set of tools resolved via GitHub releases is now an exported `as const` array with a derived type, eliminating the duplicate definition that previously lived only in the test file.
18
+ - **`startupFailureWindowMs` option on `launchLSP`** — callers can now override the startup-failure detection window per-launch instead of relying solely on the Windows/non-Windows heuristic. Used by the LSP lifecycle test to avoid the full `WINDOWS_NAV_STARTUP_FAILURE_WINDOW_MS` delay in CI.
19
+ - **Test log pollution fix for read-guard** — `read-guard.test.ts` now mocks `read-guard-logger` unconditionally, so test events never reach `~/.pi-lens/read-guard.log` regardless of how the test suite is invoked.
20
+ - **Tab/space indentation mismatch correction in the edit hook** — some models output spaces in `oldText` when the file uses tabs (or vice versa), causing edits to fail with a cryptic "not found" error. The `tool_call` hook now detects this before execution by trying tabs↔2-spaces and tabs↔4-spaces conversions against the actual file. On mismatch it blocks with a `🔄 RETRYABLE` message containing the corrected `oldText` verbatim, so the model retries successfully on the next attempt at zero cost when `oldText` already matches.
21
+ - **Global project-data storage is now the default for new projects** — project-scoped pi-lens artifacts (turn state, worklog, metrics history, index, install choices, runner scratch data) now default to `~/.pi-lens/projects/<project-slug>/` instead of creating `<project>/.pi-lens/`. Existing projects that already have `<project>/.pi-lens/` continue to reuse it unless `PILENS_DATA_DIR` is explicitly set. This closes issue #40 while preserving backward compatibility.
22
+ - **`PILENS_DATA_DIR` and `PI_LENS_STARTUP_MODE` documented in README** — both env vars are now listed under a dedicated _Environment Variables_ section between `## Run` and `## Key Commands`.
23
+ - **Tree-sitter read expansion for the read-before-edit guard** — partial reads (requested `limit ≤ 60` lines) are now automatically expanded to cover the full enclosing function, method, or class using the tree-sitter AST. The agent receives the full symbol as context, and the read guard records symbol-level coverage so edits anywhere within the symbol pass without requiring the agent to have read every line. Supports TypeScript, TSX, JavaScript, JSX, Python, Go, Rust, and Ruby. Runs within a 200 ms budget; falls back silently on parse failure or unsupported extension. Replaces the dead LSP-based expansion (which required `limit = 1` and a warm server — zero production hits).
24
+ - **`read_pattern` structured log on every read** — `~/.pi-lens/read-guard.log` now records a `read_pattern` JSONL event for each read tool call: `offset`, `limit`, `totalLines`, `fractionRead`, `isPartial`, `fileKind`, and `expandedByTs`. Enables analysis of actual agent read behaviour across sessions.
25
+ - **`prettier.config.ts` and `eslint.config.ts` added to config detection arrays** — both config filenames are now recognised by `hasPrettierConfig` and `hasEslintConfig` respectively. Previously only `.js`/`.cjs`/`.mjs` variants were listed, so TypeScript-based configs were silently ignored.
26
+ - **Walk-up boundary stops at nearest `package.json`** — all 8 config-detection walk-up functions (`hasEslintConfig`, `getBiomeConfigPath`, `hasOxlintConfig`, `hasMypyConfig`, `hasDetektConfig`, `hasBlackConfig`, `hasRuffConfig`, `hasPrettierConfig`) now stop ascending once they reach the directory containing the nearest `package.json` instead of walking all the way to the filesystem root. This prevents cross-project config bleed in monorepos where an unrelated project higher up the tree happens to have a config file. A shared `walkUpDirsUntilPackageJson` helper encapsulates the boundary logic.
27
+ - **Formatter and linter selection logged to `latency.log`** — `getFormattersForFile` now emits a `formatter_selected` phase entry recording the chosen formatter name, selection reason (`explicit-config`, `smart-default`, `detect`, or `none`), and `cwd`. `getLinterPolicyForCwd` emits a `linter_selected` phase entry recording the chosen runner, gate, `cwd`, and the full detection-context flags. Both events are skipped in test mode.
28
+
29
+ ### Fixed
30
+
31
+ - **Config detection walks up the directory tree for all competing tools** — `hasEslintConfig`, `hasBiomeConfig` / `getBiomeConfigPath`, `hasOxlintConfig`, `hasMypyConfig`, `hasDetektConfig`, `hasBlackConfig`, and `hasRuffConfig` now all walk up to the filesystem root (matching the `findNearestPackageJsonPath` pattern) instead of only checking `cwd`. In monorepos where pi-lens passes a subdirectory as `cwd`, configs at the project root are now found correctly. Prevents wrong smart-default selection (e.g. oxlint firing instead of eslint, ruff firing instead of black) and restores optional runners (mypy, detekt) that were silently dropped when their configs lived above `cwd`. Functions with no competing smart-default (stylelint, sqlfluff, rubocop, golangci-lint, etc.) are unchanged.
32
+ - **Biome smart-default no longer overrides explicit Prettier config** — `getFormattersForFile` now only activates the Biome smart-default when no candidate formatter has explicit project config. Previously, a project with `.prettierrc` but no `biome.json` would still have Biome auto-installed and selected. `hasPrettierConfig` also now walks up the directory tree (matching the `findUp` pattern used elsewhere) so a Prettier config in a parent directory is detected even when pi-lens passes a subdirectory as `cwd`. The inline `package.json#prettier` field check uses `Object.prototype.hasOwnProperty` instead of truthiness, correctly handling `"prettier": false` and `"prettier": null`.
33
+ - **Duplicate `oldText` in edit calls now blocked early** — the read guard pre-flight check (`resolveOldTextEdits`) returns a `🔴 BLOCKED` error before the edit tool executes when `oldText` matches more than one location in the file, with per-match line numbers so the model can tighten its context.
34
+ - **Read-guard `oldText` inference hardened** — unresolved `oldText` targets no longer degrade into permissive `no_line_info` allows. Missing matches now return a blocking preflight error, partial multi-edit resolution blocks the whole edit, and indentation-correctable `oldText` is recognized during touched-line derivation as well as in the retryable pipeline guard.
35
+ - **Cascade diagnostics unified through review graph + LSP touch flow** — cascade results now accumulate as structured `CascadeResult` values across the turn, merge/deduplicate by dependent file at turn end, use review-graph references for broader neighbor discovery, respect TypeScript/Deno auto-propagation capabilities, and fall back to passive LSP snapshots when no trustworthy neighbor LSP data is produced.
36
+ - **Cascade LSP diagnostics now use shared conversion/tracking** — cascade diagnostics are converted through the shared LSP→dispatch diagnostic utility, participate in `DiagnosticTracker`, use separate cascade delta baselines (`session.baseline.cascade.*`), and share centralized cascade formatting.
37
+ - **`touchFile({ collectDiagnostics: true })`** — LSP touch can now return merged diagnostics from the clients it opened/synced, allowing cascade to collect diagnostics from the same silently touched clients without a second aggregate `getDiagnostics()` call.
38
+ - **Review graph workspace cache** — cascade graph builds now reuse the parsed review graph across pipeline invocations when source file mtimes/sizes are unchanged, while still applying per-write changed-symbol state. Cascade logs now record whether the graph was reused and the build mode.
39
+ - **`PILENS_DATA_DIR` env var for external project data storage** — when set, all project-generated data (caches, index, worklog, LSP install choices, elixir outputs, metrics history) is written to `$PILENS_DATA_DIR/<project-slug>/`. Slug is derived from the project's absolute path using the existing cross-platform `normalizeFilePath` utility.
40
+
41
+ ### Fixed
42
+
43
+ - **Cascade silent LSP opens no longer broadcast file-watch changes** — cascade neighbor reads now open documents with `silent: true`, suppressing `workspace/didChangeWatchedFiles` so TypeScript/Python servers do not schedule project-wide rechecks for every dependent file touched.
44
+ - **Cascade cache/fallback correctness** — per-turn cascade caches are scoped by turn/write sequence, empty cascade results are suppressed, no-LSP neighbors are treated as no signal, and degraded fallback now triggers when no neighbor produced LSP data rather than only when the graph returned zero neighbors.
45
+ - **LSP touch `no_clients` latency diagnostics** — `lsp_touch_file` no-client records now include attempted server count, source, and wait budget so slow no-client outcomes can be distinguished from unsupported-file fast paths.
46
+ - **Misleading LSP error when `filePath` is a directory** — `lsp_navigation` now stat-checks the resolved path before server lookup. Passing a directory (e.g. `.`) to `workspaceDiagnostics` falls through to workspace-scoped mode; file-scoped operations return a clear `filepath_is_directory` error instead of the previous "No LSP server available … Check that the language server is installed" message, which incorrectly implied an install problem.
47
+ - **LSP `didChangeWatchedFiles` sends correct change type** — `handleNotifyOpen` now uses `type: 2` (Changed) for existing files instead of unconditionally sending `type: 1` (Created). File-watching LSPs no longer treat every open as a newly created file, which could invalidate caches differently than intended.
48
+ - **`getAllDiagnostics()` deduplicates across multiple LSP clients** — when TypeScript + ESLint both report an error on the same line, the fallback/snapshot path now merges and deduplicates instead of showing both. Prevents duplicates from pushing out unique diagnostics under the `MAX_PER_FILE` cap.
49
+ - **`formatImpactCascade` respects configurable `cascadeMaxFiles`** — removed hardcoded `MAX_FILES = 4` in `format.ts`; the display cap now matches `RUNTIME_CONFIG.pipeline.cascadeMaxFiles` (default 8), so the impact header and truncation hint are consistent with actual analysis.
50
+ - **Turn-end cascade merge preserves impact context** — previously `runtime-turn.ts` rebuilt output from raw `neighbors`, discarding impact headers, changed symbols, risk flags, and truncation hints. It now uses the pre-built `CascadeResult.formatted` field (deduplicated by primary file), so the agent sees causal context ("Changed symbols: X", "Direct importers: Y", "Risk: Z") alongside diagnostics.
51
+ - **Neighbor touch cache is turn-scoped** — `neighborTouchCache` previously invalidated on every `writeIndex` bump, so reading a file then editing it would re-touch the same neighbor. The cache now keys on `turnSeq` only, so neighbors are touched once per turn regardless of how many files are edited.
52
+ - **Dead opportunistic LSP read expansion removed** — the `findSymbolAtLine` / `withTimeout` / `LSP_READ_EXPANSION_BUDGET_MS` code path was never triggered in production (zero `lsp_range_expanded` events outside tests) and added complexity/latency to every read tool call. Removed entirely. Read guard records now use `peekWriteIndex()` instead of `nextWriteIndex()`, fixing the cascade cache invalidation bug where reads incremented the write counter.
53
+ - **Test-mode guards for all loggers** — every logger that writes to `~/.pi-lens/` now skips disk I/O when `PI_LENS_TEST_MODE === "1"` or when running under `VITEST` (unless explicitly opted out with `PI_LENS_TEST_MODE=0`). Eliminates test pollution in `cascade.log`, `read-guard.log`, `latency.log`, `sessionstart.log`, `tree-sitter.log`, and diagnostic JSONL. The `dbg()` function already had this guard; it is now applied consistently across `logCascade`, `logReadGuardEvent`, `logLatency`, `logTreeSitter`, `logSessionStart`, and `DiagnosticLogger.log`.
54
+ - **`read-guard.log` included in automatic cleanup** — `runLogCleanup()` now covers `read-guard.log` alongside the existing `sessionstart.log`, `tree-sitter.log`, and `cascade.log`.
55
+
56
+ - **oxfmt `.oxfmtrc.json` detection** — `hasOxfmtConfig` now treats `.oxfmtrc.json` as an activation signal alongside `oxfmt.toml` and `@oxc-project/oxfmt` in package.json.
57
+
9
58
  ## [3.8.33] - 2026-04-27
10
59
 
11
60
  ### Fixed
@@ -1023,6 +1072,7 @@ All runtime-applicable TypeScript ast-grep rules now have JavaScript equivalents
1023
1072
  - **Rust performance core (`pi-lens-core`)** — Optional Rust binary for CPU-intensive operations.
1024
1073
  All features fall back to TypeScript automatically if the binary is not available (it is **not**
1025
1074
  built automatically on `npm install` — run `npm run rust:build` once if you have Rust installed).
1075
+
1026
1076
  - **File scanning** — ripgrep’s `ignore` crate for `.gitignore`-aware project scanning
1027
1077
  - **Similarity detection** — parallel 57×72 state-matrix index, persisted to
1028
1078
  `.pi-lens/rust-index.json` between invocations (fixes in-memory cache that reset on every
@@ -1076,6 +1126,7 @@ All runtime-applicable TypeScript ast-grep rules now have JavaScript equivalents
1076
1126
  - Removed `clients/interviewer-templates.ts` (240 lines)
1077
1127
  - Removed initialization from `index.ts`
1078
1128
  - **Deleted deprecated commands** — All were superseded by `/lens-booboo`:
1129
+
1079
1130
  - `/lens-booboo-fix` command (fix-from-booboo.ts, 430 lines) — showed warning to use `/lens-booboo`
1080
1131
  - `/lens-fix-simplified` command (fix-simplified.ts, 770 lines) — never registered, unused
1081
1132
  - `/lens-rate` command (rate.ts, 340 lines) — showed warning to use `/lens-booboo`
@@ -1094,6 +1145,7 @@ All runtime-applicable TypeScript ast-grep rules now have JavaScript equivalents
1094
1145
  - Broken runner tests (7 files) — thin CLI wrappers with wrong imports
1095
1146
  - Trivial utility tests (5 files) — file extension parsing, string sanitization
1096
1147
  - **Added meaningful integration tests**:
1148
+
1097
1149
  - `tests/clients/dispatch/dispatcher-flow.test.ts` — Runner registration, execution, delta mode, conditional runners
1098
1150
  - `tests/extension-hooks.test.ts` — pi API: tool/command/flag registration, event handlers
1099
1151
  - `tests/mocks/runner-factory.ts` — Mock runners for testing without real CLI tools
@@ -1429,6 +1481,7 @@ Migrated 20 critical security rules to NAPI (fast native execution):
1429
1481
  Three new lint runners with full test coverage:
1430
1482
 
1431
1483
  - **Spellcheck runner** (`clients/dispatch/runners/spellcheck.ts`): Markdown spellchecking
1484
+
1432
1485
  - Uses `typos-cli` (Rust-based, fast, low false positives)
1433
1486
  - Checks `.md` and `.mdx` files
1434
1487
  - Priority 30, runs after code quality checks
@@ -1436,6 +1489,7 @@ Three new lint runners with full test coverage:
1436
1489
  - Install: `cargo install typos-cli`
1437
1490
 
1438
1491
  - **Oxlint runner** (`clients/dispatch/runners/oxlint.ts`): Fast JS/TS linting
1492
+
1439
1493
  - Uses `oxlint` from Oxc project (Rust-based, ~100x faster than ESLint)
1440
1494
  - Zero-config by default
1441
1495
  - JSON output with fix suggestions
package/README.md CHANGED
@@ -30,6 +30,7 @@ At `session_start`, pi-lens:
30
30
  - warms caches and optional indexes (with overlap/session guardrails)
31
31
  - emits missing-tool install hints for detected languages when relevant
32
32
  - injects session guidance through internal context (non-user channel) to reduce acknowledgement-only first responses
33
+ - opens `warmFiles` (if configured in `.pi-lens/lsp.json`) to seed lazy-indexing language servers like clangd before the first symbol query
33
34
 
34
35
  For one-shot print sessions (for example `pi --print ...`), pi-lens auto-uses a quick startup path that skips heavy bootstrap work to reduce startup latency. Override with `PI_LENS_STARTUP_MODE=full|minimal|quick`.
35
36
 
@@ -64,6 +65,12 @@ pi-lens includes **37 language server definitions**. LSP is **enabled by default
64
65
 
65
66
  **LSP Idle Management:** LSP servers shut down after 240 seconds of inactivity (no files modified) to free resources. The timer resets when you resume editing, preventing cold-start penalties during active development.
66
67
 
68
+ **Warm files:** For language servers that index lazily (e.g. clangd), configure `warmFiles` in `.pi-lens/lsp.json` to open entry-point files at session start so the server has AST/index context before the first symbol query:
69
+
70
+ ```json
71
+ { "warmFiles": ["src/main.cpp", "src/lib.cpp"] }
72
+ ```
73
+
67
74
  LSP servers for: TypeScript, Deno, Python (pyright + pylsp), Go, Rust, Ruby (ruby-lsp + solargraph), PHP, C# (omnisharp), F#, Java, Kotlin, Swift, Dart, Lua, C/C++, Zig, Haskell, Elixir, Gleam, OCaml, Clojure, Terraform, Nix, Bash, Docker, YAML, JSON, HTML, TOML, Prisma, Vue, Svelte, ESLint, CSS.
68
75
 
69
76
  ### Formatters
@@ -90,7 +97,7 @@ pi-lens enforces a **read-before-edit** policy on all file writes and edits. Bef
90
97
  - **File-modified block** — blocks if the file changed on disk since the last read (auto-format, external tool, or a previous edit that was then reformatted)
91
98
  - **Out-of-range block** — blocks if the edit target lines fall outside the ranges previously read, ensuring the agent cannot modify code it hasn't seen
92
99
 
93
- Coverage is tracked across multiple reads: two reads of lines 1–100 and 101–200 together satisfy a full-file write. LSP-expanded reads (single-line reads silently widened to the enclosing symbol) count toward coverage. Markdown, text, and log files are exempt.
100
+ Coverage is tracked across multiple reads: two reads of lines 1–100 and 101–200 together satisfy a full-file write. Symbol-expanded reads (small reads silently widened to the enclosing symbol via tree-sitter) count toward coverage at the symbol level. Markdown, text, and log files are exempt.
94
101
 
95
102
  Override for a single edit: `/lens-allow-edit <path>`
96
103
 
@@ -98,7 +105,9 @@ Configure behavior with `--no-read-guard` to disable entirely, or set mode to `w
98
105
 
99
106
  ### Opportunistic Read Expansion
100
107
 
101
- When the agent reads a single line of a file and a warm LSP client is already running for that language, pi-lens transparently expands the read to the full enclosing symbol (function, method, or class). This happens without blocking the read if LSP responds in time, the agent sees the full context; otherwise the original line is returned unchanged.
108
+ When the agent reads a small slice of a file (≤ 60 lines), pi-lens transparently expands the read to the full enclosing symbol (function, method, or class) using the tree-sitter AST. The agent receives the full symbol as context, and the read guard records symbol-level coverage so edits anywhere within that symbol pass without requiring the agent to have read every line individually. Expansion runs within a 200 ms budget and falls back silently on unsupported file types or parse failures.
109
+
110
+ Supported: TypeScript, TSX, JavaScript, JSX, Python, Go, Rust, Ruby.
102
111
 
103
112
  ### Fact Rules Pipeline
104
113
 
@@ -196,6 +205,18 @@ pi --no-delta # Disable delta mode (show all diagnostics, not just n
196
205
  pi --lens-guard # Block git commit/push when unresolved blockers exist (experimental)
197
206
  ```
198
207
 
208
+ ## Environment Variables
209
+
210
+ - `PILENS_DATA_DIR` — redirect per-project state (scanner caches,
211
+ turn-state.json) out of the project directory. By default pi-lens writes
212
+ `<cwd>/.pi-lens/`; if set, it writes to
213
+ `<PILENS_DATA_DIR>/<sanitized-cwd-slug>/` instead. Useful for keeping repos
214
+ clean or for mounted/ephemeral setups. Tool binaries always live in
215
+ `~/.pi-lens/bin/` regardless.
216
+ - `PI_LENS_STARTUP_MODE` — `full` | `minimal` | `quick`. Override the
217
+ auto-selected startup path. One-shot `pi --print` sessions auto-use `quick`
218
+ to reduce latency.
219
+
199
220
  ## Key Commands
200
221
 
201
222
  - `/lens-booboo` — full quality report for current project state
@@ -11,6 +11,7 @@
11
11
 
12
12
  import * as fs from "node:fs";
13
13
  import * as path from "node:path";
14
+ import { getProjectDataDir } from "./file-utils.js";
14
15
  import { normalizeMapKey } from "./path-utils.js";
15
16
 
16
17
  // --- Types ---
@@ -57,7 +58,7 @@ const DEFAULT_TURN_STATE: TurnState = {
57
58
  // --- Helpers ---
58
59
 
59
60
  function getLensDir(cwd: string): string {
60
- return path.join(cwd, ".pi-lens");
61
+ return getProjectDataDir(cwd);
61
62
  }
62
63
 
63
64
  function getCacheDir(cwd: string): string {
@@ -0,0 +1,27 @@
1
+ import type { CascadeNeighborResult } from "./cascade-types.js";
2
+ import { toRunnerDisplayPath } from "./dispatch/runner-context.js";
3
+
4
+ export function formatCascadeNeighborDiagnostics(
5
+ cwd: string,
6
+ neighbors: CascadeNeighborResult[],
7
+ options: { noun?: string; includeReason?: boolean } = {},
8
+ ): string {
9
+ const withErrors = neighbors.filter((n) => n.diagnostics.length > 0);
10
+ if (withErrors.length === 0) return "";
11
+
12
+ const noun = options.noun ?? "neighbor";
13
+ let out = `📐 Cascade errors in ${withErrors.length} ${noun} file(s) — fix before finishing turn:`;
14
+ for (const neighbor of withErrors) {
15
+ const display = toRunnerDisplayPath(cwd, neighbor.filePath);
16
+ const reason = options.includeReason ? ` reason="${neighbor.reason}"` : "";
17
+ out += `\n<diagnostics file="${display}"${reason}>`;
18
+ for (const d of neighbor.diagnostics) {
19
+ const line = d.line ?? 1;
20
+ const col = d.column ?? 1;
21
+ const rule = d.rule ? ` rule=${d.rule}` : "";
22
+ out += `\n line ${line}, col ${col}${rule}: ${d.message.split("\n")[0].slice(0, 100)}`;
23
+ }
24
+ out += "\n</diagnostics>";
25
+ }
26
+ return out;
27
+ }
@@ -0,0 +1,78 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+
5
+ const CASCADE_LOG_DIR = path.join(os.homedir(), ".pi-lens");
6
+ const CASCADE_LOG_FILE = path.join(CASCADE_LOG_DIR, "cascade.log");
7
+
8
+ try {
9
+ if (!fs.existsSync(CASCADE_LOG_DIR)) {
10
+ fs.mkdirSync(CASCADE_LOG_DIR, { recursive: true });
11
+ }
12
+ } catch {}
13
+
14
+ export interface CascadeLogEntry {
15
+ ts?: string;
16
+ phase:
17
+ | "cascade_skip" // primary has blockers — cascade suppressed
18
+ | "graph_build" // graph built or reused
19
+ | "neighbors_computed" // impact cascade result ready
20
+ | "neighbor_touch" // single neighbor LSP active touch result
21
+ | "neighbor_snapshot" // neighbor read from passive snapshot (autoPropagate jsts)
22
+ | "neighbor_fallback" // neighbor fell back to getAllDiagnostics (error or degraded)
23
+ | "cascade_result" // final per-file cascade result
24
+ | "cascade_turn_end"; // merged result emitted at turn_end
25
+ filePath: string;
26
+ neighborFile?: string;
27
+ reason?: string;
28
+
29
+ // graph_build
30
+ graphBuiltMs?: number;
31
+ graphReused?: boolean; // true when FactStore cache was valid (future: incremental rebuild)
32
+ graphNodeCount?: number;
33
+ graphFileCount?: number;
34
+ graphChangedSymbolCount?: number;
35
+
36
+ // neighbors_computed
37
+ neighborCount?: number;
38
+ totalNeighborCount?: number; // before cap
39
+ importerCount?: number;
40
+ callerCount?: number;
41
+ referenceCount?: number;
42
+ riskFlags?: string[];
43
+
44
+ // neighbor_snapshot
45
+ snapshotMissing?: boolean; // true when file not found in allDiags
46
+ snapshotAgeSec?: number; // age of snapshot entry in seconds
47
+
48
+ // neighbor_touch
49
+ lspServerCount?: number; // number of LSP servers configured for this file type
50
+ touchedCount?: number;
51
+ snapshotCount?: number;
52
+
53
+ // shared
54
+ fallbackUsed?: boolean;
55
+ diagnosticCount?: number;
56
+ durationMs?: number;
57
+ autoPropagate?: boolean;
58
+ lspTouched?: boolean;
59
+ error?: string;
60
+ metadata?: Record<string, unknown>;
61
+ }
62
+
63
+ export function logCascade(entry: CascadeLogEntry): void {
64
+ if (
65
+ process.env.PI_LENS_TEST_MODE === "1" ||
66
+ (process.env.VITEST && process.env.PI_LENS_TEST_MODE !== "0")
67
+ ) {
68
+ return;
69
+ }
70
+ const line = `${JSON.stringify({ ts: new Date().toISOString(), ...entry })}\n`;
71
+ try {
72
+ fs.appendFileSync(CASCADE_LOG_FILE, line);
73
+ } catch {}
74
+ }
75
+
76
+ export function getCascadeLogPath(): string {
77
+ return CASCADE_LOG_FILE;
78
+ }
@@ -0,0 +1,17 @@
1
+ import type { Diagnostic } from "./dispatch/types.js";
2
+ import type { ImpactCascadeResult } from "./review-graph/types.js";
3
+
4
+ export interface CascadeNeighborResult {
5
+ filePath: string;
6
+ reason: "imports" | "calls" | "references" | "fallback";
7
+ diagnostics: Diagnostic[];
8
+ lspTouched: boolean;
9
+ durationMs?: number;
10
+ }
11
+
12
+ export interface CascadeResult {
13
+ filePath: string;
14
+ impact: ImpactCascadeResult;
15
+ neighbors: CascadeNeighborResult[];
16
+ formatted: string;
17
+ }
@@ -108,6 +108,12 @@ export function createDiagnosticLogger(): DiagnosticLogger {
108
108
 
109
109
  return {
110
110
  log(entry: DiagnosticEntry) {
111
+ if (
112
+ process.env.PI_LENS_TEST_MODE === "1" ||
113
+ (process.env.VITEST && process.env.PI_LENS_TEST_MODE !== "0")
114
+ ) {
115
+ return;
116
+ }
111
117
  pending.push(entry);
112
118
  writePending(); // async, non-blocking
113
119
  },