pi-lens 4.1.2 → 4.1.4

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 (359) hide show
  1. package/CHANGELOG.md +1308 -0
  2. package/README.md +3 -0
  3. package/config/biome/core.jsonc +11 -2
  4. package/config/dependency-cruiser-eager-allowlist.json +91 -0
  5. package/dist/clients/actionable-warnings-logger.js +18 -3
  6. package/dist/clients/actionable-warnings.js +927 -80
  7. package/dist/clients/agent-behavior-client.js +13 -4
  8. package/dist/clients/ast-grep-client.js +136 -9
  9. package/dist/clients/ast-grep-rule-manager.js +60 -5
  10. package/dist/clients/ast-grep-tool-logger.js +2 -2
  11. package/dist/clients/availability-probe-flight.js +12 -0
  12. package/dist/clients/bash-file-access.js +1 -3
  13. package/dist/clients/biome-client.js +16 -2
  14. package/dist/clients/blocker-freshness.js +34 -0
  15. package/dist/clients/bootstrap.js +509 -73
  16. package/dist/clients/bounded-cache.js +152 -12
  17. package/dist/clients/bounded-telemetry.js +115 -23
  18. package/dist/clients/build-identity.js +95 -0
  19. package/dist/clients/bundled-resource-health.js +113 -0
  20. package/dist/clients/bus-events-logger.js +2 -2
  21. package/dist/clients/bus-publish.js +3 -0
  22. package/dist/clients/cache/rule-cache.js +33 -5
  23. package/dist/clients/cache-manager.js +105 -6
  24. package/dist/clients/cache-observability.js +46 -19
  25. package/dist/clients/cargo-manifest.js +422 -0
  26. package/dist/clients/cascade-logger.js +17 -3
  27. package/dist/clients/code-quality-warnings.js +13 -3
  28. package/dist/clients/complexity-client.js +16 -5
  29. package/dist/clients/config-core/deny.js +221 -0
  30. package/dist/clients/config-core/index.js +47 -0
  31. package/dist/clients/config-core/merge.js +357 -0
  32. package/dist/clients/config-core/normalize.js +340 -0
  33. package/dist/clients/config-core/process-spec.js +248 -0
  34. package/dist/clients/config-core/provenance.js +171 -0
  35. package/dist/clients/config-core/records.js +218 -0
  36. package/dist/clients/config-core/resolve.js +125 -0
  37. package/dist/clients/config-core/safe-object.js +78 -0
  38. package/dist/clients/config-core/schema.js +167 -0
  39. package/dist/clients/config-diagnostic-codes.js +281 -0
  40. package/dist/clients/config-locations.js +160 -0
  41. package/dist/clients/config-resolve.js +789 -0
  42. package/dist/clients/config-schema.js +197 -0
  43. package/dist/clients/config-warn.js +407 -0
  44. package/dist/clients/dead-code-client.js +33 -25
  45. package/dist/clients/dead-code-logger.js +2 -2
  46. package/dist/clients/deadline-utils.js +178 -0
  47. package/dist/clients/debug-handles.js +2 -2
  48. package/dist/clients/debug-heap.js +3 -3
  49. package/dist/clients/deferred-lsp-work.js +106 -0
  50. package/dist/clients/degradation-ledger.js +166 -8
  51. package/dist/clients/demoted-finding-render.js +14 -0
  52. package/dist/clients/dependency-checker.js +28 -22
  53. package/dist/clients/deps/minimatch.js +1 -1
  54. package/dist/clients/diagnostic-line-freshness.js +12 -9
  55. package/dist/clients/diagnostic-logger.js +14 -4
  56. package/dist/clients/dispatch/collect-later-tier.js +39 -0
  57. package/dist/clients/dispatch/dispatcher.js +304 -19
  58. package/dist/clients/dispatch/fact-store.js +328 -9
  59. package/dist/clients/dispatch/integration.js +214 -107
  60. package/dist/clients/dispatch/pending-runner-findings.js +87 -0
  61. package/dist/clients/dispatch/plan.js +21 -5
  62. package/dist/clients/dispatch/runners/actionlint.js +0 -1
  63. package/dist/clients/dispatch/runners/ast-grep-napi.js +550 -77
  64. package/dist/clients/dispatch/runners/biome-check.js +0 -1
  65. package/dist/clients/dispatch/runners/cpp-check.js +0 -1
  66. package/dist/clients/dispatch/runners/credo.js +0 -1
  67. package/dist/clients/dispatch/runners/cue-vet.js +0 -1
  68. package/dist/clients/dispatch/runners/dart-analyze.js +0 -1
  69. package/dist/clients/dispatch/runners/detekt.js +0 -1
  70. package/dist/clients/dispatch/runners/dotnet-build.js +0 -1
  71. package/dist/clients/dispatch/runners/elixir-check.js +0 -1
  72. package/dist/clients/dispatch/runners/eslint.js +5 -2
  73. package/dist/clients/dispatch/runners/fact-rules.js +0 -1
  74. package/dist/clients/dispatch/runners/fish-indent.js +0 -1
  75. package/dist/clients/dispatch/runners/gleam-check.js +0 -1
  76. package/dist/clients/dispatch/runners/go-vet.js +1 -3
  77. package/dist/clients/dispatch/runners/golangci-lint.js +0 -1
  78. package/dist/clients/dispatch/runners/hadolint.js +0 -1
  79. package/dist/clients/dispatch/runners/helm-lint.js +16 -4
  80. package/dist/clients/dispatch/runners/helm-render.js +54 -10
  81. package/dist/clients/dispatch/runners/htmlhint.js +0 -1
  82. package/dist/clients/dispatch/runners/index.js +1 -3
  83. package/dist/clients/dispatch/runners/javac.js +0 -1
  84. package/dist/clients/dispatch/runners/ktlint.js +0 -1
  85. package/dist/clients/dispatch/runners/lsp.js +37 -2
  86. package/dist/clients/dispatch/runners/markdownlint.js +4 -2
  87. package/dist/clients/dispatch/runners/mypy.js +0 -1
  88. package/dist/clients/dispatch/runners/oxlint.js +24 -22
  89. package/dist/clients/dispatch/runners/php-lint.js +0 -1
  90. package/dist/clients/dispatch/runners/phpstan.js +0 -1
  91. package/dist/clients/dispatch/runners/prisma-validate.js +0 -1
  92. package/dist/clients/dispatch/runners/psscriptanalyzer.js +4 -1
  93. package/dist/clients/dispatch/runners/pyright.js +7 -4
  94. package/dist/clients/dispatch/runners/rubocop.js +0 -1
  95. package/dist/clients/dispatch/runners/ruff.js +0 -1
  96. package/dist/clients/dispatch/runners/rust-clippy.js +9 -6
  97. package/dist/clients/dispatch/runners/shellcheck.js +0 -1
  98. package/dist/clients/dispatch/runners/shfmt.js +0 -1
  99. package/dist/clients/dispatch/runners/spellcheck.js +0 -1
  100. package/dist/clients/dispatch/runners/spotbugs.js +0 -1
  101. package/dist/clients/dispatch/runners/sqlfluff.js +0 -1
  102. package/dist/clients/dispatch/runners/stylelint.js +0 -1
  103. package/dist/clients/dispatch/runners/swiftlint.js +0 -1
  104. package/dist/clients/dispatch/runners/taplo.js +0 -1
  105. package/dist/clients/dispatch/runners/terragrunt.js +0 -1
  106. package/dist/clients/dispatch/runners/tflint.js +0 -1
  107. package/dist/clients/dispatch/runners/tree-sitter.js +0 -1
  108. package/dist/clients/dispatch/runners/trivy-config.js +0 -1
  109. package/dist/clients/dispatch/runners/utils/availability-policy.js +3 -0
  110. package/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +3 -4
  111. package/dist/clients/dispatch/runners/utils/runner-helpers.js +270 -105
  112. package/dist/clients/dispatch/runners/utils/spawn-outcome.js +4 -0
  113. package/dist/clients/dispatch/runners/utils/tool-failure.js +19 -7
  114. package/dist/clients/dispatch/runners/utils/toolchain-availability.js +89 -26
  115. package/dist/clients/dispatch/runners/vale.js +0 -1
  116. package/dist/clients/dispatch/runners/yaml-rule-parser.js +51 -17
  117. package/dist/clients/dispatch/runners/yamllint.js +0 -1
  118. package/dist/clients/dispatch/runners/zig-check.js +0 -1
  119. package/dist/clients/disposition-logger.js +3 -3
  120. package/dist/clients/effective-config.js +403 -0
  121. package/dist/clients/error-class.js +23 -0
  122. package/dist/clients/event-loop-hold.js +274 -0
  123. package/dist/clients/event-loop-monitor.js +66 -1
  124. package/dist/clients/extension-log.js +2 -2
  125. package/dist/clients/feature-hints.js +2 -1
  126. package/dist/clients/file-time.js +6 -1
  127. package/dist/clients/file-utils.js +266 -13
  128. package/dist/clients/finding-delivery-gate.js +55 -13
  129. package/dist/clients/format-service.js +6 -1
  130. package/dist/clients/formatters.js +303 -104
  131. package/dist/clients/freshness-cadence.js +17 -0
  132. package/dist/clients/generated-artifacts.js +114 -19
  133. package/dist/clients/generation-guard.js +4 -9
  134. package/dist/clients/git-guard.js +198 -48
  135. package/dist/clients/git-tracked-ignore.js +32 -1
  136. package/dist/clients/go-client.js +39 -0
  137. package/dist/clients/govulncheck-client.js +5 -1
  138. package/dist/clients/gradle-ktfmt-style.js +252 -0
  139. package/dist/clients/gzip-stage-write.js +7 -0
  140. package/dist/clients/hashline-anchor.js +424 -0
  141. package/dist/clients/hook-budgets.js +76 -0
  142. package/dist/clients/host-edit-normalize.js +5 -2
  143. package/dist/clients/host-ports.js +1 -1
  144. package/dist/clients/installer/index.js +200 -84
  145. package/dist/clients/installer/managed-tool-refresh.js +3 -1
  146. package/dist/clients/instance-reaper.js +108 -243
  147. package/dist/clients/instance-registry-lock.js +145 -0
  148. package/dist/clients/instance-registry.js +435 -122
  149. package/dist/clients/jscpd-client.js +6 -1
  150. package/dist/clients/language-profile.js +4 -2
  151. package/dist/clients/language-registry.js +595 -0
  152. package/dist/clients/latency-logger.js +179 -3
  153. package/dist/clients/ledger-bounds.js +13 -0
  154. package/dist/clients/lens-config.js +141 -30
  155. package/dist/clients/lens-engine.js +30 -37
  156. package/dist/clients/lens-flag-registry.js +67 -6
  157. package/dist/clients/lens-map.js +3 -1
  158. package/dist/clients/log-cleanup.js +2 -2
  159. package/dist/clients/lsp/aggregation.js +3 -1
  160. package/dist/clients/lsp/cascade-tier.js +4 -0
  161. package/dist/clients/lsp/client.js +672 -49
  162. package/dist/clients/lsp/config.js +504 -156
  163. package/dist/clients/lsp/diagnostic-binding.js +10 -5
  164. package/dist/clients/lsp/edits.js +15 -2
  165. package/dist/clients/lsp/index.js +1179 -247
  166. package/dist/clients/lsp/jvm-runtime.js +5 -0
  167. package/dist/clients/lsp/language.js +18 -169
  168. package/dist/clients/lsp/launch.js +38 -9
  169. package/dist/clients/lsp/pending-aux-coverage.js +113 -12
  170. package/dist/clients/lsp/server.js +426 -160
  171. package/dist/clients/lsp/session-roots.js +192 -0
  172. package/dist/clients/lsp/spawn-history.js +2 -7
  173. package/dist/clients/lsp/tsserver-sync.js +9 -2
  174. package/dist/clients/lsp/workspace-diagnostics-cache.js +17 -7
  175. package/dist/clients/lsp/workspace-sweep-hold.js +32 -23
  176. package/dist/clients/lsp-mutation.js +179 -23
  177. package/dist/clients/map-with-concurrency.js +36 -0
  178. package/dist/clients/mcp/analyze.js +45 -10
  179. package/dist/clients/mcp/session.js +19 -17
  180. package/dist/clients/memory-sampler.js +28 -1
  181. package/dist/clients/message-end-attribution.js +30 -0
  182. package/dist/clients/metrics-history.js +21 -4
  183. package/dist/clients/middle-man-analysis.js +2 -4
  184. package/dist/clients/module-report.js +20 -45
  185. package/dist/clients/mutating-tool.js +651 -0
  186. package/dist/clients/mutation-attribution.js +368 -0
  187. package/dist/clients/mutation-bridge.js +240 -0
  188. package/dist/clients/ndjson-logger.js +247 -20
  189. package/dist/clients/observed-mutation-sources.js +101 -0
  190. package/dist/clients/observed-mutation.js +1215 -0
  191. package/dist/clients/opaque-mutation-scan.js +300 -40
  192. package/dist/clients/opengrep-config.js +4 -0
  193. package/dist/clients/package-manager.js +201 -44
  194. package/dist/clients/partial-edit-apply.js +359 -80
  195. package/dist/clients/path-attribution-telemetry.js +23 -7
  196. package/dist/clients/path-keyed-map.js +35 -3
  197. package/dist/clients/path-utils.js +460 -13
  198. package/dist/clients/persist-debounce.js +8 -1
  199. package/dist/clients/php-cs-fixer-config.js +114 -0
  200. package/dist/clients/pipeline.js +77 -24
  201. package/dist/clients/probe-home-state.js +228 -0
  202. package/dist/clients/process-bridge.js +66 -0
  203. package/dist/clients/process-singletons.js +210 -0
  204. package/dist/clients/process-snapshot.js +68 -0
  205. package/dist/clients/project-diagnostics/fresh-fetch.js +4 -3
  206. package/dist/clients/project-diagnostics/runner-adapters/madge.js +2 -1
  207. package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +27 -3
  208. package/dist/clients/project-diagnostics/scanner.js +61 -24
  209. package/dist/clients/project-lens-config.js +393 -122
  210. package/dist/clients/project-snapshot.js +85 -35
  211. package/dist/clients/python-environment.js +217 -0
  212. package/dist/clients/python-provenance.js +639 -0
  213. package/dist/clients/quiet-window.js +6 -1
  214. package/dist/clients/read-bridge.js +13 -21
  215. package/dist/clients/read-expansion.js +9 -46
  216. package/dist/clients/read-guard-logger.js +39 -6
  217. package/dist/clients/read-guard-tool-lines.js +364 -212
  218. package/dist/clients/read-guard.js +95 -7
  219. package/dist/clients/recent-touches.js +17 -5
  220. package/dist/clients/resource-sampler.js +195 -96
  221. package/dist/clients/review-graph/builder.js +752 -132
  222. package/dist/clients/review-graph/service.js +55 -3
  223. package/dist/clients/review-graph/shared-extraction-ir.js +7 -15
  224. package/dist/clients/review-graph/tsconfig-paths.js +5 -1
  225. package/dist/clients/review-graph/workspace-modules.js +10 -74
  226. package/dist/clients/review-graph-logger.js +30 -4
  227. package/dist/clients/runtime-agent-end.js +18 -2
  228. package/dist/clients/runtime-context.js +49 -16
  229. package/dist/clients/runtime-coordinator.js +93 -6
  230. package/dist/clients/runtime-session.js +169 -34
  231. package/dist/clients/runtime-tool-call.js +183 -35
  232. package/dist/clients/runtime-tool-result.js +894 -241
  233. package/dist/clients/runtime-turn.js +1264 -74
  234. package/dist/clients/rust-client.js +31 -0
  235. package/dist/clients/safe-spawn.js +191 -28
  236. package/dist/clients/sanitize.js +7 -1
  237. package/dist/clients/scratch-tree-policy.js +1 -3
  238. package/dist/clients/security-scan-client.js +51 -17
  239. package/dist/clients/session-lifecycle.js +238 -42
  240. package/dist/clients/session-start-observability.js +79 -0
  241. package/dist/clients/sessionstart-logger.js +12 -3
  242. package/dist/clients/sg-runner.js +27 -15
  243. package/dist/clients/sgconfig.js +6 -1
  244. package/dist/clients/shared-checkout-guard.js +338 -0
  245. package/dist/clients/skills-resolver.js +105 -0
  246. package/dist/clients/smells-rollup.js +2 -2
  247. package/dist/clients/spawn-output-cap.js +37 -0
  248. package/dist/clients/spawn-timeout-cooldown.js +15 -1
  249. package/dist/clients/startup-scan.js +6 -7
  250. package/dist/clients/startup-timing.js +69 -4
  251. package/dist/clients/string-utils.js +33 -0
  252. package/dist/clients/subagent-mode.js +17 -4
  253. package/dist/clients/test-runner-client.js +586 -118
  254. package/dist/clients/test-runner-delivery.js +239 -0
  255. package/dist/clients/tool-definition.js +41 -1
  256. package/dist/clients/tool-policy.js +92 -22
  257. package/dist/clients/tree-sitter-cache.js +40 -40
  258. package/dist/clients/tree-sitter-client.js +97 -53
  259. package/dist/clients/tree-sitter-logger.js +19 -3
  260. package/dist/clients/tree-sitter-query-loader.js +81 -1
  261. package/dist/clients/tree-sitter-shared.js +51 -46
  262. package/dist/clients/tree-sitter-symbol-extractor.js +28 -0
  263. package/dist/clients/typos-config.js +5 -0
  264. package/dist/clients/user-notify.js +6 -2
  265. package/dist/clients/warm-attach.js +8 -10
  266. package/dist/clients/widget-state.js +191 -26
  267. package/dist/clients/word-index-logger.js +15 -3
  268. package/dist/clients/word-index-store.js +463 -0
  269. package/dist/clients/word-index.js +815 -125
  270. package/dist/clients/workspace-topology.js +25 -1
  271. package/dist/clients/zizmor-config.js +29 -6
  272. package/dist/index.js +68577 -53950
  273. package/dist/mcp/analyze-cli.js +3 -1
  274. package/dist/mcp/build-staleness.js +8 -7
  275. package/dist/mcp/cli.js +3 -1
  276. package/dist/mcp/server.js +133 -6
  277. package/dist/scripts/lib/process-scan.mjs +583 -0
  278. package/dist/scripts/lib/skills-predicate.mjs +129 -0
  279. package/dist/tools/effective-config.js +89 -0
  280. package/dist/tools/lens-diagnostics.js +99 -24
  281. package/dist/tools/lsp-diagnostics.js +81 -85
  282. package/dist/tools/lsp-navigation.js +40 -17
  283. package/dist/tools/shared.js +0 -1
  284. package/docs/agent-guide.md +68 -3
  285. package/docs/ast-grep_rules_catalog.md +41 -10
  286. package/docs/astplayground.md +26 -17
  287. package/docs/configuration.md +222 -0
  288. package/docs/dependencies.md +3 -3
  289. package/docs/features.md +44 -5
  290. package/docs/language-coverage.md +2 -2
  291. package/docs/pi-lens-fixer.md +25 -0
  292. package/docs/pi-lens-investigator.md +25 -0
  293. package/docs/pi-lens-reviewer.md +27 -0
  294. package/docs/pi-lens-subagent.md +38 -0
  295. package/docs/pi-lens-warden.md +55 -0
  296. package/docs/public-api-stability.md +359 -0
  297. package/docs/release-qa-baseline.md +182 -0
  298. package/docs/subagent-compat.md +110 -30
  299. package/docs/tree-sitter_rules_catalog.md +2 -2
  300. package/package.json +17 -21
  301. package/rules/ast-grep-rules/rule-tests/c-no-malloc-free-test.yml +20 -0
  302. package/rules/ast-grep-rules/rule-tests/cpp-no-malloc-free-test.yml +25 -0
  303. package/rules/ast-grep-rules/rule-tests/cpp-no-printf-test.yml +19 -0
  304. package/rules/ast-grep-rules/rule-tests/go-no-fmt-println-test.yml +19 -0
  305. package/rules/ast-grep-rules/rule-tests/go-no-panic-in-lib-test.yml +17 -0
  306. package/rules/ast-grep-rules/rule-tests/go-no-underscore-func-name-test.yml +18 -0
  307. package/rules/ast-grep-rules/rule-tests/go-prefer-errors-is-test.yml +15 -0
  308. package/rules/ast-grep-rules/rule-tests/go-prefer-string-builder-test.yml +21 -0
  309. package/rules/ast-grep-rules/rule-tests/hyphenated-svg-attribute-test.yml +11 -0
  310. package/rules/ast-grep-rules/rule-tests/kotlin-no-lateinit-test.yml +17 -0
  311. package/rules/ast-grep-rules/rule-tests/kotlin-no-nullable-boolean-test.yml +15 -0
  312. package/rules/ast-grep-rules/rule-tests/kotlin-no-println-test.yml +19 -0
  313. package/rules/ast-grep-rules/rule-tests/kotlin-no-unnecessary-let-test.yml +7 -0
  314. package/rules/ast-grep-rules/rule-tests/kotlin-no-unsafe-cast-test.yml +7 -0
  315. package/rules/ast-grep-rules/rule-tests/kotlin-no-var-test.yml +7 -0
  316. package/rules/ast-grep-rules/rule-tests/kotlin-prefer-data-class-test.yml +25 -0
  317. package/rules/ast-grep-rules/rule-tests/kotlin-prefer-is-empty-test.yml +11 -0
  318. package/rules/ast-grep-rules/rule-tests/no-important-test.yml +21 -0
  319. package/rules/ast-grep-rules/rule-tests/no-raw-types-test.yml +30 -0
  320. package/rules/ast-grep-rules/rule-tests/no-string-concat-in-loop-test.yml +100 -0
  321. package/rules/ast-grep-rules/rule-tests/no-system-out-println-test.yml +7 -0
  322. package/rules/ast-grep-rules/rule-tests/prefer-string-is-empty-test.yml +7 -0
  323. package/rules/ast-grep-rules/rule-tests/python-optional-type-test.yml +9 -0
  324. package/rules/ast-grep-rules/rule-tests/reducible-list-comprehension-test.yml +11 -0
  325. package/rules/ast-grep-rules/rule-tests/ruby-symbol-to-proc-candidate-test.yml +10 -0
  326. package/rules/ast-grep-rules/rule-tests/rust-no-chars-enumerate-test.yml +7 -0
  327. package/rules/ast-grep-rules/rules/c-no-malloc-free.yml +39 -0
  328. package/rules/ast-grep-rules/rules/cpp-no-malloc-free.yml +45 -0
  329. package/rules/ast-grep-rules/rules/cpp-no-printf.yml +41 -0
  330. package/rules/ast-grep-rules/rules/go-no-fmt-println.yml +43 -0
  331. package/rules/ast-grep-rules/rules/go-no-panic-in-lib.yml +41 -0
  332. package/rules/ast-grep-rules/rules/go-no-underscore-func-name.yml +35 -0
  333. package/rules/ast-grep-rules/rules/go-prefer-errors-is.yml +40 -0
  334. package/rules/ast-grep-rules/rules/go-prefer-string-builder.yml +43 -0
  335. package/rules/ast-grep-rules/rules/hyphenated-svg-attribute.yml +32 -0
  336. package/rules/ast-grep-rules/rules/kotlin-no-lateinit.yml +37 -0
  337. package/rules/ast-grep-rules/rules/kotlin-no-nullable-boolean.yml +33 -0
  338. package/rules/ast-grep-rules/rules/kotlin-no-println.yml +42 -0
  339. package/rules/ast-grep-rules/rules/kotlin-no-unnecessary-let.yml +28 -0
  340. package/rules/ast-grep-rules/rules/kotlin-no-unsafe-cast.yml +28 -0
  341. package/rules/ast-grep-rules/rules/kotlin-no-var.yml +35 -0
  342. package/rules/ast-grep-rules/rules/kotlin-prefer-data-class.yml +66 -0
  343. package/rules/ast-grep-rules/rules/kotlin-prefer-is-empty.yml +35 -0
  344. package/rules/ast-grep-rules/rules/no-important.yml +21 -0
  345. package/rules/ast-grep-rules/rules/no-raw-types.yml +33 -0
  346. package/rules/ast-grep-rules/rules/no-string-concat-in-loop.yml +90 -0
  347. package/rules/ast-grep-rules/rules/no-system-out-println.yml +16 -0
  348. package/rules/ast-grep-rules/rules/prefer-string-is-empty.yml +11 -0
  349. package/rules/ast-grep-rules/rules/python-optional-type.yml +22 -0
  350. package/rules/ast-grep-rules/rules/reducible-list-comprehension.yml +29 -0
  351. package/rules/ast-grep-rules/rules/ruby-symbol-to-proc-candidate.yml +25 -0
  352. package/rules/ast-grep-rules/rules/rust-no-chars-enumerate.yml +19 -0
  353. package/rules/tree-sitter-queries/python/python-cross-language-method.yml +3 -3
  354. package/rules/tree-sitter-queries/python/python-hallucinated-import.yml +2 -2
  355. package/rules/tree-sitter-queries/python/python-sql-injection.yml +12 -2
  356. package/scripts/analyze-pi-lens-logs.mjs +192 -1
  357. package/scripts/install-selftest.mjs +58 -2
  358. package/scripts/lib/skills-predicate.mjs +129 -0
  359. package/scripts/rpc-load-check.mjs +3 -1
@@ -23,39 +23,7 @@ import { isBlocking, reconcileScanDiagnostics, } from "../clients/widget-state.j
23
23
  import { baseName, compactRenderResult } from "./render-compact.js";
24
24
  import { makeProgressReporter, scanningSummaryLine } from "./scan-progress.js";
25
25
  import { isWarmAttached, tryWarmAttachedDiagnostics, } from "../clients/warm-attach.js";
26
- const LANG_EXTENSIONS = {
27
- ".ts": [".ts", ".tsx", ".mts", ".cts"],
28
- ".tsx": [".ts", ".tsx", ".mts", ".cts"],
29
- ".js": [".js", ".jsx", ".mjs", ".cjs"],
30
- ".py": [".py", ".pyi"],
31
- ".rs": [".rs"],
32
- ".go": [".go"],
33
- ".rb": [".rb", ".rake", ".gemspec"],
34
- ".java": [".java"],
35
- ".kt": [".kt", ".kts"],
36
- ".swift": [".swift"],
37
- ".cs": [".cs"],
38
- ".cpp": [".cpp", ".cc", ".cxx", ".hpp", ".hxx"],
39
- ".c": [".c", ".h"],
40
- ".zig": [".zig", ".zon"],
41
- ".hs": [".hs", ".lhs"],
42
- ".ex": [".ex", ".exs"],
43
- ".gleam": [".gleam"],
44
- ".tf": [".tf", ".tfvars"],
45
- ".nix": [".nix"],
46
- ".sh": [".sh", ".bash", ".zsh"],
47
- ".php": [".php"],
48
- ".lua": [".lua"],
49
- ".dart": [".dart"],
50
- ".vue": [".vue"],
51
- ".svelte": [".svelte"],
52
- ".css": [".css", ".scss", ".less"],
53
- ".html": [".html", ".htm"],
54
- ".json": [".json", ".jsonc"],
55
- ".yaml": [".yaml", ".yml"],
56
- ".toml": [".toml"],
57
- ".prisma": [".prisma"],
58
- };
26
+ import { extensionsForLanguage, SCAN_LANGUAGE_PRIORITY, } from "../clients/language-registry.js";
59
27
  const MAX_FILES = 100;
60
28
  const MAX_BATCH_FILES = 100;
61
29
  const MAX_DIAGNOSTICS = 200;
@@ -192,9 +160,10 @@ function projectIgnorePredicate(root) {
192
160
  * This walk was fully synchronous with NO yielding of any kind, bounded only by
193
161
  * `maxFiles` *kept* — an ignored-heavy or cloud-backed (OneDrive/network) tree
194
162
  * could traverse unboundedly many entries, and a single stalled `readdirSync`
195
- * held the loop for the whole stall. It is also called once PER LANGUAGE in
196
- * `runDirectoryDiagnostics`'s `LANG_EXTENSIONS` loop, so a directory-mode
197
- * `lsp_diagnostics` could pay that cost several times over.
163
+ * held the loop for the whole stall. It is also called once PER FAMILY in
164
+ * `runDirectoryDiagnostics`'s `SCAN_LANGUAGE_PRIORITY` loop (#2434, grouped by
165
+ * family since #2458 fix-round F1), so a directory-mode `lsp_diagnostics`
166
+ * could pay that cost several times over.
198
167
  *
199
168
  * The traversal is deliberately still **depth-first with immediate descent**
200
169
  * (not the shared stack-based `walkTreeStackAsync`): the `maxFiles` cap makes
@@ -456,13 +425,17 @@ async function collectDiagnosticsForFile(absPath, lspService, waitMs, serverScop
456
425
  // `touchFile` is the authoritative collection boundary for every scope: it
457
426
  // preserves per-touch timeout, content-binding, and silent-clean confirmation
458
427
  // metadata while returning diagnostics from only the requested clients. The
459
- // legacy openFile/getDiagnostics path remains only for an older/mock service
460
- // without touchFile, or when touchFile cannot resolve any clients.
428
+ // getDiagnostics fallback below remains for the two states a REAL service
429
+ // still reaches: the file read threw (no content to touch with), or the
430
+ // touch resolved no clients and returned `undefined` (clients/lsp/index.ts
431
+ // `touchFile`'s `no_clients`/`destroyed` returns). #2598 deleted the third
432
+ // reason — "an older/mock service without touchFile" — because the real
433
+ // `LSPService` has always defined the method unconditionally, so that arm
434
+ // was reachable only from a partial test double (AGENTS.md shape 7).
461
435
  // #1179: the result is an explicit wrapper so side-channel fields survive
462
436
  // array copies; confirmation was added when Marksman's lower-level clean verdict
463
437
  // proved that a successful empty collection also needs explicit provenance.
464
438
  let touched;
465
- let usedTouch = false;
466
439
  try {
467
440
  content = fs.readFileSync(absPath, "utf-8");
468
441
  if (isWarmAttached()) {
@@ -501,35 +474,25 @@ async function collectDiagnosticsForFile(absPath, lspService, waitMs, serverScop
501
474
  };
502
475
  }
503
476
  }
504
- const serviceWithTouch = lspService;
505
- if (typeof serviceWithTouch.touchFile === "function") {
506
- usedTouch = true;
507
- touched = await serviceWithTouch.touchFile(absPath, content, {
508
- diagnostics: "document",
509
- collectDiagnostics: true,
510
- maxClientWaitMs: waitMs,
511
- source: "lsp_diagnostics",
512
- clientScope: serverScope,
513
- });
514
- timedOut = touched?.inconclusive === true;
515
- }
516
- else {
517
- await lspService.openFile(absPath, content, {
518
- preserveDiagnostics: false,
519
- });
520
- }
477
+ touched = await lspService.touchFile(absPath, content, {
478
+ diagnostics: "document",
479
+ collectDiagnostics: true,
480
+ maxClientWaitMs: waitMs,
481
+ source: "lsp_diagnostics",
482
+ clientScope: serverScope,
483
+ });
484
+ timedOut = touched?.inconclusive === true;
521
485
  }
522
486
  catch {
523
487
  // Non-fatal: getDiagnostics may still have stale/health information.
524
488
  }
525
489
  // Only fall through to the unscoped getDiagnostics() read when the touch
526
- // branch wasn't taken (openFile-only path, which never collected anything
527
- // and genuinely needs the follow-up call) or couldn't resolve any clients
528
- // at all (touched stays undefined despite usedTouch). When touched IS
529
- // defined it's already the answer — reusing it is what makes
530
- // serverScope:"primary" actually skip auxiliary scanners and drops the
531
- // common case back to a single LSP round trip instead of two.
532
- const diagnostics = usedTouch && touched !== undefined
490
+ // produced nothing to read — it threw, the file read threw before it ran, or
491
+ // it resolved no clients at all. When touched IS defined it's already the
492
+ // answer — reusing it is what makes serverScope:"primary" actually skip
493
+ // auxiliary scanners and drops the common case back to a single LSP round
494
+ // trip instead of two.
495
+ const diagnostics = touched !== undefined
533
496
  ? touched.diags
534
497
  : await lspService.getDiagnostics(absPath, waitMs !== undefined ? "document" : "full");
535
498
  // #586: honor each auxiliary profile's native inline-suppression comment
@@ -546,24 +509,25 @@ async function collectDiagnosticsForFile(absPath, lspService, waitMs, serverScop
546
509
  fileRole: detectFileRole(absPath, content),
547
510
  })
548
511
  : diagnostics;
549
- // #1095: surface the touch's content binding (only the touch path carries
550
- // one; the openFile-only / getDiagnostics fallback leaves it undefined →
512
+ // #1095: surface the touch's content binding (only a touch that resolved
513
+ // clients carries one; the getDiagnostics fallback leaves it undefined →
551
514
  // "unknown", no demotion).
552
- const binding = usedTouch ? touched?.binding : undefined;
515
+ const binding = touched?.binding;
553
516
  // #1470: `"partial"` counts here. `confirmedByTouch` feeds
554
517
  // `canTrustTouchConfirmation`, which asks about the PRIMARY's own verdict —
555
518
  // and a partial touch is one whose primary confirmed while an auxiliary was
556
519
  // cut off. Excluding it would render "Primary LSP: unconfirmed" for a primary
557
520
  // that did confirm. The coverage gap is carried separately, below.
558
- const confirmedByTouch = usedTouch && touchCompletedConfirmationPolicy(touched);
521
+ const confirmedByTouch = touchCompletedConfirmationPolicy(touched);
559
522
  return {
560
523
  diagnostics: filtered,
561
524
  timedOut,
525
+ skipReason: touched?.skipReason,
562
526
  confirmedByTouch,
563
- // #1470: only a touch actually contributes a coverage gap; the
564
- // openFile+getDiagnostics fallback never reports one, which is honest —
565
- // that path claims no confirmation at all.
566
- unconfirmedServerIds: usedTouch ? touchCoverageGap(touched) : [],
527
+ // #1470: only a touch that resolved clients contributes a coverage gap;
528
+ // the getDiagnostics fallback never reports one, which is honest — that
529
+ // path claims no confirmation at all.
530
+ unconfirmedServerIds: touchCoverageGap(touched),
567
531
  content,
568
532
  binding,
569
533
  };
@@ -813,7 +777,7 @@ onConfirmedNoBlockers) {
813
777
  };
814
778
  }
815
779
  }
816
- const { diagnostics: rawDiags, timedOut, confirmedByTouch, unconfirmedServerIds, content: collectedContent, binding, } = await collectDiagnosticsForFile(file, lspService, waitMs, serverScope);
780
+ const { diagnostics: rawDiags, timedOut, confirmedByTouch, unconfirmedServerIds, content: collectedContent, binding, skipReason, } = await collectDiagnosticsForFile(file, lspService, waitMs, serverScope);
817
781
  const health = lspService.getDiagnosticsHealth?.(file);
818
782
  // #570: a timed-out priming check is never a confirmed "clean" — treat it
819
783
  // as unconfirmed without consulting the (unrelated) silent-tier
@@ -824,7 +788,10 @@ onConfirmedNoBlockers) {
824
788
  // be merged in rather than discarded.
825
789
  let effectiveRawDiags = rawDiags;
826
790
  let confirmation;
827
- if (timedOut) {
791
+ if (skipReason !== undefined) {
792
+ confirmation = "unconfirmed";
793
+ }
794
+ else if (timedOut) {
828
795
  if (applySeverityFilter(rawDiags, severity).length === 0) {
829
796
  confirmation = "unconfirmed";
830
797
  }
@@ -887,7 +854,7 @@ onConfirmedNoBlockers) {
887
854
  unconfirmedServerIds.length === 0) {
888
855
  cacheCtx.record(file, scopeKey, effectiveRawDiags, stat.mtimeMs, collectedContent !== undefined
889
856
  ? hashDiagnosticContent(collectedContent)
890
- : undefined);
857
+ : undefined, stat.size);
891
858
  }
892
859
  return {
893
860
  file,
@@ -895,6 +862,7 @@ onConfirmedNoBlockers) {
895
862
  unavailable: lspUnavailableMessage(file, health),
896
863
  confirmation,
897
864
  timedOut: confirmation === "unconfirmed" ? timedOut : undefined,
865
+ ...(skipReason !== undefined && { skipReason }),
898
866
  primaryServerId: primaryServerId(file),
899
867
  };
900
868
  }
@@ -905,7 +873,7 @@ onConfirmedNoBlockers) {
905
873
  // Reserve the token before awaiting this file's LSP result. The direct-file
906
874
  // path performs its own confirmation/reconciliation below (#1198).
907
875
  const writeIndex = nextWriteIndex?.();
908
- const { diagnostics: rawDiags, timedOut, confirmedByTouch, unconfirmedServerIds, content: collectedContent, binding, } = await collectDiagnosticsForFile(absPath, lspService, waitMs, serverScope);
876
+ const { diagnostics: rawDiags, timedOut, confirmedByTouch, unconfirmedServerIds, content: collectedContent, binding, skipReason, } = await collectDiagnosticsForFile(absPath, lspService, waitMs, serverScope);
909
877
  const lspHealth = lspService.getDiagnosticsHealth?.(absPath);
910
878
  const unavailable = lspUnavailableMessage(absPath, lspHealth);
911
879
  // #533: an empty result needs a confirmed/unconfirmed verdict — a push-only,
@@ -919,7 +887,10 @@ onConfirmedNoBlockers) {
919
887
  // diagnostics it surfaces are merged in, not discarded.
920
888
  let effectiveRawDiags = rawDiags;
921
889
  let confirmation;
922
- if (timedOut) {
890
+ if (skipReason !== undefined) {
891
+ confirmation = "unconfirmed";
892
+ }
893
+ else if (timedOut) {
923
894
  if (applySeverityFilter(rawDiags, severity).length === 0) {
924
895
  confirmation = "unconfirmed";
925
896
  }
@@ -1060,6 +1031,7 @@ onConfirmedNoBlockers) {
1060
1031
  truncated,
1061
1032
  unconfirmed,
1062
1033
  timedOut: unconfirmed ? timedOut : undefined,
1034
+ ...(skipReason !== undefined && { skipReason }),
1063
1035
  // #1470: which servers this result does NOT speak for. Absent when it
1064
1036
  // speaks for all of them.
1065
1037
  ...(unconfirmedServerIds.length > 0 && {
@@ -1341,18 +1313,42 @@ async function runBatchFileDiagnostics(absPaths, severity, lspService, options)
1341
1313
  },
1342
1314
  };
1343
1315
  }
1316
+ /**
1317
+ * The scan-language DECISION `runDirectoryDiagnostics` makes, pulled out as
1318
+ * its own function so `tests/tools/lsp-diagnostics-scan-family.test.ts` can
1319
+ * pin it against the golden `LANG_EXTENSIONS` table for every 1- and
1320
+ * 2-extension combination in that table's universe without spinning up one
1321
+ * real directory (and the full LSP-mocked tool) per combination — `hasMatch`
1322
+ * is the only I/O-shaped seam, so the caller decides whether it is backed by
1323
+ * a real `collectFiles` walk (production) or an in-memory extension-set probe
1324
+ * (tests).
1325
+ *
1326
+ * Walks `SCAN_LANGUAGE_PRIORITY`'s FAMILIES in order and unions each member
1327
+ * id's {@link extensionsForLanguage} into ONE `hasMatch` call — the first
1328
+ * family with any match wins the whole directory for this pass. Grouping by
1329
+ * family (not trying each id alone) is what makes a directory mixing BOTH
1330
+ * extensions of a registry-split pair (`.ts`+`.tsx`, `.css`+`.scss`, ...)
1331
+ * behavior-preserving vs the pre-#2434 bundled-key table: a flat per-id loop
1332
+ * stopped at the first id with ANY match, silently losing the sibling half of
1333
+ * a split pair present in the same directory (#2458 fix-round F1).
1334
+ */
1335
+ export async function resolveDirectoryScanExtensions(hasMatch) {
1336
+ for (const family of SCAN_LANGUAGE_PRIORITY) {
1337
+ const exts = family.flatMap((languageId) => extensionsForLanguage(languageId));
1338
+ if (await hasMatch(exts)) {
1339
+ return exts;
1340
+ }
1341
+ }
1342
+ return undefined;
1343
+ }
1344
1344
  async function runDirectoryDiagnostics(absPath, severity, lspService, options) {
1345
- let extension;
1346
1345
  let collectedFiles = [];
1347
1346
  const isIgnored = projectIgnorePredicate(absPath);
1348
- for (const [ext, exts] of Object.entries(LANG_EXTENSIONS)) {
1349
- collectedFiles = await collectFiles(absPath, exts, MAX_FILES + 1, isIgnored);
1350
- if (collectedFiles.length > 0) {
1351
- extension = ext;
1352
- break;
1353
- }
1354
- }
1355
- if (!extension || collectedFiles.length === 0) {
1347
+ await resolveDirectoryScanExtensions(async (exts) => {
1348
+ collectedFiles = await collectFiles(absPath, [...exts], MAX_FILES + 1, isIgnored);
1349
+ return collectedFiles.length > 0;
1350
+ });
1351
+ if (collectedFiles.length === 0) {
1356
1352
  return {
1357
1353
  content: [
1358
1354
  {
@@ -10,6 +10,8 @@ import { Type } from "../clients/deps/typebox.js";
10
10
  import { logLatency } from "../clients/latency-logger.js";
11
11
  import { newLspMutationCorrelationId, recordLspMutationOutcome, } from "../clients/lsp-mutation.js";
12
12
  import { uriToPath } from "../clients/path-utils.js";
13
+ import { escapeRegExp } from "../clients/string-utils.js";
14
+ import { isRecordableProjectPath } from "../clients/file-utils.js";
13
15
  import { compactRenderResult } from "./render-compact.js";
14
16
  import { applyWorkspaceEdit, summarizeWorkspaceEdit, } from "../clients/lsp/edits.js";
15
17
  import { getLSPService, } from "../clients/lsp/index.js";
@@ -106,9 +108,6 @@ function emptyReasonForOperation(operation) {
106
108
  return "no-call-hierarchy-results";
107
109
  return "no-results";
108
110
  }
109
- function escapeRegExp(value) {
110
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
111
- }
112
111
  function parseSymbolSelector(symbol) {
113
112
  const trimmed = symbol.trim();
114
113
  const match = /^([^#]*)(?:#(-?\d+))?$/.exec(trimmed);
@@ -467,6 +466,8 @@ function formatCapabilities(snapshots, filePath) {
467
466
  ["hover", (s) => !!s.operationSupport.hover],
468
467
  ["rename", (s) => !!s.operationSupport.rename],
469
468
  ["codeAction", (s) => !!s.operationSupport.codeAction],
469
+ ["codeAction/resolve", (s) => !!s.operationSupport.codeActionResolve],
470
+ ["workspace/willRenameFiles", (s) => !!s.operationSupport.willRenameFiles],
470
471
  ["workspaceSymbol", (s) => !!s.operationSupport.workspaceSymbol],
471
472
  ["implementation", (s) => !!s.operationSupport.implementation],
472
473
  ["signatureHelp", (s) => !!s.operationSupport.signatureHelp],
@@ -480,7 +481,7 @@ function formatCapabilities(snapshots, filePath) {
480
481
  [
481
482
  "rename_file",
482
483
  () => true,
483
- "willRenameFiles/didRenameFiles helper available",
484
+ "filesystem rename; preflight support shown above",
484
485
  ],
485
486
  ];
486
487
  const lines = [];
@@ -528,16 +529,16 @@ async function openFileBestEffort(lspService, filePath, waitForDiagnostics = fal
528
529
  if (!fileContent)
529
530
  return;
530
531
  try {
531
- if (typeof lspService.touchFile === "function") {
532
- await lspService.touchFile(filePath, fileContent, {
533
- diagnostics: waitForDiagnostics ? "document" : "none",
534
- source: "lsp_navigation",
535
- clientScope: waitForDiagnostics ? "all" : "primary",
536
- });
537
- }
538
- else {
539
- await lspService.openFile(filePath, fileContent);
540
- }
532
+ // #2598: `touchFile` is defined unconditionally on the real `LSPService`
533
+ // (clients/lsp/index.ts), so the former `typeof … === "function"` hedge
534
+ // and its `openFile` arm were reachable only from a partial test double
535
+ // (AGENTS.md shape 7). Nothing here reads the result — a touch that
536
+ // resolves no clients is already the no-op this helper wants.
537
+ await lspService.touchFile(filePath, fileContent, {
538
+ diagnostics: waitForDiagnostics ? "document" : "none",
539
+ source: "lsp_navigation",
540
+ clientScope: waitForDiagnostics ? "all" : "primary",
541
+ });
541
542
  }
542
543
  catch {
543
544
  /* LSP server may not be ready yet — proceed anyway */
@@ -727,7 +728,7 @@ getFlag, mutationDeps) {
727
728
  },
728
729
  ],
729
730
  details: {
730
- ...(payload.details ?? {}),
731
+ ...payload.details,
731
732
  failureKind: meta.failureKind,
732
733
  },
733
734
  };
@@ -777,15 +778,37 @@ getFlag, mutationDeps) {
777
778
  if (requestedApply &&
778
779
  ["rename", "rename_file", "executeCommand"].includes(operation)) {
779
780
  const cwd = ctx.cwd || ".";
781
+ // Name the specific LSP operation in the receipt rather than a
782
+ // generic "lsp-edit" tag, so a rename's change-log entry reads
783
+ // differently from an executeCommand-solicited edit (#2450).
784
+ const mutationSource = operation === "executeCommand" ? "lsp-execute-command" : "lsp-rename";
780
785
  mutationContext = {
781
786
  cwd,
782
787
  correlationId: newLspMutationCorrelationId(_toolCallId),
783
- tool: "lsp_navigation",
784
- source: "lsp-edit",
788
+ tool: `lsp_navigation:${operation}`,
789
+ source: mutationSource,
785
790
  ...mutationDeps,
786
791
  readGuard: getFlag("no-read-guard", cwd)
787
792
  ? undefined
788
793
  : mutationDeps?.readGuard,
794
+ // #2450 review round 2 (F4)/round 3 (F2, F4): the SAME gate
795
+ // `registerMutationBridge` applies internally in `index.ts`, so
796
+ // this directly-threaded path and the bridge fallback
797
+ // (`clients/lsp-mutation.ts`, reached when `mutationDeps` is
798
+ // absent/partial — e.g. the MCP server) agree on which files
799
+ // count as project source, rather than the direct path
800
+ // recording an ignored/vendor write the fallback would drop.
801
+ // `no-read-guard` is intentionally NOT checked here (round 3
802
+ // F2): it gates only the read-guard stamp above, the same
803
+ // canonical split `clients/runtime-tool-result.ts` applies
804
+ // (`:1120`, `:1485`) — bookkeeping (turn-state / receipts)
805
+ // still runs under `--no-read-guard`. Judge against the
806
+ // project root, not the request `cwd` (round 3 F4): a
807
+ // sub-package `cwd` must not read a sibling-package rename as
808
+ // external.
809
+ isRecordable: (filePath) => {
810
+ return isRecordableProjectPath(filePath, mutationDeps?.runtime?.projectRoot ?? cwd);
811
+ },
789
812
  };
790
813
  }
791
814
  const isCallHierarchyTraversal = operation === "incomingCalls" || operation === "outgoingCalls";
@@ -22,7 +22,6 @@ export const LANGUAGES = [
22
22
  "ruby",
23
23
  "rust",
24
24
  "scala",
25
- "solidity",
26
25
  "swift",
27
26
  "tsx",
28
27
  "typescript",
@@ -45,10 +45,20 @@ On every write/edit, and at session/turn boundaries, pi-lens runs — without yo
45
45
  | **Structural rules** | ast-grep (NAPI engine) + tree-sitter rules flag correctness/security smells. |
46
46
  | **Opengrep security scan** | Always-on: per-edit via an auxiliary LSP, plus a cached project-wide CLI scan for `mode=full`. |
47
47
  | **Other scanners** | Config-/presence-gated: gitleaks (secrets), trivy (CVEs/IaC/license), govulncheck (Go), knip/jscpd/madge (JS/TS dead-code/dupes/cycles), vulture (Python), zizmor (GH Actions), typos. |
48
- | **Test-runner-on-write** | Related/affected tests are run asynchronously for the next turn's findings (edit-scoped, not a full-suite run). |
48
+ | **Test-runner-on-write** | Related/affected tests are run asynchronously for the next turn's findings (edit-scoped, not a full-suite run). Integration/e2e tests are never auto-fired — see below. |
49
49
  | **Read-guard** | Tracks that you read a file before editing it; blocks/warns zero-read or stale-range edits. |
50
50
  | **Context injection** | Injects session-start guidance and turn-end findings into your context (see §2). |
51
51
 
52
+ **Test-runner-on-write scope.** A built-in exclusion list keeps integration/e2e
53
+ suites out of the auto-fired batch — `**/integration/**`, `**/e2e/**`,
54
+ `**/*.integration.*`, `**/*.e2e.*` — since those commonly spawn external
55
+ processes (another CLI, a browser, a live provider) that don't belong on an
56
+ unattended per-edit turn. This is a fixed, built-in list; there is no
57
+ per-project config knob for it. If a runner itself fails to complete (a
58
+ timeout, or a missing provider/binary — not a failing assertion), pi-lens
59
+ delivers it as an advisory ("could not complete last turn"), not as a
60
+ "fix before continuing" blocker — you didn't introduce anything to fix.
61
+
52
62
  You don't invoke these. They happen. Your job is to **read the results** and **respond**.
53
63
 
54
64
  ---
@@ -143,8 +153,10 @@ Edits fail or warn when:
143
153
 
144
154
  A blocked edit returns a retryable message, e.g.:
145
155
 
146
- > `🔄 RETRYABLE — Edit without read … Read the file first, then retry the edit:
147
- > read path="<file>"`
156
+ ```text
157
+ 🔄 RETRYABLE — Edit without read: you have not read `<file>` in this
158
+ conversation. Read it first, then retry: `read path="<file>"`.
159
+ ```
148
160
 
149
161
  **Correct behavior:** read the file (or the relevant range/symbol) *before* editing.
150
162
  Helpful mechanics you can rely on:
@@ -183,9 +195,62 @@ pi-lens writes to files **outside your own tool calls** (`docs/features.md`
183
195
  - **`write` then `edit` on the same file, same turn:** the write's autofix
184
196
  demotes to deferred too, so the file's mutation history stays coherent. This
185
197
  resets at the next turn.
198
+ - **Any other tool that edits a file:** pi-lens recognizes it by the SHAPE of
199
+ its arguments rather than its name (`clients/mutating-tool.ts`), so a host or
200
+ extension tool called `replace` or `insert` gets the same chain `edit` gets:
201
+ a turn-state entry, a change-log receipt attributed to that tool, and a
202
+ *deferred* auto-fix. Its lines are resolved when the anchor is unambiguous
203
+ (roughly two-thirds of anchors in practice — `clients/hashline-anchor.ts`);
204
+ otherwise the mutation is recorded whole-file with lines unknown, and the
205
+ read-before-edit guard takes its no-line-info arm rather than guessing.
206
+ Deferred is the default for every edit-shaped tool pi-lens cannot place,
207
+ because formatting between the steps of a multi-call rewrite fights the tool
208
+ that is still writing.
209
+ - **A tool whose shape is unrecognized too:** pi-lens watches instead of
210
+ guessing (`clients/observed-mutation.ts`). A call that names a file gets a
211
+ bounded pre/post snapshot of THAT PATH — the file itself, or a directory's own
212
+ entries — and nothing else, so a write landing on a neighbouring file is never
213
+ attributed to it. Anything that changed is replayed through the same chain,
214
+ and the tool is then remembered as mutating: for this session on the first
215
+ sighting, persisted under the project's data directory on the second, so a
216
+ later session classifies it by name with no snapshot at all. Three quiet
217
+ observations in a row withdraw a session attribution again, and a withdrawn
218
+ tool can be learned back from a later real edit. An observation pi-lens could
219
+ not finish — a directory with more entries than it watches — counts as
220
+ neither, so a wide codemod is never written off on a look it never took. A
221
+ call that names
222
+ no file is caught at `agent_settled` by an incremental content check over the
223
+ files pi-lens has already read, written, diagnosed or opened on a language
224
+ server — a rotating window per turn, reading only what actually moved; a file
225
+ it has never seen has no baseline, so that last-resort net does not cover it,
226
+ and a file it cannot verify is reported as such rather than reformatted on a
227
+ timestamp alone.
186
228
  - The conservative actionable-warnings autofix (LSP quickfixes, hard-capped)
187
229
  is unchanged: it always runs at `agent_end`.
188
230
 
231
+ **Extension authors: record your own writes.** If your extension writes files
232
+ outside pi-lens's tool events — its own registered tool, a spawned rewriter —
233
+ tell pi-lens through the in-process mutation bridge, the write-side sibling of
234
+ the read bridge:
235
+
236
+ ```js
237
+ const bridge = globalThis[Symbol.for("pi-lens:mutation-bridge")];
238
+ if (bridge?.version === 1) {
239
+ bridge.recordMutation({
240
+ filePath, // absolute path
241
+ kind: "edit", // "write" replaces the whole file; "edit" is partial
242
+ editRanges: [[12, 18]], // optional, 1-based inclusive
243
+ consumer: "my-extension",
244
+ });
245
+ }
246
+ ```
247
+
248
+ Check `version` before calling; a version you do not recognize is unsupported.
249
+ Calling when pi-lens is absent or the guard is disabled is safe — the bridge is
250
+ missing or drops the call. `recordMutation` returns `true` when pi-lens took the
251
+ record and `false` when it dropped it, so you can count your own drops. pi-lens's
252
+ own `ast_grep_replace apply:true` records through this same bridge.
253
+
189
254
  Consequences for you:
190
255
 
191
256
  - Your exact written bytes may be reformatted/fixed. **This is expected pipeline