@herbertgao/pi-extensions 2026.8.4 → 2026.8.5

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 (288) hide show
  1. package/README.md +4 -4
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +10 -2
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +10 -2
  4. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +24 -0
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +91 -1
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +7 -3
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +7 -6
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +21 -12
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +4 -5
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +5 -8
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +22 -12
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +2 -4
  13. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  14. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
  15. package/node_modules/@herbertgao/pi-subagents/package.json +8 -4
  16. package/node_modules/@herbertgao/pi-subagents/src/agent-color.ts +72 -67
  17. package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +255 -0
  18. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +191 -3
  19. package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +15 -2
  20. package/node_modules/@herbertgao/pi-subagents/src/index.ts +289 -113
  21. package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +24 -1
  22. package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +24 -2
  23. package/node_modules/@herbertgao/pi-subagents/src/ui/schedule-menu.ts +9 -8
  24. package/node_modules/@herbertgao/pi-subagents/src/ui/select-item.ts +48 -0
  25. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +7 -0
  26. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  27. package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +7 -0
  28. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +16 -5
  29. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +4 -2
  30. package/node_modules/@narumitw/pi-btw/README.md +8 -7
  31. package/node_modules/@narumitw/pi-btw/package.json +2 -2
  32. package/node_modules/@narumitw/pi-btw/src/menu.ts +1 -0
  33. package/node_modules/pi-lens/CHANGELOG.md +2440 -954
  34. package/node_modules/pi-lens/README.md +13 -0
  35. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +73 -18
  36. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +21 -4
  37. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +68 -16
  38. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -1
  39. package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +37 -10
  40. package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +63 -0
  41. package/node_modules/pi-lens/dist/clients/atomic-write.js +114 -14
  42. package/node_modules/pi-lens/dist/clients/bash-file-access.js +193 -23
  43. package/node_modules/pi-lens/dist/clients/biome-client.js +29 -25
  44. package/node_modules/pi-lens/dist/clients/bootstrap.js +11 -2
  45. package/node_modules/pi-lens/dist/clients/bounded-cache.js +34 -0
  46. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +192 -0
  47. package/node_modules/pi-lens/dist/clients/bus-publish.js +10 -3
  48. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +47 -5
  49. package/node_modules/pi-lens/dist/clients/cache-manager.js +97 -9
  50. package/node_modules/pi-lens/dist/clients/cache-observability.js +301 -14
  51. package/node_modules/pi-lens/dist/clients/call-graph.js +449 -117
  52. package/node_modules/pi-lens/dist/clients/child-unref.js +85 -0
  53. package/node_modules/pi-lens/dist/clients/codebase-model.js +52 -23
  54. package/node_modules/pi-lens/dist/clients/collateral-test-role.js +40 -0
  55. package/node_modules/pi-lens/dist/clients/complexity-client.js +2 -1
  56. package/node_modules/pi-lens/dist/clients/console-guard-install.js +14 -0
  57. package/node_modules/pi-lens/dist/clients/cooperative-budget.js +42 -0
  58. package/node_modules/pi-lens/dist/clients/dead-code-client.js +2 -1
  59. package/node_modules/pi-lens/dist/clients/debug-handles.js +206 -0
  60. package/node_modules/pi-lens/dist/clients/debug-heap.js +167 -0
  61. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +153 -0
  62. package/node_modules/pi-lens/dist/clients/dependency-checker.js +347 -75
  63. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +61 -11
  64. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +10 -3
  65. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +63 -20
  66. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +23 -7
  67. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +8 -18
  68. package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +8 -5
  69. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +35 -0
  70. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +17 -8
  71. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +1085 -681
  72. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +14 -0
  73. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +11 -1
  74. package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +50 -0
  75. package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +135 -0
  76. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +23 -5
  77. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +5 -9
  78. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +3 -2
  79. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
  80. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +154 -0
  81. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +3 -2
  82. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +5 -1
  83. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +14 -6
  84. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -9
  85. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +9 -5
  86. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +4 -8
  87. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -3
  88. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -3
  89. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
  90. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
  91. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +163 -0
  92. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +37 -5
  93. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +3 -1
  94. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +13 -7
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +277 -26
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
  99. package/node_modules/pi-lens/dist/clients/disposition-publish.js +10 -3
  100. package/node_modules/pi-lens/dist/clients/durable-store.js +97 -0
  101. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +94 -3
  102. package/node_modules/pi-lens/dist/clients/extension-log.js +139 -0
  103. package/node_modules/pi-lens/dist/clients/extension-mode.js +73 -0
  104. package/node_modules/pi-lens/dist/clients/file-kinds.js +57 -0
  105. package/node_modules/pi-lens/dist/clients/file-role.js +18 -3
  106. package/node_modules/pi-lens/dist/clients/file-utils.js +56 -34
  107. package/node_modules/pi-lens/dist/clients/format-events-publish.js +16 -3
  108. package/node_modules/pi-lens/dist/clients/format-service.js +24 -8
  109. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +8 -0
  110. package/node_modules/pi-lens/dist/clients/formatters.js +358 -61
  111. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +148 -15
  112. package/node_modules/pi-lens/dist/clients/git-guard.js +681 -11
  113. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +4 -0
  114. package/node_modules/pi-lens/dist/clients/go-client.js +2 -1
  115. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +8 -0
  116. package/node_modules/pi-lens/dist/clients/grammar-source.js +12 -4
  117. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +21 -3
  118. package/node_modules/pi-lens/dist/clients/host-ports.js +31 -0
  119. package/node_modules/pi-lens/dist/clients/installer/index.js +390 -99
  120. package/node_modules/pi-lens/dist/clients/instance-reaper.js +142 -172
  121. package/node_modules/pi-lens/dist/clients/jscpd-client.js +31 -54
  122. package/node_modules/pi-lens/dist/clients/knip-client.js +15 -36
  123. package/node_modules/pi-lens/dist/clients/language-policy.js +14 -2
  124. package/node_modules/pi-lens/dist/clients/language-profile.js +13 -7
  125. package/node_modules/pi-lens/dist/clients/latency-logger.js +23 -1
  126. package/node_modules/pi-lens/dist/clients/lens-config.js +12 -1
  127. package/node_modules/pi-lens/dist/clients/lens-engine.js +86 -17
  128. package/node_modules/pi-lens/dist/clients/lens-events.js +27 -4
  129. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
  130. package/node_modules/pi-lens/dist/clients/lens-map.js +94 -19
  131. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +30 -0
  132. package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -1
  133. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +1 -1
  134. package/node_modules/pi-lens/dist/clients/lsp/client.js +493 -48
  135. package/node_modules/pi-lens/dist/clients/lsp/config.js +13 -2
  136. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +86 -0
  137. package/node_modules/pi-lens/dist/clients/lsp/edits.js +1012 -121
  138. package/node_modules/pi-lens/dist/clients/lsp/index.js +1807 -780
  139. package/node_modules/pi-lens/dist/clients/lsp/launch.js +32 -26
  140. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
  141. package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +83 -0
  142. package/node_modules/pi-lens/dist/clients/lsp/server.js +196 -32
  143. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
  144. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +8 -0
  145. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +248 -0
  146. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +123 -32
  147. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +157 -24
  148. package/node_modules/pi-lens/dist/clients/mcp/session.js +216 -8
  149. package/node_modules/pi-lens/dist/clients/memory-sampler.js +115 -0
  150. package/node_modules/pi-lens/dist/clients/metrics-client.js +2 -1
  151. package/node_modules/pi-lens/dist/clients/metrics-history.js +2 -1
  152. package/node_modules/pi-lens/dist/clients/module-report-lsp.js +5 -0
  153. package/node_modules/pi-lens/dist/clients/module-report.js +168 -3
  154. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +319 -118
  155. package/node_modules/pi-lens/dist/clients/opengrep-client.js +1 -1
  156. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +120 -5
  157. package/node_modules/pi-lens/dist/clients/path-utils.js +110 -9
  158. package/node_modules/pi-lens/dist/clients/pipeline.js +47 -13
  159. package/node_modules/pi-lens/dist/clients/print-mode.js +21 -0
  160. package/node_modules/pi-lens/dist/clients/project-changes.js +40 -0
  161. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +3 -2
  162. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +8 -0
  163. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +21 -1
  164. package/node_modules/pi-lens/dist/clients/project-lens-config.js +86 -9
  165. package/node_modules/pi-lens/dist/clients/project-report.js +27 -13
  166. package/node_modules/pi-lens/dist/clients/project-snapshot.js +175 -20
  167. package/node_modules/pi-lens/dist/clients/project-trust.js +181 -0
  168. package/node_modules/pi-lens/dist/clients/quiet-window.js +18 -4
  169. package/node_modules/pi-lens/dist/clients/read-bridge.js +147 -0
  170. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +159 -1
  171. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +150 -29
  172. package/node_modules/pi-lens/dist/clients/read-guard.js +185 -4
  173. package/node_modules/pi-lens/dist/clients/resource-sampler.js +70 -84
  174. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +1122 -221
  175. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +2 -1
  176. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +21 -10
  177. package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +46 -0
  178. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +57 -6
  179. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +51 -3
  180. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +30 -1
  181. package/node_modules/pi-lens/dist/clients/ruff-client.js +27 -44
  182. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +107 -73
  183. package/node_modules/pi-lens/dist/clients/runtime-context.js +36 -12
  184. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +74 -6
  185. package/node_modules/pi-lens/dist/clients/runtime-session.js +405 -117
  186. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +71 -48
  187. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +218 -23
  188. package/node_modules/pi-lens/dist/clients/runtime-turn.js +237 -64
  189. package/node_modules/pi-lens/dist/clients/rust-client.js +2 -1
  190. package/node_modules/pi-lens/dist/clients/safe-spawn.js +777 -62
  191. package/node_modules/pi-lens/dist/clients/security-scan-client.js +2 -1
  192. package/node_modules/pi-lens/dist/clients/session-state-store.js +3 -2
  193. package/node_modules/pi-lens/dist/clients/sg-runner.js +284 -174
  194. package/node_modules/pi-lens/dist/clients/slow-fs.js +2 -1
  195. package/node_modules/pi-lens/dist/clients/smells-rollup.js +224 -0
  196. package/node_modules/pi-lens/dist/clients/source-filter.js +184 -20
  197. package/node_modules/pi-lens/dist/clients/source-walker.js +66 -10
  198. package/node_modules/pi-lens/dist/clients/startup-scan.js +15 -16
  199. package/node_modules/pi-lens/dist/clients/startup-timing.js +1 -1
  200. package/node_modules/pi-lens/dist/clients/test-runner-client.js +38 -8
  201. package/node_modules/pi-lens/dist/clients/tool-event.js +35 -0
  202. package/node_modules/pi-lens/dist/clients/tool-policy.js +388 -39
  203. package/node_modules/pi-lens/dist/clients/tool-render.js +155 -0
  204. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +6 -1
  205. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +419 -50
  206. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +21 -0
  207. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +7 -1
  208. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +7 -2
  209. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +44 -7
  210. package/node_modules/pi-lens/dist/clients/user-notify.js +51 -0
  211. package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +57 -0
  212. package/node_modules/pi-lens/dist/clients/warm-attach.js +33 -13
  213. package/node_modules/pi-lens/dist/clients/widget-state.js +300 -43
  214. package/node_modules/pi-lens/dist/clients/word-index.js +391 -64
  215. package/node_modules/pi-lens/dist/clients/workspace-topology.js +89 -6
  216. package/node_modules/pi-lens/dist/index.js +49361 -36888
  217. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +182 -29
  218. package/node_modules/pi-lens/dist/mcp/server.js +211 -44
  219. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +93 -27
  220. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +200 -36
  221. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +153 -40
  222. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -3
  223. package/node_modules/pi-lens/dist/tools/module-report.js +9 -0
  224. package/node_modules/pi-lens/docs/agent-guide.md +2 -2
  225. package/node_modules/pi-lens/docs/agent-tools.md +21 -10
  226. package/node_modules/pi-lens/docs/analysisall.md +192 -0
  227. package/node_modules/pi-lens/docs/api-ports-inventory.md +165 -0
  228. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +5 -2
  229. package/node_modules/pi-lens/docs/durable-store-audit-1202.md +32 -0
  230. package/node_modules/pi-lens/docs/environment-variables.md +44 -0
  231. package/node_modules/pi-lens/docs/fable.md +27 -0
  232. package/node_modules/pi-lens/docs/features.md +31 -2
  233. package/node_modules/pi-lens/docs/globalconfig.md +45 -1
  234. package/node_modules/pi-lens/docs/language-coverage.md +4 -3
  235. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +2 -2
  236. package/node_modules/pi-lens/docs/mcp.md +37 -4
  237. package/node_modules/pi-lens/docs/servercapabilities.md +3 -3
  238. package/node_modules/pi-lens/docs/settings.md +12 -1
  239. package/node_modules/pi-lens/docs/tools.md +4 -1
  240. package/node_modules/pi-lens/docs/usage.md +18 -0
  241. package/node_modules/pi-lens/package.json +10 -10
  242. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
  243. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
  244. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
  245. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
  246. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
  247. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
  248. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
  249. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
  250. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
  251. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
  252. package/node_modules/pi-lens/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
  253. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +2 -2
  254. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +55 -2
  255. package/node_modules/pi-mcp-adapter/CHANGELOG.md +25 -0
  256. package/node_modules/pi-mcp-adapter/README.md +34 -3
  257. package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +1 -1
  258. package/node_modules/pi-mcp-adapter/commands.ts +15 -7
  259. package/node_modules/pi-mcp-adapter/direct-tools.ts +54 -7
  260. package/node_modules/pi-mcp-adapter/host-html-template.ts +0 -8
  261. package/node_modules/pi-mcp-adapter/index.ts +8 -3
  262. package/node_modules/pi-mcp-adapter/init.ts +29 -5
  263. package/node_modules/pi-mcp-adapter/lifecycle.ts +0 -4
  264. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
  265. package/node_modules/pi-mcp-adapter/mcp-auth.ts +60 -2
  266. package/node_modules/pi-mcp-adapter/mcp-panel.ts +45 -9
  267. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +22 -20
  268. package/node_modules/pi-mcp-adapter/metadata-cache.ts +27 -4
  269. package/node_modules/pi-mcp-adapter/npx-resolver.ts +81 -26
  270. package/node_modules/pi-mcp-adapter/package.json +3 -3
  271. package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -26
  272. package/node_modules/pi-mcp-adapter/search-ranking.ts +86 -7
  273. package/node_modules/pi-mcp-adapter/server-manager.ts +36 -7
  274. package/node_modules/pi-mcp-adapter/session-recovery.ts +8 -2
  275. package/node_modules/pi-mcp-adapter/tool-approval.ts +48 -7
  276. package/node_modules/pi-mcp-adapter/tool-metadata.ts +42 -4
  277. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +138 -12
  278. package/node_modules/pi-mcp-adapter/types.ts +93 -27
  279. package/node_modules/pi-mcp-adapter/ui-server.ts +20 -1
  280. package/package.json +11 -12
  281. package/node_modules/pi-lens/dist/clients/ast-grep-parser.js +0 -86
  282. package/node_modules/pi-lens/dist/clients/lsp/interactive-install.js +0 -367
  283. package/node_modules/pi-lens/dist/clients/lsp/lsp-index.js +0 -10
  284. package/node_modules/pi-lens/dist/clients/lsp/server-strategies.js +0 -5
  285. package/node_modules/pi-lens/dist/clients/project-metadata.js +0 -690
  286. package/node_modules/pi-lens/dist/clients/source-groups.js +0 -96
  287. package/node_modules/pi-lens/dist/clients/startup-marker.js +0 -6
  288. package/node_modules/pi-lens/dist/clients/types.js +0 -11
@@ -14,6 +14,7 @@ import { primaryServerId } from "../clients/lsp/config.js";
14
14
  import { combineAbortSignals, withDeadline } from "../clients/deadline-utils.js";
15
15
  import { applyAuxiliarySuppressions, retagAuxiliaryDiagnostics, } from "../clients/dispatch/auxiliary-lsp.js";
16
16
  import { detectFileRole } from "../clients/file-role.js";
17
+ import { hashDiagnosticContent, } from "../clients/lsp/diagnostic-binding.js";
17
18
  import { classifyCascadeWaitTier } from "../clients/lsp/wait-policy/index.js";
18
19
  import { attemptTsserverSyncDiagnostics, } from "../clients/lsp/tsserver-sync.js";
19
20
  import { convertLspDiagnostics } from "../clients/dispatch/utils/lsp-diagnostics.js";
@@ -184,14 +185,30 @@ function projectIgnorePredicate(root) {
184
185
  return () => false;
185
186
  }
186
187
  }
187
- function collectFiles(dir, extensions, maxFiles, isIgnored = () => false) {
188
+ /**
189
+ * #1137: async so a directory read never blocks the event loop (and pi's TUI).
190
+ *
191
+ * This walk was fully synchronous with NO yielding of any kind, bounded only by
192
+ * `maxFiles` *kept* — an ignored-heavy or cloud-backed (OneDrive/network) tree
193
+ * could traverse unboundedly many entries, and a single stalled `readdirSync`
194
+ * held the loop for the whole stall. It is also called once PER LANGUAGE in
195
+ * `runDirectoryDiagnostics`'s `LANG_EXTENSIONS` loop, so a directory-mode
196
+ * `lsp_diagnostics` could pay that cost several times over.
197
+ *
198
+ * The traversal is deliberately still **depth-first with immediate descent**
199
+ * (not the shared stack-based `walkTreeStackAsync`): the `maxFiles` cap makes
200
+ * traversal ORDER observable — a stack walk would keep a different subset of
201
+ * files on an over-large tree. Only scheduling changes here; the returned list
202
+ * is identical. Awaiting each directory read is itself the yield point.
203
+ */
204
+ async function collectFiles(dir, extensions, maxFiles, isIgnored = () => false) {
188
205
  const files = [];
189
- function walk(current) {
206
+ async function walk(current) {
190
207
  if (files.length >= maxFiles)
191
208
  return;
192
209
  let entries;
193
210
  try {
194
- entries = fs.readdirSync(current, { withFileTypes: true });
211
+ entries = await fs.promises.readdir(current, { withFileTypes: true });
195
212
  }
196
213
  catch {
197
214
  return;
@@ -204,7 +221,7 @@ function collectFiles(dir, extensions, maxFiles, isIgnored = () => false) {
204
221
  const full = path.join(current, entry.name);
205
222
  if (entry.isDirectory()) {
206
223
  if (!isExcludedDirName(entry.name) && !isIgnored(full, true))
207
- walk(full);
224
+ await walk(full);
208
225
  }
209
226
  else if (entry.isFile() && extensions.includes(path.extname(full))) {
210
227
  if (isIgnored(full, false))
@@ -213,15 +230,15 @@ function collectFiles(dir, extensions, maxFiles, isIgnored = () => false) {
213
230
  }
214
231
  }
215
232
  }
216
- walk(dir);
233
+ await walk(dir);
217
234
  return files;
218
235
  }
219
236
  export function createLspDiagnosticsTool(
220
- // #571: same shared write-ordering token source `lens_diagnostics` mode=full
221
- // uses (index.ts injects `() => runtime.nextWriteIndex()`) — a confirmed
222
- // fresh result this tool reconciles into the footer draws a fresh token so
223
- // `WriteOrderingGuard` can tell it apart from a concurrent, genuinely newer
224
- // per-edit write for the same file. Optional/undefined in tests.
237
+ // #571/#1198: same shared write-ordering token source `lens_diagnostics`
238
+ // mode=full uses (index.ts injects `() => runtime.nextWriteIndex()`). A
239
+ // confirmed result reconciled into the footer reserves its token when the
240
+ // per-file check starts, so settlement order cannot make an older result look
241
+ // newer. Optional/undefined in tests.
225
242
  nextWriteIndex) {
226
243
  return {
227
244
  name: "lsp_diagnostics",
@@ -419,19 +436,14 @@ nextWriteIndex) {
419
436
  async function collectDiagnosticsForFile(absPath, lspService, waitMs, serverScope = "all") {
420
437
  let timedOut = false;
421
438
  let content;
422
- // #629: `touched` (when defined) is ALREADY the correctly-scoped,
423
- // already-collected diagnostics array for this touch — `touchFile` below
424
- // is called with `collectDiagnostics: true` and `clientScope: serverScope`,
425
- // so its return value only contains diagnostics from the servers
426
- // `serverScope` asked for. Previously this function discarded `touched`
427
- // (reading only `.inconclusive` off it) and made a SECOND, unconditionally
428
- // -unscoped `getDiagnostics()` call for the actual content — meaning every
429
- // touchFile-branch call paid for two LSP round trips instead of one, and
430
- // `serverScope: "primary"` never actually skipped the auxiliary scanners
431
- // (getDiagnostics always queries every registered server for the file).
432
- // `touched` is only undefined when touchFile itself couldn't produce a
433
- // result (service destroyed, no clients resolved) — that's the one case
434
- // that still needs the getDiagnostics() fallback below.
439
+ // `touchFile` is the authoritative collection boundary for every scope: it
440
+ // preserves per-touch timeout, content-binding, and silent-clean confirmation
441
+ // metadata while returning diagnostics from only the requested clients. The
442
+ // legacy openFile/getDiagnostics path remains only for an older/mock service
443
+ // without touchFile, or when touchFile cannot resolve any clients.
444
+ // #1179: the result is an explicit wrapper so side-channel fields survive
445
+ // array copies; confirmation was added when Marksman's lower-level clean verdict
446
+ // proved that a successful empty collection also needs explicit provenance.
435
447
  let touched;
436
448
  let usedTouch = false;
437
449
  try {
@@ -447,12 +459,26 @@ async function collectDiagnosticsForFile(absPath, lspService, waitMs, serverScop
447
459
  ? attached.response.diagnostics.filter((item) => item.source === primaryServerId(absPath))
448
460
  : attached.response.diagnostics;
449
461
  const filtered = applyAuxiliarySuppressions(scopedDiagnostics, content, { fileRole: detectFileRole(absPath, content) });
450
- return { diagnostics: filtered, timedOut: false, content };
462
+ return {
463
+ diagnostics: filtered,
464
+ timedOut: false,
465
+ // #1253: the incumbent's touch carries the same confirmation
466
+ // provenance a local touch does (an `available: true` answer is
467
+ // already gated on `fresh && !inconclusive`, but that is NOT
468
+ // evidence a silent-on-clean server was confirmed — only the
469
+ // explicit flag is). The incumbent always touches with
470
+ // `clientScope: "with-auxiliary"`, so this is an AGGREGATE
471
+ // confirmation: it carries the same caveat all-scope local
472
+ // touches do, and classic TypeScript still needs the primary-only
473
+ // tsserver sync check below rather than this verdict.
474
+ confirmedByTouch: attached.response.confirmation === "confirmed" &&
475
+ primaryServerId(absPath) !== "typescript",
476
+ content,
477
+ };
451
478
  }
452
479
  }
453
480
  const serviceWithTouch = lspService;
454
- if ((waitMs !== undefined || serverScope === "primary") &&
455
- typeof serviceWithTouch.touchFile === "function") {
481
+ if (typeof serviceWithTouch.touchFile === "function") {
456
482
  usedTouch = true;
457
483
  touched = await serviceWithTouch.touchFile(absPath, content, {
458
484
  diagnostics: "document",
@@ -480,7 +506,7 @@ async function collectDiagnosticsForFile(absPath, lspService, waitMs, serverScop
480
506
  // serverScope:"primary" actually skip auxiliary scanners and drops the
481
507
  // common case back to a single LSP round trip instead of two.
482
508
  const diagnostics = usedTouch && touched !== undefined
483
- ? touched
509
+ ? touched.diags
484
510
  : await lspService.getDiagnostics(absPath, waitMs !== undefined ? "document" : "full");
485
511
  // #586: honor each auxiliary profile's native inline-suppression comment
486
512
  // (e.g. opengrep's `// nosemgrep`, #441) the same way the per-edit dispatch
@@ -496,7 +522,18 @@ async function collectDiagnosticsForFile(absPath, lspService, waitMs, serverScop
496
522
  fileRole: detectFileRole(absPath, content),
497
523
  })
498
524
  : diagnostics;
499
- return { diagnostics: filtered, timedOut, content };
525
+ // #1095: surface the touch's content binding (only the touch path carries
526
+ // one; the openFile-only / getDiagnostics fallback leaves it undefined →
527
+ // "unknown", no demotion).
528
+ const binding = usedTouch ? touched?.binding : undefined;
529
+ const confirmedByTouch = usedTouch && touched?.confirmation === "confirmed";
530
+ return {
531
+ diagnostics: filtered,
532
+ timedOut,
533
+ confirmedByTouch,
534
+ content,
535
+ binding,
536
+ };
500
537
  }
501
538
  function diagnosticsToFileDiags(file, diagnostics) {
502
539
  return diagnostics.map((d) => ({
@@ -567,6 +604,17 @@ async function resolveEmptyResult(file, lspService) {
567
604
  return { confirmed: true, diagnostics: [] };
568
605
  }
569
606
  }
607
+ /**
608
+ * A primary-scope touch is authoritative because `touchFile` runs that
609
+ * primary's own confirmation path (including TypeScript's sync fallback). For
610
+ * all-scope TypeScript touches, the aggregate silent-clean gate can settle
611
+ * without that primary-only sync request, so preserve `resolveEmptyResult`'s
612
+ * existing tsserver fallback instead of accepting the aggregate verdict.
613
+ */
614
+ function canTrustTouchConfirmation(file, serverScope, confirmedByTouch) {
615
+ return (confirmedByTouch &&
616
+ (serverScope === "primary" || primaryServerId(file) !== "typescript"));
617
+ }
570
618
  /**
571
619
  * #571: reconcile this tool's fresh LSP result into the footer cache
572
620
  * (`widget-state.ts`'s `allDiagnostics`) — same shared choke point
@@ -574,6 +622,11 @@ async function resolveEmptyResult(file, lspService) {
574
622
  * `reconcileScanDiagnostics`). A manual `lsp_diagnostics` check that proves a
575
623
  * stale footer error is actually gone (the real-world case that surfaced
576
624
  * #571) is exactly the kind of confirmed result that should correct it.
625
+ * Direct `lsp_diagnostics` deliberately does not perform a second disk read at
626
+ * this reconciliation seam: its collecting touch already read the content and
627
+ * supplies the binding verdict. `false` is unconfirmed and is never written;
628
+ * `true` is trusted, while an absent/`unknown` verdict preserves the legacy
629
+ * fail-open policy for servers that cannot bind their diagnostics to content.
577
630
  *
578
631
  * `rawDiags` (pre-severity-filter) is what gets written — the footer records
579
632
  * the true known state, independent of this call's display-only severity
@@ -592,8 +645,22 @@ async function resolveEmptyResult(file, lspService) {
592
645
  * /skipTestFiles-filtered at write time — an empty string here is then a safe
593
646
  * no-op re-check, not a behavior gap).
594
647
  */
595
- function reconcileWidgetFromLspResult(file, rawDiags, confirmation, nextWriteIndex, cwd, content) {
596
- const confirmed = rawDiags.length > 0 || confirmation !== "unconfirmed";
648
+ function reconcileWidgetFromLspResult(file, rawDiags, confirmation, writeIndex, cwd, content,
649
+ // #1095: true when the result's content binding demonstrably mismatches disk
650
+ // (boundToCurrentDisk === false). Such a result — even a NON-EMPTY one, which
651
+ // the pre-#1095 "non-empty is definitionally confirmed" doctrine would have
652
+ // written straight through — must NOT re-cement the footer with a stale view
653
+ // (#1092's window-1 transcript: 17 live-looking diagnostics while tsc exits
654
+ // 0). "unknown"/true bindings leave the doctrine unchanged (fallback preserved
655
+ // for servers that never bind a version).
656
+ boundMismatch = false,
657
+ // #1093: when these `rawDiags` were actually OBSERVED. Fresh touches observe
658
+ // now (`undefined` → `Date.now()`); a cache HIT replays diagnostics scanned
659
+ // earlier, so its caller passes the cache entry's `scannedAt` here so the
660
+ // footer's `touchedAt` isn't re-armed to now() and the mtime-staleness gate
661
+ // stays live (the #1092 re-arming defect).
662
+ observedAt) {
663
+ const confirmed = (rawDiags.length > 0 || confirmation !== "unconfirmed") && !boundMismatch;
597
664
  if (!confirmed)
598
665
  return;
599
666
  try {
@@ -603,7 +670,7 @@ function reconcileWidgetFromLspResult(file, rawDiags, confirmation, nextWriteInd
603
670
  scanOrigin: "lsp_diagnostics",
604
671
  });
605
672
  const retagged = retagAuxiliaryDiagnostics(diagnostics, rawDiags, content ?? "", { cwd, fileRole: detectFileRole(file, content) });
606
- reconcileScanDiagnostics(file, retagged, true, nextWriteIndex?.());
673
+ reconcileScanDiagnostics(file, retagged, true, writeIndex, observedAt);
607
674
  }
608
675
  catch {
609
676
  // Never let a footer-reconciliation hiccup fail the diagnostics check.
@@ -621,6 +688,10 @@ cacheCtx, scopeKey,
621
688
  // `process.cwd()` for any call site that predates this (there are none
622
689
  // today besides the two below, both of which now pass it explicitly).
623
690
  cwd = process.cwd()) {
691
+ // Reserve the ordering token when this diagnostic operation starts, not when
692
+ // its LSP promise settles. A slower old result must not receive a newer token
693
+ // merely because it completed later (#1198).
694
+ const writeIndex = nextWriteIndex?.();
624
695
  let stat;
625
696
  try {
626
697
  stat = fs.statSync(file);
@@ -633,14 +704,27 @@ cwd = process.cwd()) {
633
704
  }
634
705
  if (cacheCtx && scopeKey !== undefined) {
635
706
  const cached = cacheCtx.lookup(file, scopeKey);
636
- if (cached) {
707
+ // #1095: a cached entry whose content binding demonstrably mismatches disk
708
+ // (bytes changed without the mtime bump the freshness gate would catch) is
709
+ // NOT served — fall through to a fresh touch instead of replaying a stale
710
+ // cached result. "unknown"/true bindings serve as before.
711
+ if (cached && cached.binding.boundToCurrentDisk !== false) {
637
712
  const filteredDiags = applySeverityFilter(cached.diagnostics, severity);
638
713
  const confirmation = cached.diagnostics.length === 0 ? "clean" : undefined;
639
714
  // #692: cached.diagnostics were already suppression-/skipTestFiles-
640
715
  // filtered at write time (this same code path); no file content was
641
716
  // cached alongside them, so `undefined` here is a safe re-check, not
642
717
  // a gap.
643
- reconcileWidgetFromLspResult(file, cached.diagnostics, confirmation, nextWriteIndex, cwd, undefined);
718
+ // #1093: this is a CACHE HIT — a replay of an OLD observation. Stamp the
719
+ // footer's `touchedAt` with the cache entry's original `scannedAt`, NOT
720
+ // now(), or a repeat check that only re-serves the cache would keep
721
+ // re-arming the mtime-staleness gate and a resolved finding would render
722
+ // forever (the #1092 defect).
723
+ reconcileWidgetFromLspResult(file, cached.diagnostics, confirmation, writeIndex, cwd, undefined,
724
+ // This branch only runs when the cache binding is not a mismatch
725
+ // (guarded above), so boundMismatch is false; #1093's observedAt is
726
+ // the cache entry's original scan time.
727
+ false, cached.scannedAt);
644
728
  return {
645
729
  file,
646
730
  diagnostics: diagnosticsToFileDiags(file, filteredDiags),
@@ -649,7 +733,7 @@ cwd = process.cwd()) {
649
733
  };
650
734
  }
651
735
  }
652
- const { diagnostics: rawDiags, timedOut, content: collectedContent, } = await collectDiagnosticsForFile(file, lspService, waitMs, serverScope);
736
+ const { diagnostics: rawDiags, timedOut, confirmedByTouch, content: collectedContent, binding, } = await collectDiagnosticsForFile(file, lspService, waitMs, serverScope);
653
737
  const health = lspService.getDiagnosticsHealth?.(file);
654
738
  // #570: a timed-out priming check is never a confirmed "clean" — treat it
655
739
  // as unconfirmed without consulting the (unrelated) silent-tier
@@ -665,6 +749,11 @@ cwd = process.cwd()) {
665
749
  confirmation = "unconfirmed";
666
750
  }
667
751
  }
752
+ else if (canTrustTouchConfirmation(file, serverScope, confirmedByTouch)) {
753
+ if (applySeverityFilter(rawDiags, severity).length === 0) {
754
+ confirmation = "clean";
755
+ }
756
+ }
668
757
  else if (rawDiags.length === 0) {
669
758
  const resolved = await resolveEmptyResult(file, lspService);
670
759
  effectiveRawDiags = resolved.diagnostics;
@@ -677,16 +766,26 @@ cwd = process.cwd()) {
677
766
  else if (applySeverityFilter(rawDiags, severity).length === 0) {
678
767
  confirmation = await classifyEmptyResult(file, lspService);
679
768
  }
769
+ // #1095: a result whose content binding demonstrably mismatches disk is
770
+ // demoted to "unconfirmed" — it must neither confirm the footer nor be cached
771
+ // as clean, regardless of how many diagnostics it carries (the #1092
772
+ // re-cementing path). "unknown"/true bindings leave the verdict untouched.
773
+ const boundMismatch = binding?.boundToCurrentDisk === false;
774
+ if (boundMismatch)
775
+ confirmation = "unconfirmed";
680
776
  const filteredDiags = applySeverityFilter(effectiveRawDiags, severity);
681
- reconcileWidgetFromLspResult(file, effectiveRawDiags, confirmation, nextWriteIndex, cwd, collectedContent);
777
+ reconcileWidgetFromLspResult(file, effectiveRawDiags, confirmation, writeIndex, cwd, collectedContent, boundMismatch);
682
778
  // #671: only a CONFIRMED outcome ("clean", or a non-empty result — either
683
779
  // is definitionally confirmed per this function's own doctrine above) is
684
780
  // safe to cache; "unconfirmed" (timeout OR a silent-tier server's
685
781
  // unescapable empty push) must never be persisted as a cacheable clean
686
782
  // result — same false-clean bug class `runWorkspaceDiagnostics`'s cache
687
- // wiring guards against.
783
+ // wiring guards against. #1095: the entry carries the content fingerprint so a
784
+ // later lookup can verify it against disk beyond the mtime proxy.
688
785
  if (cacheCtx && scopeKey !== undefined && confirmation !== "unconfirmed") {
689
- cacheCtx.record(file, scopeKey, effectiveRawDiags, stat.mtimeMs);
786
+ cacheCtx.record(file, scopeKey, effectiveRawDiags, stat.mtimeMs, collectedContent !== undefined
787
+ ? hashDiagnosticContent(collectedContent)
788
+ : undefined);
690
789
  }
691
790
  return {
692
791
  file,
@@ -698,7 +797,10 @@ cwd = process.cwd()) {
698
797
  };
699
798
  }
700
799
  async function runFileDiagnostics(absPath, severity, lspService, waitMs, nextWriteIndex, serverScope = "all", cwd = process.cwd()) {
701
- const { diagnostics: rawDiags, timedOut, content: collectedContent, } = await collectDiagnosticsForFile(absPath, lspService, waitMs, serverScope);
800
+ // Reserve the token before awaiting this file's LSP result. The direct-file
801
+ // path performs its own confirmation/reconciliation below (#1198).
802
+ const writeIndex = nextWriteIndex?.();
803
+ const { diagnostics: rawDiags, timedOut, confirmedByTouch, content: collectedContent, binding, } = await collectDiagnosticsForFile(absPath, lspService, waitMs, serverScope);
702
804
  const lspHealth = lspService.getDiagnosticsHealth?.(absPath);
703
805
  const unavailable = lspUnavailableMessage(absPath, lspHealth);
704
806
  // #533: an empty result needs a confirmed/unconfirmed verdict — a push-only,
@@ -717,6 +819,11 @@ async function runFileDiagnostics(absPath, severity, lspService, waitMs, nextWri
717
819
  confirmation = "unconfirmed";
718
820
  }
719
821
  }
822
+ else if (canTrustTouchConfirmation(absPath, serverScope, confirmedByTouch)) {
823
+ if (applySeverityFilter(rawDiags, severity).length === 0) {
824
+ confirmation = "clean";
825
+ }
826
+ }
720
827
  else if (rawDiags.length === 0) {
721
828
  const resolved = await resolveEmptyResult(absPath, lspService);
722
829
  effectiveRawDiags = resolved.diagnostics;
@@ -729,12 +836,18 @@ async function runFileDiagnostics(absPath, severity, lspService, waitMs, nextWri
729
836
  else if (applySeverityFilter(rawDiags, severity).length === 0) {
730
837
  confirmation = await classifyEmptyResult(absPath, lspService);
731
838
  }
839
+ // #1095: demote a result whose content binding mismatches disk to
840
+ // "unconfirmed" (the #1092 re-cementing path) — a non-empty stale result is no
841
+ // longer "definitionally confirmed". "unknown"/true bindings are unchanged.
842
+ const boundMismatch = binding?.boundToCurrentDisk === false;
843
+ if (boundMismatch)
844
+ confirmation = "unconfirmed";
732
845
  const filtered = applySeverityFilter(effectiveRawDiags, severity);
733
846
  const total = filtered.length;
734
847
  const truncated = total > MAX_DIAGNOSTICS;
735
848
  const limited = truncated ? filtered.slice(0, MAX_DIAGNOSTICS) : filtered;
736
849
  const unconfirmed = confirmation === "unconfirmed";
737
- reconcileWidgetFromLspResult(absPath, effectiveRawDiags, confirmation, nextWriteIndex, cwd, collectedContent);
850
+ reconcileWidgetFromLspResult(absPath, effectiveRawDiags, confirmation, writeIndex, cwd, collectedContent, boundMismatch);
738
851
  const primaryId = primaryServerId(absPath);
739
852
  const primaryDiags = limited.filter((d) => d.source === primaryId);
740
853
  const auxiliaryDiags = limited.filter((d) => d.source !== primaryId);
@@ -1072,7 +1185,7 @@ async function runDirectoryDiagnostics(absPath, severity, lspService, options) {
1072
1185
  let collectedFiles = [];
1073
1186
  const isIgnored = projectIgnorePredicate(absPath);
1074
1187
  for (const [ext, exts] of Object.entries(LANG_EXTENSIONS)) {
1075
- collectedFiles = collectFiles(absPath, exts, MAX_FILES + 1, isIgnored);
1188
+ collectedFiles = await collectFiles(absPath, exts, MAX_FILES + 1, isIgnored);
1076
1189
  if (collectedFiles.length > 0) {
1077
1190
  extension = ext;
1078
1191
  break;
@@ -8,6 +8,7 @@ import * as path from "node:path";
8
8
  import { fileURLToPath, pathToFileURL } from "node:url";
9
9
  import { Type } from "../clients/deps/typebox.js";
10
10
  import { logLatency } from "../clients/latency-logger.js";
11
+ import { newLspMutationCorrelationId, recordLspMutationOutcome, } from "../clients/lsp-mutation.js";
11
12
  import { compactRenderResult } from "./render-compact.js";
12
13
  import { applyWorkspaceEdit, summarizeWorkspaceEdit, } from "../clients/lsp/edits.js";
13
14
  import { getLSPService } from "../clients/lsp/index.js";
@@ -550,7 +551,7 @@ export function createLspNavigationTool(
550
551
  * constructed in; callers that only have a single fixed project root (e.g.
551
552
  * pi's own `getLensFlag`) may ignore the second argument.
552
553
  */
553
- getFlag) {
554
+ getFlag, mutationDeps) {
554
555
  return {
555
556
  name: "lsp_navigation",
556
557
  label: "LSP Navigate",
@@ -675,7 +676,15 @@ getFlag) {
675
676
  let supported = null;
676
677
  let diagnosticsMode = "unknown";
677
678
  let columnResolution;
679
+ let mutationContext;
680
+ let requestedApply = false;
678
681
  const finalize = (payload, meta) => {
682
+ if (payload.isError &&
683
+ requestedApply &&
684
+ mutationContext &&
685
+ ["rename", "rename_file", "executeCommand"].includes(meta.operation)) {
686
+ recordLspMutationOutcome(mutationContext, "failed");
687
+ }
679
688
  const normalizedFilePath = meta.filePath.replace(/\\/g, "/");
680
689
  logLatency({
681
690
  type: "phase",
@@ -759,6 +768,21 @@ getFlag) {
759
768
  });
760
769
  }
761
770
  const operation = normalizedOperation;
771
+ requestedApply = apply === true;
772
+ if (requestedApply &&
773
+ ["rename", "rename_file", "executeCommand"].includes(operation)) {
774
+ const cwd = ctx.cwd || ".";
775
+ mutationContext = {
776
+ cwd,
777
+ correlationId: newLspMutationCorrelationId(_toolCallId),
778
+ tool: "lsp_navigation",
779
+ source: "lsp-edit",
780
+ ...mutationDeps,
781
+ readGuard: getFlag("no-read-guard", cwd)
782
+ ? undefined
783
+ : mutationDeps?.readGuard,
784
+ };
785
+ }
762
786
  const isCallHierarchyTraversal = operation === "incomingCalls" || operation === "outgoingCalls";
763
787
  const needsFilePath = operation !== "workspaceDiagnostics" &&
764
788
  operation !== "workspaceSymbol" &&
@@ -1060,7 +1084,7 @@ getFlag) {
1060
1084
  edit,
1061
1085
  };
1062
1086
  }
1063
- const applied = await applyWorkspaceEdit(edit, ctx.cwd || ".");
1087
+ const applied = await applyWorkspaceEdit(edit, ctx.cwd || ".", { mutationContext });
1064
1088
  for (const touchedFile of applied.files) {
1065
1089
  try {
1066
1090
  await openFileBestEffort(lspService, touchedFile, false);
@@ -1081,6 +1105,7 @@ getFlag) {
1081
1105
  const result = await lspService.renameFile(filePath, resolvedNewFilePath, {
1082
1106
  cwd: ctx.cwd || ".",
1083
1107
  apply: apply ?? false,
1108
+ ...(mutationContext ? { mutationContext } : {}),
1084
1109
  });
1085
1110
  if (result.applied) {
1086
1111
  for (const touchedFile of result.files ?? []) {
@@ -1122,7 +1147,7 @@ getFlag) {
1122
1147
  if (!isAdvertised) {
1123
1148
  throw new Error(`__UNSUPPORTED__ command "${command}" is not advertised by the active LSP server (advertised: ${advertised.join(", ") || "none"})`);
1124
1149
  }
1125
- return lspService.executeCommand(rawPath ? filePath : undefined, command, commandArguments);
1150
+ return lspService.executeCommand(rawPath ? filePath : undefined, command, commandArguments, mutationContext);
1126
1151
  }
1127
1152
  case "incomingCalls": {
1128
1153
  if (!callHierarchyItem) {
@@ -1177,6 +1202,24 @@ getFlag) {
1177
1202
  }
1178
1203
  }
1179
1204
  }
1205
+ if (mutationContext &&
1206
+ requestedApply &&
1207
+ !mutationContext.summaryEmitted) {
1208
+ const outcome = operation === "rename_file" &&
1209
+ result &&
1210
+ typeof result === "object" &&
1211
+ "applied" in result &&
1212
+ result.applied === false
1213
+ ? "failed"
1214
+ : operation === "executeCommand" &&
1215
+ result &&
1216
+ typeof result === "object" &&
1217
+ "executed" in result &&
1218
+ result.executed === false
1219
+ ? "failed"
1220
+ : "skipped";
1221
+ recordLspMutationOutcome(mutationContext, outcome);
1222
+ }
1180
1223
  }
1181
1224
  catch (err) {
1182
1225
  const msg = err instanceof Error ? err.message : String(err);
@@ -29,6 +29,7 @@ export function createModuleReportTool(getProjectRoot) {
29
29
  description: "Structured, navigable overview of a source module — a token-efficient substitute for reading the whole file. Returns each symbol's name/kind/signature/line-range (plus a first-line `doc` summary when a doc comment is attached), important inline callbacks/closures/lambdas with stable handles, plus who-uses-this, risk flags, and ranked recommendedReads. To read a symbol's body: call read/read_symbol with offset=startLine, limit=endLine-startLine+1 on THIS report's `path` — those aren't repeated per symbol. Prefer this before a full read; then use read_symbol (or read) for the exact body you need.\n" +
30
30
  "Single mode: language-uniform tree-sitter outline + review-graph who-uses-this + inline executable extraction; degrades to outline-only when no cached graph is available. `semantic.source` reports whether graph data was used.\n" +
31
31
  'Pass `blastRadius: true` to also get the cross-file blast radius — the transitive dependents of this module aggregated to ranked file `read` args ("if you change this, verify these files"). Read-only over the cached graph; omitted on a cold cache. Supersedes the standalone impact query.\n' +
32
+ 'Pass `callGraph: true` to include bounded derived callers/callees from the cached FunctionCallGraph; unavailable cache state is explicit and never reported as zero calls.\n' +
32
33
  '`view: "compact"` returns a line-oriented text rendering (one line per symbol/callback, cheapest option) instead of JSON — same data, roughly a quarter of the token cost; use it for a quick skim. Default view returns JSON. An outline shows shape, not bodies — it does NOT count as having read a symbol\'s body for editing; use read_symbol for that.',
33
34
  promptSnippet: "Navigable file outline — a cheap substitute for reading a whole file",
34
35
  renderResult: compactRenderResult(({ details, args, isError }) => {
@@ -65,6 +66,12 @@ export function createModuleReportTool(getProjectRoot) {
65
66
  blastRadiusDepth: Type.Optional(Type.Number({
66
67
  description: "Max hops for the blast-radius walk (default 3). Only used with blastRadius.",
67
68
  })),
69
+ callGraph: Type.Optional(Type.Boolean({
70
+ description: "Include bounded derived callers/callees from the cached FunctionCallGraph; cold or stale cache state is explicit.",
71
+ })),
72
+ maxCallGraphEntries: Type.Optional(Type.Number({
73
+ description: "Per-direction cap for call-graph relations (default 20).",
74
+ })),
68
75
  }),
69
76
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
70
77
  // Resolve the file against the agent's cwd (sibling-tool convention); build
@@ -79,6 +86,8 @@ export function createModuleReportTool(getProjectRoot) {
79
86
  view: params.view,
80
87
  blastRadius: params.blastRadius,
81
88
  blastRadiusDepth: params.blastRadiusDepth,
89
+ callGraph: params.callGraph,
90
+ maxCallGraphEntries: params.maxCallGraphEntries,
82
91
  });
83
92
  }
84
93
  catch (err) {
@@ -122,7 +122,7 @@ unresolved blockers exist, with:
122
122
  > `🔴 COMMIT BLOCKED (--lens-guard): unresolved blockers must be fixed before
123
123
  > commit/push. … Run lens_diagnostics mode=all for full details, then commit again.`
124
124
 
125
- Don't try to route around it — clear the blockers, then commit.
125
+ Don't try to route around it — clear the blockers, then commit. The guard is strictly opt-in (off by default) and is marked **EXPERIMENTAL**. It gates only structured blocking findings, including blocking test failures under the current test-runner policy; advisory/no-action-required findings never gate. The blocker state is sequence- and session-bound, so an ambiguous or stale blocker record blocks conservatively until pi-lens runs again; advisory records never gate.
126
126
 
127
127
  ---
128
128
 
@@ -207,7 +207,7 @@ Verified in `index.ts`:
207
207
  | `/lens-toggle` | Turn pi-lens on/off for the session. |
208
208
  | `/lens-context-toggle` | Toggle context injection (tools/LSP/read-guard/formatting stay active). |
209
209
  | `/lens-widget-toggle` | Show/hide the diagnostics footer widget. |
210
- | `/lens-health` | Runtime health: pipeline crashes, slow runners, last dispatch latency. |
210
+ | `/lens-health` | Runtime health: pipeline crashes, slow runners, last dispatch latency, and a bounded degradation-ledger summary (trust refusals, LSP breakers, formatter skips/failures, idle evictions, WASM aborts, timeout tallies). |
211
211
  | `/lens-perf` | Slowest latency-log phases (p50/p99). |
212
212
  | `/lens-tools` | Tool installation status (global / auto-installed / npx fallback). |
213
213
  | `/lens-tdi` | Technical Debt Index and project health trend. |
@@ -14,9 +14,11 @@ share) — current exceptions: `ast_grep_outline` and `ast_grep_dump`
14
14
  are registered but
15
15
  inactive by default; the model activates the ones it needs via the always-active
16
16
  loader tool `pi_lens_activate_tools`, per pi's dynamic-tool-loading API
17
- (`pi.setActiveTools`/`pi.getActiveTools`). Feature-detected: on hosts without
18
- that API, the five situational tools fall back to being statically active,
19
- exactly as before (`tools/activate-tools.ts`, wired in `index.ts`).
17
+ (`pi.setActiveTools`/`pi.getActiveTools`). The loader explicitly reports
18
+ "Available starting next turn"; do not retry the tool in the same turn.
19
+ Feature-detected: on hosts without that API, the six situational tools fall back
20
+ to being statically active, exactly as before (`tools/activate-tools.ts`, wired
21
+ in `index.ts`).
20
22
 
21
23
  ## Per-edit
22
24
 
@@ -40,13 +42,22 @@ exactly as before (`tools/activate-tools.ts`, wired in `index.ts`).
40
42
  - **`ast_grep_search`** — AST-aware structural search across ~40 languages via
41
43
  the `sg` CLI. Supports metavariables (`$VAR`, `$$$ARGS`), `strictness`
42
44
  modes (`smart`, `relaxed`, `ast`, `cst`, `signature`, `template`), structural
43
- constraints (`insideKind`, `hasKind`, `follows`, `precedes`), raw YAML `rule`
44
- passthrough, `validateOnly` for compile/shape checks without scanning project
45
- files, and pagination via `skip` / `maxMatches` (per-call cap, default 50,
46
- max 200; also sets the pagination step). `groupByFile: true` renders a compact
47
- one-line-per-file distribution (`L<line>:<col>` locations) instead of each
48
- match body — for high-volume searches. `pattern` is optional when a `rule` is
49
- given.
45
+ constraints (`insideKind`, `hasKind`, `hasDescendantKind`, `follows`,
46
+ `precedes`), raw YAML `rule` passthrough, `validateOnly` for compile/shape
47
+ checks without scanning project files, and pagination via `skip` / `maxMatches`
48
+ (per-call cap, default 50, max 200; also sets the pagination step).
49
+ `nodeKind` is an expert grammar-specific escape hatch: it finds every node of
50
+ the exact kind used by the target grammar. Node kinds are not universal across
51
+ languages; use `ast_grep_dump` to discover the kind in the target language. It is mutually exclusive with `pattern` and `rule`.
52
+ `hasKind` retains ast-grep's immediate-child semantics; use
53
+ `hasDescendantKind` for an explicit recursive descendant search. A future
54
+ canonical `find`/`query` facade (call/function/import/etc.) should map to
55
+ this same synthesized YAML path, with per-language templates and a clear
56
+ unsupported-concept error — it is intentionally not a second search engine
57
+ in this patch.
58
+ `groupByFile: true` renders a compact one-line-per-file distribution
59
+ (`L<line>:<col>` locations) instead of each match body — for high-volume
60
+ searches. `pattern` is optional when a `rule` or `nodeKind` is given.
50
61
  Results include `details.matchLocations[]` — each hit carries a ready
51
62
  `readSlice` (`path`/`offset`/`limit`) for a bounded context read; zero-match
52
63
  results include a `suggestedDump` hint pointing at `ast_grep_dump`.